Skip to content

Drawer: full focus containment (body-recovery + portal-aware owner stack) #43

Description

@mgoldsborough

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:

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions