Skip to content

[Modal] Fix stale aria-hidden when the container changes - #48889

Open
atharv-sys32 wants to merge 3 commits into
mui:masterfrom
atharv-sys32:fix/aria-hidden-dialog-container
Open

[Modal] Fix stale aria-hidden when the container changes#48889
atharv-sys32 wants to merge 3 commits into
mui:masterfrom
atharv-sys32:fix/aria-hidden-dialog-container

Conversation

@atharv-sys32

@atharv-sys32 atharv-sys32 commented Aug 1, 2026

Copy link
Copy Markdown

Related to #19450 . Another PR attempt to fix: #43318

Root cause

When a Modal's container prop changes while the modal stays open, the modal is moved into the new container but the previous container keeps the aria-hidden state that was computed for the previous sibling set. For example, a modal mounted in document.body (default container) marks #root as aria-hidden; switching container to a node inside #root leaves #root hidden, making the dialog inaccessible to assistive technologies (getByRole('dialog') fails).

Changes

  • useModal: track the container the modal is registered with (registeredContainerRef) and re-register the modal with the ModalManager when the resolved container changes while open.
  • ModalManager: when removing the last modal of a container, restore siblings using the container itself as the blacklist entry instead of modal.mount. The mount node is already updated to the new container by the portal before the manager is notified, which previously kept the new container in the blacklist and left it aria-hidden.

Tests

  • ModalManager.test.ts: unit test covering the re-registration flow to a nested container.
  • Modal.test.js: regression test that changes the container prop after mount and asserts the previous container loses aria-hidden.

@code-infra-dashboard

code-infra-dashboard Bot commented Aug 1, 2026

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-48889--material-ui.netlify.app/
QR code for https://deploy-preview-48889--material-ui.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+135B(+0.03%) 🔺+51B(+0.03%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


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

@atharv-sys32

Copy link
Copy Markdown
Author

Hi @ZeeshanTamboli, could you take a look when you get a chance? This fixes #48882 (stale aria-hidden when a Modal's container changes). All checks are passing, including a regression test that swaps the container after mount. A maintainer label on the PR would also let the remaining label check go green.

@atharv-sys32

atharv-sys32 commented Aug 3, 2026

Copy link
Copy Markdown
Author

Hi @ZeeshanTamboli @silviuaavram, quick nudge on this one. The fix is ready, all CI checks pass, and I added a regression test for the stale aria-hidden case. The test-label-applied check needs a maintainer to apply the label before this can merge. Would appreciate a review when you have a moment. Thanks!

@silviuaavram

Copy link
Copy Markdown
Member

@ZeeshanTamboli this seems to be in the same area as your current transition fix, that also checks whether the portal container changes. Can you sync the work and make sure there's no duplicated effort?

@ZeeshanTamboli

ZeeshanTamboli commented Aug 5, 2026

Copy link
Copy Markdown
Member

@ZeeshanTamboli this seems to be in the same area as your current transition fix, that also checks whether the portal container changes. Can you sync the work and make sure there's no duplicated effort?

@silviuaavram This is a bit different than the fix in the transition PR (#48881). I will review this PR later after the transition one is accepted and merged, incase if there is any duplication.

@github-actions github-actions Bot added PR: out-of-date The pull request has merge conflicts and can't be merged. and removed PR: out-of-date The pull request has merge conflicts and can't be merged. labels Aug 5, 2026
@atharv-sys32
atharv-sys32 force-pushed the fix/aria-hidden-dialog-container branch from 1ef9f63 to b7e5c8b Compare August 5, 2026 16:52
@atharv-sys32

Copy link
Copy Markdown
Author

Hi @ZeeshanTamboli, I rebased this branch on the latest master and resolved the conflicts that came up after your transition fix (#48881) landed. Both tests are kept: yours for the exit transition and mine for the stale aria-hidden case, they cover different scenarios. useModal.ts rebased cleanly with no duplicated logic. The PR is mergeable again and ready for review when you have a moment.

@ZeeshanTamboli

Copy link
Copy Markdown
Member

See #48882 (comment). A proper reproduction is needed first.

@ZeeshanTamboli ZeeshanTamboli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I added this PR build in package.json:

"@mui/material": "https://pkg.pr.new/mui/material-ui/@mui/material@eb8e990",

to test it out and it does not work as expected: https://stackblitz.com/edit/xnjsefyz-bwdrxq7f.

It should work both when disablePortal is used and also when container changes dynamically by using a controlled component. There should be a root fix for these.

ariaHiddenSiblings skipped hiding a container child if it contained the
modal, so a non-portaled modal (disablePortal) whose DOM node lives
inside one of the container's children is no longer rendered inside an
aria-hidden ancestor. Previously the container's children were hidden
unconditionally, making such modals inaccessible to assistive tech.
@atharv-sys32

Copy link
Copy Markdown
Author

I added this PR build in package.json:

"@mui/material": "https://pkg.pr.new/mui/material-ui/@mui/material@eb8e990",

to test it out and it does not work as expected: https://stackblitz.com/edit/xnjsefyz-bwdrxq7f.

It should work both when disablePortal is used and also when container changes dynamically by using a controlled component. There should be a root fix for these.

The disablePortal case is the same pre-existing bug, just triggered differently: since the modal isn't portaled, its DOM node sits inside one of the siblings the manager hides, so that sibling gets aria-hidden and the dialog becomes inaccessible.

I've extended the fix so we never hide a container child that contains the modal. Both disablePortal and the container-change case should work now, and the normal portaled behavior is unchanged.

One edge case I left out of scope: a portaled modal and a disablePortal modal open at the same time. That's a very rare combo so I'm not including that in this PR.

@ZeeshanTamboli ZeeshanTamboli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Findings

  • [P1] Moving the top modal can leave it inside aria-hidden.
    The new remove/add migration fails when another modal remains in the old container. Moving the top body modal into #root leaves #root hidden because the old body container is not fully restored; adding the modal to #root cannot unhide the container itself.

  • [P1] Container migration changes modal stack order.
    Removing and re-adding a lower modal appends it to ModalManager.modals, promoting it above a newer modal. Focus trapping and Escape handling then move to the visually underlying modal. In the reproduction, pressing Escape inside the migrated lower modal incorrectly invoked its onClose. Migration must preserve the original global stack position.

  • [P1] The ancestor guard also blocks restoration.
    isNotModalAncestor is applied for both hiding and unhiding. If the portal node has actually moved inside the destination before remove, restoration skips that destination and leaves its stale aria-hidden="true". The new test claims to simulate this but only changes modal.mount; it never reparents modalRef.

  • [P2] disablePortal leaves the modal background accessible.
    Skipping the entire React root prevents the modal from being hidden, but also leaves every background sibling inside that root exposed to assistive technology. Two inline modals similarly leave the older modal accessible. The intended fix from #43318 hides siblings along each ancestor level and handles mixed inline/portaled stacks. The author explicitly excluded that stack case in the latest reply, but it is central to the linked issue.

  • [P2] container is still honored by the manager when disablePortal ignores it.
    Portal renders inline when disablePortal is enabled, but handleOpen registers and scroll-locks against the supplied container. An explicit or changing container can therefore hide and lock an unrelated subtree. Accessibility topology should come from the actual inline DOM location; scroll-lock container selection should remain a separate concern.

@ZeeshanTamboli ZeeshanTamboli added the type: bug It doesn't behave as expected. label Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: modal Changes related to the modal. type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants