@@ -88,9 +88,9 @@ const TestWrapper: React.FC<{ children: React.ReactNode }> = ({
8888// Fluent's Dropdown renders its listbox in a portal guarded by a focus
8989// modalizer. Under jsdom the popover only toggles via a direct click event,
9090// and the `aria-hidden` the modalizer puts on the dialog while the listbox is
91- // open is never restored once it closes, which would hide the rest of the form
92- // from role-based queries. Both behaviors are jsdom artifacts — real pointer
93- // and keyboard interaction is covered by e2e/config.spec.ts.
91+ // open is not reliably restored once it closes, which would hide the rest of
92+ // the form from role-based queries. Both behaviors are jsdom artifacts — real
93+ // pointer and keyboard interaction is covered by e2e/config.spec.ts.
9494async function openTargetTypePicker ( ) : Promise < HTMLElement > {
9595 const picker = screen . getByRole ( "combobox" , { name : / t a r g e t t y p e / i } ) ;
9696 await waitFor ( ( ) => {
@@ -101,9 +101,13 @@ async function openTargetTypePicker(): Promise<HTMLElement> {
101101 return picker ;
102102}
103103
104+ // Drops the leftover `aria-hidden` from the dialog and everything above it.
105+ // It is a no-op while a listbox is open, so the modalizer keeps its real
106+ // behavior and only the missing restore is compensated for.
104107function restoreDialogAccessibility ( ) : void {
105108 const dialog = document . querySelector ( '[role="dialog"]' ) ;
106109 if ( ! dialog ) return ;
110+ if ( document . querySelector ( '[role="listbox"]' ) ) return ;
107111 const hiddenAncestors = document . querySelectorAll ( '[aria-hidden="true"]' ) ;
108112 for ( const element of Array . from ( hiddenAncestors ) ) {
109113 if ( element === dialog || element . contains ( dialog ) ) {
@@ -112,6 +116,21 @@ function restoreDialogAccessibility(): void {
112116 }
113117}
114118
119+ // The modalizer can re-apply `aria-hidden` well after the listbox closed —
120+ // on an unrelated focus change, for instance — so a one-shot cleanup leaves
121+ // every later `*ByRole` query racing against it. Re-run the cleanup whenever
122+ // the attribute reappears instead.
123+ function watchDialogAccessibility ( ) : MutationObserver {
124+ const observer = new MutationObserver ( restoreDialogAccessibility ) ;
125+ observer . observe ( document . documentElement , {
126+ subtree : true ,
127+ childList : true ,
128+ attributes : true ,
129+ attributeFilter : [ "aria-hidden" ] ,
130+ } ) ;
131+ return observer ;
132+ }
133+
115134async function selectTargetType ( value : string ) : Promise < void > {
116135 await openTargetTypePicker ( ) ;
117136 fireEvent . click (
@@ -198,7 +217,10 @@ describe("CreateTargetDialog", () => {
198217 onCreated : jest . fn ( ) ,
199218 } ;
200219
220+ let dialogAccessibilityObserver : MutationObserver ;
221+
201222 beforeEach ( ( ) => {
223+ dialogAccessibilityObserver = watchDialogAccessibility ( ) ;
202224 jest . clearAllMocks ( ) ;
203225 mockedTargetsApi . listTargetCatalog . mockResolvedValue ( TARGET_CATALOG ) ;
204226 mockedTargetsApi . listTargets . mockResolvedValue ( {
@@ -207,6 +229,10 @@ describe("CreateTargetDialog", () => {
207229 } as unknown as Awaited < ReturnType < typeof mockedTargetsApi . listTargets > > ) ;
208230 } ) ;
209231
232+ afterEach ( ( ) => {
233+ dialogAccessibilityObserver . disconnect ( ) ;
234+ } ) ;
235+
210236 it ( "should render dialog when open" , ( ) => {
211237 render (
212238 < TestWrapper >
0 commit comments