fix(react-dialog): stale aria-hidden breaks focus trap when closing stacked non-nested sibling dialogs#35990
fix(react-dialog): stale aria-hidden breaks focus trap when closing stacked non-nested sibling dialogs#35990
Conversation
…ed dialogs close (#35985) Agent-Logs-Url: https://github.com/microsoft/fluentui/sessions/fc8d830a-fba4-4001-91af-89f9e553572e Co-authored-by: Hotell <1223799+Hotell@users.noreply.github.com>
|
@copilot tests are failing, try using instead your workaround |
… revert incorrect aria-hidden workaround Agent-Logs-Url: https://github.com/microsoft/fluentui/sessions/c4585627-39c5-4f06-8884-09fab6e1fd81 Co-authored-by: Hotell <1223799+Hotell@users.noreply.github.com>
Done in commit The root issue was that the previous The actual fix was in the test fixtures: the trigger buttons opening the dialogs need |
When two sibling (non-nested)
Dialogcomponents are stacked, closing the top dialog can cause focus to escape to the page-level trigger instead of returning to the button inside the underlying dialog. Tabster's Modalizer does not currently maintain an activation stack, so focus restoration requires the caller to properly mark trigger elements as restore targets.Changes
Dialog.cy.tsx— Addeddescribe('stacked non-nested dialogs (sibling)')with three Cypress regression tests:aria-hidden="true"after Dialog 2 closesWhen using controlled (unmanaged) open state without
<DialogTrigger>, trigger buttons must haveuseRestoreFocusTarget()spread on them so Tabster's Restorer knows where to return focus when the stacked dialog closes. This follows the same documented pattern asDialogTriggerOutsideDialog.Change file — Patch bump for
@fluentui/react-dialog.Example scenario
Original prompt
Context
This PR addresses #35985: when two independent (non-nested, sibling)
Dialogcomponents are stacked — e.g. Dialog 1 is open, and from a button inside it Dialog 2 is opened programmatically — closing Dialog 2 leaves a stalearia-hidden="true"on Dialog 1's portal mount node. This blocks browser focus from entering that subtree, so the Tabster Restorer fires focus back to the original page-level trigger rather than back into Dialog 1.Root cause
Each
Dialogindependently callsuseModalAttributeswhich configures a Tabster Modalizer + Restorer pair. When Dialog 2's Modalizer activates, itaria-hiddens everything outside itself, including Dialog 1's portal. When Dialog 2 unmounts, its Modalizer cleanup removes thearia-hiddenit set. However, Dialog 1 is still open and its own Modalizer'saria-hiddenwalk marked Dialog 1's portal container as hidden from its own perspective. The net result is a stalearia-hidden="true"left on Dialog 1's portal node after Dialog 2 closes. The browser refuses.focus()into anaria-hiddensubtree, soRestorerTypes.Targetfocus restoration silently fails and focus bubbles up to the page-level trigger.The ultimate fix belongs in Tabster's Modalizer (it should maintain an activation stack). Until that lands, a client-side mitigation is needed in
useFocusFirstElement.Required changes
1. Cypress regression test
File:
packages/react-components/react-dialog/library/src/components/Dialog/Dialog.cy.tsxAdd a new
describe('stacked non-nested dialogs (sibling)', ...)block appended after the existing'should allow nested dialogs'test (after line 702, before the closing});of the top-leveldescribe('Dialog', ...)).The block should use the existing imports already present in the file (
React,Button,Dialog,DialogSurface,DialogBody,DialogTitle,DialogContent,DialogActions,DialogTrigger, and the selectorsdialogTriggerOpenId,dialogTriggerOpenSelector,dialogTriggerCloseId,dialogTriggerCloseSelector,dialogSurfaceSelector).Add this describe block: