Skip to content

[popover] Keep focus that moved elsewhere before the deferred focus restore - #6015

Merged
atomiks merged 1 commit into
mui:masterfrom
cristianchiriac:fix-deferred-focus-restore
Oct 20, 2026
Merged

atomiks merged 1 commit into
mui:masterfrom
cristianchiriac:fix-deferred-focus-restore

Conversation

@cristianchiriac

Copy link
Copy Markdown
Contributor

Fixes #5980

FloatingFocusManager's handleFocusOutside with restoreFocus: 'popup' (which DialogPopup and PopoverPopup pass) focuses the popup when the focused element is removed and focus falls to <body>. It then focuses it again on the next animation frame, to win the race where the browser moves focus back to the removed element. That deferred focus was unconditional. If the app or the user moved focus somewhere else before the frame ran, it was pulled back to the popup. For example, app code that focuses another control right after a removal lost that focus, and a nested non-modal Popover opened in that window closed with reason focus-out.

The frame callback now re-focuses the popup only while focus is still lost (activeElement is <body> or null, read with the shadow-DOM-safe activeElement utility). The race it exists for is unchanged: in that case focus is lost when the frame runs, so the popup is still re-focused.

Tests (FloatingFocusManager.test.tsx, prop: restoreFocus, Chromium only like the other removal tests there):

  • popup: keeps focus that moved elsewhere before the deferred re-focus: removes the focused button and focuses another one before the frame runs. Frames are held with a requestAnimationFrame spy so the order is deterministic. Fails before this change (the popup steals focus) and passes after.
  • popup: re-focuses the popup when focus is still lost on the next frame: the sibling case with focus lost again before the frame. The popup is re-focused, and the test fails if the deferred focus is removed.

Validation:

  • pnpm test:chromium FloatingFocusManager Dialog Popover --no-watch: 1001 passed.

  • pnpm test:jsdom FloatingFocusManager Dialog Popover --no-watch: 832 passed.

  • eslint and prettier --check on both files, and tsc --noEmit for @base-ui/react: clean.

  • I have followed (at least) the PR section of the contributing guide.

@pkg-pr-new

pkg-pr-new Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

commit: dc4e9c8

@code-infra-dashboard

code-infra-dashboard Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+35B(+0.01%) 🔺+20B(+0.01%)

Details of bundle changes

Performance

Total duration: 387.39 ms -57.69 ms(-13.0%) | Renders: 75 (+0) | Paint: 714.09 ms -84.55 ms(-10.6%)

Test Duration Renders
Menu open (500 items) 23.80 ms 🔺+6.74 ms(+39.6%) 10 (+0)
Mixed surface mount (app-like density) 33.57 ms ▼-21.78 ms(-39.3%) 5 (+0)
Scroll Area mount (300 instances) 30.29 ms ▼-12.06 ms(-28.5%) 3 (+0)
Combobox type — 500 items, all stay mounted (type "Row ") 10.55 ms ▼-6.73 ms(-39.0%) 11 (+0)
Combobox open — 500 items 8.84 ms ▼-4.32 ms(-32.8%) 4 (+0)

…and 1 more (+9 within noise) — details

Metric alarms

Test Metric Change
Menu open (500 items) bench:paint 🔺 +15.72 ms
Menu open (500 items) bench:paint#menu-open 🔺 +15.72 ms

Check out the code infra dashboard for more information about this PR.

@netlify

netlify Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit dc4e9c8
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6ad6f43308019627e7a8da69
😎 Deploy Preview https://deploy-preview-6015--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@atomiks atomiks added type: bug It doesn't behave as expected. scope: all components Widespread work has an impact on almost all components. labels Oct 20, 2026

@atomiks atomiks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the fix

…estore

With restoreFocus 'popup', the popup was re-focused on the next animation
frame unconditionally, pulling back focus that the app or the user had
moved in the meantime. Only re-focus while focus is still lost.
@atomiks
atomiks force-pushed the fix-deferred-focus-restore branch from 5ee616e to dc4e9c8 Compare October 20, 2026 04:55
@atomiks
atomiks merged commit b45439f into mui:master Oct 20, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: all components Widespread work has an impact on almost all components. type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[dialog][popover] Deferred focus restoration after removing the focused element steals focus that moved elsewhere

2 participants