Skip to content

feat(react-headless-components-preview): add trapFocus prop#36123

Merged
mainframev merged 3 commits into
microsoft:masterfrom
mainframev:feat/headless-popover-focustrap
May 21, 2026
Merged

feat(react-headless-components-preview): add trapFocus prop#36123
mainframev merged 3 commits into
microsoft:masterfrom
mainframev:feat/headless-popover-focustrap

Conversation

@mainframev

@mainframev mainframev commented May 8, 2026

Copy link
Copy Markdown
Contributor

Previous Behavior

Popover had no focus trap. The surface rendered as a <div popover="auto"> and relied entirely on the browser's light-dismiss (Escape, click-outside, popover-stack peer dismissal).

New Behavior

Adds a trapFocus prop on Popover that delegates modal behavior. The surface always renders as a single element; the show mode is wired in usePopover:

  • trapFocus={false} (default) — surface.showPopover(). Browser owns light dismiss; no focus trap or autofocus. Explicit role="group" overrides the dialog's implicit role="dialog" so assistive tech
    doesn't announce modal semantics that aren't present.
  • trapFocus={true} — surface.showModal(). The platform supplies the focus trap, autofocus, trigger-restoration on close, and inert background — all spec-mandated by . The explicit role is
    dropped so the implicit role="dialog" + aria-modal="true" apply. The cancel event is intercepted (preventDefault + close via onOpenChange) so Escape flows through the same React state path as any other
    dismiss.

A single element supporting both modes means consumers flip one prop instead of swapping component variants, and there's no portal/stacking-context divergence between the two paths.

IMPORTANT NOTE:

There one notable behavioral difference compared to a custom focus trap implementation: By default with a native dialog, all elements within the same document, except the dialog and its descendants become inert, but it is scoped only to the containing document. If the dialog is rendered inside an iframe, the rest of the parent page remains interactive (focusable), which differs to v9 behaviour

Example: https://dialog-focus-trap-test.surge.sh/

@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-headless-components-preview
react-headless-components-preview: entire library
157.13 kB
46.022 kB
157.511 kB
46.154 kB
381 B
132 B

🤖 This report was generated against 8531d26ae750f91537ced80d6679bc406acd4d23

@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown

Pull request demo site: URL

@mainframev mainframev force-pushed the feat/headless-popover-focustrap branch 2 times, most recently from 9a7ebfb to d0ef8d5 Compare May 8, 2026 22:45
@mainframev mainframev marked this pull request as ready for review May 11, 2026 01:22
@mainframev mainframev requested a review from a team as a code owner May 11, 2026 01:22
@mainframev mainframev changed the title feat(react-headless-components-preview): add focustrap feat(react-headless-components-preview): add trap focus support May 14, 2026
@mainframev mainframev changed the title feat(react-headless-components-preview): add trap focus support feat(react-headless-components-preview): add trap focus prop May 14, 2026
@mainframev mainframev changed the title feat(react-headless-components-preview): add trap focus prop feat(react-headless-components-preview): add trapFocus prop May 14, 2026
@mainframev mainframev force-pushed the feat/headless-popover-focustrap branch from d0ef8d5 to 2c19f87 Compare May 20, 2026 01:44
@mainframev mainframev merged commit c2f4e86 into microsoft:master May 21, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants