Mark background content inert while a modal dialog is open - #299
Open
sagikazarmark wants to merge 1 commit into
Open
Mark background content inert while a modal dialog is open#299sagikazarmark wants to merge 1 commit into
sagikazarmark wants to merge 1 commit into
Conversation
A modal `DialogContent` installs a focus trap that only intercepts Tab, so everything behind the dialog stayed reachable: by a pointer, by a programmatic `focus()` from application code, by a background element that autofocuses on mount, and — depending on the screen reader — by a browse mode cursor. The trap also binds to its own container, so once focus leaves by any non-Tab route no handler fires and it is never brought back. `aria-modal="true"` is set, but support for it has been inconsistent enough that the WAI-ARIA Authoring Practices and MDN both recommend pairing it with `inert` on background content. While the trap is installed, walk from the dialog up to `<body>` and mark each ancestor's other children `inert`. Each dialog attributes its marks to an owner id in a `data-inert-by` marker, so `inert` the application had already set — which carries no marker — is left alone. The whole state is recomputed from the set of open dialogs on every open and close, which is what makes stacking compose: the dialog installed last is on top, so nothing on its path to `<body>` is marked, and every dialog underneath still marks its own background and so ends up inert itself. The unwind is addressed by owner id rather than by a recorded element list, so it also works when the dialog is unmounted while open. The owner id comes from Rust rather than being derived from the element id, so that marking and unwinding cannot disagree about it, and so that a caller supplied id containing a space cannot corrupt the space separated marker. Also fix focus restoration: `FocusTrap.remove()` focused the element that was active when the trap was built without checking it was still in the document. If the opener was unmounted while the dialog was open — routine when the dialog's action re-renders the view behind it — focus fell to `<body>` and the keyboard position was lost. It now falls back to the main landmark, made focusable only for as long as it holds focus. `DialogRoot` and `AlertDialogRoot` take an `inert_background` prop (default true) for callers who manage `inert` themselves. The focus trap effect moves to a shared `use_focus_trap` hook so dialog and alert dialog compose by construction. Claude-Session: https://claude.ai/code/session_018qMUbzVT7pawv31DChxknW
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A modal
DialogContentinstalls a focus trap that only intercepts Tab, so everything behind the dialog stayed reachable: by a pointer, by a programmaticfocus()from application code, by a background element that autofocuses on mount, and — depending on the screen reader — by a browse mode cursor. The trap also binds to its own container, so once focus leaves by any non-Tab route no handler fires and it is never brought back.aria-modal="true"is set, but support for it has been inconsistent enough that the WAI-ARIA Authoring Practices and MDN both recommend pairing it withinerton background content.While the trap is installed, walk from the dialog up to
<body>and mark each ancestor's other childreninert. Each dialog attributes its marks to an owner id in adata-inert-bymarker, soinertthe application had already set — which carries no marker — is left alone. The whole state is recomputed from the set of open dialogs on every open and close, which is what makes stacking compose: the dialog installed last is on top, so nothing on its path to<body>is marked, and every dialog underneath still marks its own background and so ends up inert itself. The unwind is addressed by owner id rather than by a recorded element list, so it also works when the dialog is unmounted while open.The owner id comes from Rust rather than being derived from the element id, so that marking and unwinding cannot disagree about it, and so that a caller supplied id containing a space cannot corrupt the space separated marker.
Also fix focus restoration:
FocusTrap.remove()focused the element that was active when the trap was built without checking it was still in the document. If the opener was unmounted while the dialog was open — routine when the dialog's action re-renders the view behind it — focus fell to<body>and the keyboard position was lost. It now falls back to the main landmark, made focusable only for as long as it holds focus.DialogRootandAlertDialogRoottake aninert_backgroundprop (default true) for callers who manageinertthemselves. The focus trap effect moves to a shareduse_focus_traphook so dialog and alert dialog compose by construction.Fixes Status: #298