Follow-up to #40 / #41. #41 lands the Tab/Shift+Tab trap (keydown wrap) — focus is contained while it's inside the panel, which is the modal case. It deliberately does not attempt the full native inert a <dialog>.showModal() gave, because a <div> overlay can't get it for free and a naive approximation is worse than none:
-
Focus lost to <body> — when a focused child unmounts (a re-keyed control, a controlled↔uncontrolled swap, a removed list row), the browser fires focusout only (no focusin) and document.activeElement becomes <body>. A focusin backstop can't see this; the keydown trap is also dead from <body>. Recovering needs a focusout+relatedTarget==null handler (guarded by panel.isConnected && activeElement === document.body so it doesn't fight the close teardown's focus-restore), or a MutationObserver.
-
Nested / portal'd overlays — a document-scoped focus backstop yanks focus from any element outside the panel, so a legitimately-open portal'd overlay (dropdown, date picker, confirm) becomes keyboard-unreachable, and two open Drawers fight. Needs a module-level owner stack (only the topmost overlay contains) plus a way to exempt descendant/portal scopes.
This is the WAI-ARIA "modal dialog contains focus" guarantee done properly — a FocusScope, ~Radix's FocusScope/react-focus-lock territory. Scope it as its own primitive/effort, not bolted onto the Tab-wrap. The #41 docstring's open-effect scope note points here.
Follow-up to #40 / #41. #41 lands the Tab/Shift+Tab trap (keydown wrap) — focus is contained while it's inside the panel, which is the modal case. It deliberately does not attempt the full native
inerta<dialog>.showModal()gave, because a<div>overlay can't get it for free and a naive approximation is worse than none:Focus lost to
<body>— when a focused child unmounts (a re-keyed control, a controlled↔uncontrolled swap, a removed list row), the browser firesfocusoutonly (nofocusin) anddocument.activeElementbecomes<body>. Afocusinbackstop can't see this; the keydown trap is also dead from<body>. Recovering needs afocusout+relatedTarget==nullhandler (guarded bypanel.isConnected && activeElement === document.bodyso it doesn't fight the close teardown's focus-restore), or a MutationObserver.Nested / portal'd overlays — a document-scoped focus backstop yanks focus from any element outside the panel, so a legitimately-open portal'd overlay (dropdown, date picker, confirm) becomes keyboard-unreachable, and two open Drawers fight. Needs a module-level owner stack (only the topmost overlay contains) plus a way to exempt descendant/portal scopes.
This is the WAI-ARIA "modal dialog contains focus" guarantee done properly — a FocusScope, ~Radix's
FocusScope/react-focus-lockterritory. Scope it as its own primitive/effort, not bolted onto the Tab-wrap. The#41docstring's open-effect scope note points here.