Skip to content

[all components] Contain modal focus across nested popup roots - #6197

Closed
FelixTJDietrich wants to merge 6 commits into
mui:masterfrom
FelixTJDietrich:fix/modal-background-containment
Closed

FelixTJDietrich wants to merge 6 commits into
mui:masterfrom
FelixTJDietrich:fix/modal-background-containment

Conversation

@FelixTJDietrich

@FelixTJDietrich FelixTJDietrich commented Nov 6, 2026 •

Copy link
Copy Markdown

A modal popup can hide background controls from accessibility APIs while leaving them focusable. Apply native inert at the owning popup layer so background controls stay excluded and nested popups remain usable across portals.

Addresses #4678; related to #4714. Public props remain unchanged.

Implementation

  • Coordinate active modal ownership using Root ancestry, registered trigger ownership and open sessions. Preserve detached handles, initially open children, independent sibling modals and external child portals.
  • Reuse the outside-element traversal, preserve author-owned inert attributes, and refresh dynamic descendants and known open/closed shadow roots. The focus manager rejects same-task outside focus before observer reconciliation.
  • Preserve backdrops, focus guards, explicit initial-focus policies and eligible return targets. Retire temporary outside-focus exceptions when focus enters the popup or a descendant portal. Keep the existing hover-open exclusion policy and blur/validation behavior.
  • Keep explicit and implicit live announcements accessible while excluding their interactive descendants. Capture outside live-region activation, including shadow descendants, without blocking legitimate modal content inside a closed root.
  • Complete settled animations without waiting for another rendering frame. Continue waiting for actual new CSS exit animations and preserve queued lookup cancellation. Trigger switching owns the frame wait needed for its asynchronous positioning update.

The change belongs in Base UI's interaction runtime. Consumers need no second focus trap, wrapper observer or repository-only dependency patch.

Regression tests

The shared modal suite covers ownership, portals, shadow roots, initial/return focus, dynamic controls, announcement accessibility and cleanup. It includes positive controls proving inside activation and post-unmount outside activation still work.

The animation helper tests cover stalled rendering frames, already-finished animations and real newly introduced CSS exit animations/transitions. Existing batching, cancellation and StrictMode tests remain intact. Existing Menu and Popover trigger-switch regressions protect motion and cancellation.

The two native Toast expiry tests now control only timers and Date; real pointer input, window focus and animation frames remain real. They assert pause and subsequent expiry after interaction ends, instead of racing a 300 ms toast against browser automation. The existing scrub-dismissal conformance input uses a fresh renderer user.click press/release rather than a click-only synthetic event. Assertions and timeouts were not weakened, and no new skips or retries were added.

Deliberately breaking pause, resume, outside activation exclusion, closed-shadow admission, frame-independent completion and finished-animation handling fails the corresponding regressions. Additional existing focus/validation counterfactuals are retained in local evidence.

Validation

Check Local result
Full Chromium, final source 10,924 passed, 407 existing skips
Full React 19 JSDOM, final source 9,590 passed, 1,711 existing skips
Focused Firefox and WebKit: modal, animation helper and Toast viewport 134 passed per engine, 11 existing skips
Linux modal and animation-helper source suites 69 passed per engine in Chromium/Firefox/WebKit
Build-mode TypeScript, full-repository ESLint and changed-file Prettier Passed
Official ESM/CJS/declaration build, publint and declaration resolution Passed on the matching Linux source snapshot
Packed downstream PR22, Linux Chromium/Firefox/WebKit 123/123 passed with the corrected local runtime
Whole-library bundle impact reported by CI +6.88 KB parsed / +2.03 KB gzip (+1.33% / +1.21%)
CI performance campaign No regressions in 36 benchmarks

The earlier full React 18 JSDOM repair passed locally; the complete React 18 matrix must be confirmed on the final pushed source by upstream CI. The final packed downstream campaign replaces neither normal declared-dependency CI nor durable dependency delivery: it deliberately installs a local candidate archive and verifies matching bytes and a single shared Base UI runtime.

Full upstream CI is rerunning on the final commits. A maintainer must apply a label for the repository's test-label-applied check. This PR does not publish an npm release or adopt a preview dependency in downstream production manifests. Manual screen-reader speech remains unverified.

@pkg-pr-new

pkg-pr-new Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

commit: c802621

@code-infra-dashboard

code-infra-dashboard Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+6.88KB(+1.33%) 🔺+2.03KB(+1.21%)

Details of bundle changes

Performance

No regressions in 36 benchmarks · ran 3m 52s · details


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

@netlify

netlify Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for base-ui ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit c802621
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6aee1912dadbe27a21abafb8
😎 Deploy Preview https://deploy-preview-6197--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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant