Skip to content

fix(Dialog): capture the opener before descendant autofocus effects - #6607

Open
ManoharPaturi wants to merge 1 commit into
facebook:mainfrom
ManoharPaturi:fix/dialog-trigger-capture-on-mount
Open

ManoharPaturi wants to merge 1 commit into
facebook:mainfrom
ManoharPaturi:fix/dialog-trigger-capture-on-mount

Conversation

@ManoharPaturi

Copy link
Copy Markdown
Contributor

Fixes #5637

Summary

A controlled Dialog whose content mounts in the open commit restored focus to <body> on close. Two defects, both fixed:

  1. The opener was captured too late. The Dialog captured document.activeElement in its passive effect, but React runs child passive effects before the parent's — so a freshly mounted DialogHeader focused its own title first, and the Dialog captured its own heading. When closing unmounts that content, focus restoration targeted a detached node and the user's focus stranded on <body>. The capture now runs in a useLayoutEffect on the open edge: all layout effects run before any passive effect, so the capture always sees the element the user was on before the dialog's descendants moved focus.
  2. DialogHeader focused its title while the dialog was closed. Its shouldAutoFocus check only excluded inline mode, and the standard path renders children (hidden) while closed, so a header mounted under isOpen={false} stole focus onto an invisible element. DialogContext now carries isOpen, and the header suppresses autofocus while closed — its effect re-runs when the dialog opens, so the title still becomes the screen reader anchor on open, exactly as before for content that mounts in the open commit.

Tests

  • Regression 1 (the issue's minimal shape): opener button → open with content mounting in the same commit → title focused while open → close → document.activeElement is the opener. Failed on main (focus landed on <body>), passes here.
  • Regression 2: DialogHeader mounted under isOpen={false} leaves focus on <body>. Failed on main (title stolen focus), passes here.
  • Full Dialog directory: 85 passed. Dependent suites (CommandPalette, BottomSheet, Popover, Layer): 490 passed.

Verification

tsc --noEmit clean (core); eslint 0 problems on touched files; prettier clean; check:changesets clean. Changeset included.

Copilot AI lite review requested due to automatic review settings September 24, 2026 19:01
@vercel

vercel Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Sep 24, 2026 7:06pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Sep 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge labels Sep 24, 2026
github-actions Bot added a commit that referenced this pull request Sep 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.9KB 1.2KB

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

25 of 420 shot(s) changed. View the report

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Chat Full AI Chat probe light 42,708
Chat Empty State probe light 42,708
Chat Full AI Chat probe dark 41,786
Chat Empty State probe dark 41,786
Slider Default probe light 15,617
Slider Default probe dark 15,615
Chat With Attachments probe light 12,964
Chat With Attachments probe dark 12,114
InputGroup With Typeahead probe light 11,012
InputGroup With Typeahead probe dark 9,189
ComplexSelector Tree list with search probe light 6,245
TextArea Vertical Form Alignment probe light 5,634
ComplexSelector Tree list with search probe dark 5,602
TextArea Vertical Form Alignment probe dark 5,047
ComplexSelector Fruit ripeness selector neutral light 4,007
ComplexSelector Fruit ripeness selector neutral dark 3,825
ToggleButton Group Single probe dark 1,538
ToggleButton Group Single probe light 1,538
MediaTheme On Dark neutral dark 838
Banner Collapsible Content (Expanded) probe dark 92

and 5 more.

Chat — Full AI Chat — probe light
BeforeAfterDiff
Before visual regression frame After visual regression frame Pixel difference frame
Chat — Empty State — probe light
BeforeAfterDiff
Before visual regression frame After visual regression frame Pixel difference frame
Chat — Full AI Chat — probe dark
BeforeAfterDiff
Before visual regression frame After visual regression frame Pixel difference frame

Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot

astracat-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟡 Reviewed (for maintainers only) Sep 24, 2026, 8:07 PM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the focus fix itself looks right and matches the Dialog contract: capturing the opener in a layout effect means a DialogHeader that mounts in the open commit can no longer replace the return target, and suppressing header autofocus while the dialog is closed stops focus landing on an invisible title. The new regressions cover both shapes, and the Dialog and dependent suites pass.

One change is needed before this lands:

  • packages/core/src/Dialog/__tests__/Dialog.a11y.known-failures.ts still records modal-dialog.focus.restored for the conditional-content-focus-restoration state as a current failure ("closing the modal dialog did not return focus to its still-available invoker"). This PR makes that outcome pass, and the Chromium accessibility contract requires every known-failure record to match exactly one executed result — an unmatched record fails Dialog.a11y.chromium.spec.ts when it runs at release-check (it does not run in PR CI, which is why checks are green). Please delete just that record, keeping the enters-after-modal record, whose focus-entry ordering this PR does not change and which remains accurate. Running the Dialog Chromium accessibility spec after the removal, where a browser is available, would confirm it.

No other blocking issues were found; the pre-existing focus-entry ordering issue stays recorded and is out of scope here.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — a5ea03b3 Deployed Sep 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dialog captures the wrong trigger when focusable content mounts on open

2 participants