fix(menu): Keep persistent elements interactive while a modal menu is open#1062
fix(menu): Keep persistent elements interactive while a modal menu is open#1062engfragui wants to merge 1 commit into
Conversation
… open A modal MenuList marks the element tree outside it as inert, which removes those elements from hit-testing. On desktop apps (e.g. Electron) this breaks the native window drag region (-webkit-app-region: drag) of the title bar, since inert elements no longer receive the OS drag gesture. Default MenuList's getPersistentElements to exempt any element marked with data-reactist-persist from the inert barrier, keeping it interactive while the menu stays modal in every other respect. A consumer-provided getPersistentElements is still honored and merged with the default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
doistbot
left a comment
There was a problem hiding this comment.
This PR enables elements like native OS title bars to remain interactive while a modal MenuList is open by exempting them from the inert barrier.
Few things worth tightening:
- Merging the default selector changes the Ariakit
getPersistentElementscontract to be append-only, preventing callers from opting back into strict modal behavior—consider applying the default only when unset or providing a custom Reactist prop. - Running a document-wide
querySelectorAllwhenever persistent elements resolve adds unnecessary overhead to a common interaction path—this scan should ideally be cached or gated behind an explicit opt-in.
I also included a few optional follow-up notes in the details below.
Optional follow-up note (1)
- [P3] src/menu/menu.test.tsx:429: The implementation successfully merges
getPersistentElementswithgetPersistentElementsDefault(). However, this test doesn't verify the merge because it lacks adata-reactist-persistelement. If the code mistakenly overwrote the defaults instead of merging them, this test would still pass. Consider adding a<div data-testid="default-persist" data-reactist-persist />to this DOM and asserting that it also avoids thearia-hiddenattribute.
| const consumerElements = getPersistentElements | ||
| ? Array.from(getPersistentElements()) | ||
| : [] | ||
| return [...getPersistentElementsDefault(), ...consumerElements] |
There was a problem hiding this comment.
🟡 P2 MenuListProps inherits Ariakit’s getPersistentElements, so callers will expect this prop to define the complete persistent-element set. Merging the default selector here turns it into an append-only hook: once an app adds data-reactist-persist anywhere, every MenuList will keep those elements interactive and a caller can’t opt back into strict modal behavior for a specific menu. That’s a surprising contract change for a pass-through Ariakit prop. Consider applying the default only when getPersistentElements is unset, or introducing a separate Reactist prop for the title-bar escape hatch.
| const consumerElements = getPersistentElements | ||
| ? Array.from(getPersistentElements()) | ||
| : [] | ||
| return [...getPersistentElementsDefault(), ...consumerElements] |
There was a problem hiding this comment.
🟡 P2 This makes every modal MenuList pay for document.querySelectorAll('[data-reactist-persist]') whenever Ariakit resolves persistent elements, even in apps that never use this feature. Since menus are a repeated interaction path, walking the whole document and allocating fresh arrays here is avoidable overhead. Please gate the default lookup behind an explicit opt-in or cache the selector results for the lifetime of the open menu instead of rescanning the full DOM on each callback.
Summary
A modal
MenuListmarks the element tree outside it asinert, which removes those elements from hit-testing. On desktop apps (e.g. Electron) this breaks the native window drag region (-webkit-app-region: drag) of the title bar, sinceinertelements no longer receive the OS drag gesture.This change defaults
MenuList'sgetPersistentElementsto exempt any element marked withdata-reactist-persistfrom the inert barrier, keeping it interactive while the menu stays modal in every other respect. A consumer-providedgetPersistentElementsis still honored and merged with the default.Reference
Usage
Mark the element that should stay interactive (e.g. the draggable title bar):