From c9b113d9904fe321d6c3d672d6838f6bfec8ebb6 Mon Sep 17 00:00:00 2001 From: ewerton336 Date: Sun, 1 Nov 2026 10:52:47 -0300 Subject: [PATCH] fix(cdk/dialog): aria-hidden not restored when dialogs from parent and child are closed The list of open dialogs is shared between a `Dialog` and its parent, but each instance was keeping track of the elements it hid from assistive technology separately. If a dialog opened by a child `Dialog` was closed while one opened by the parent was still open (or the other way around), the instance that closed the last dialog didn't know about the hidden elements and the `aria-hidden` attribute was never removed. These changes share the map of hidden elements with the parent, the same way as the list of open dialogs. Fixes #30028 --- src/cdk/dialog/dialog.spec.ts | 37 +++++++++++++++++++++++++++++++++++ src/cdk/dialog/dialog.ts | 20 +++++++++++++++---- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/cdk/dialog/dialog.spec.ts b/src/cdk/dialog/dialog.spec.ts index 0e588718e3d2..5e18ea614814 100644 --- a/src/cdk/dialog/dialog.spec.ts +++ b/src/cdk/dialog/dialog.spec.ts @@ -1309,6 +1309,43 @@ describe('Dialog with a parent Dialog', () => { .toContain('Pizza'); }); + it('should restore `aria-hidden` after closing dialogs of a parent and child', async () => { + const wrapper = document.createElement('div'); + const sibling = document.createElement('div'); + wrapper.appendChild(sibling); + wrapper.appendChild(overlayContainerElement); + document.body.appendChild(wrapper); + + const childDialogRef = childDialog.open(PizzaMsg); + fixture.detectChanges(); + await fixture.whenStable(); + + const parentDialogRef = parentDialog.open(PizzaMsg); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(sibling.getAttribute('aria-hidden')) + .withContext('Expected sibling to be hidden') + .toBe('true'); + + childDialogRef.close(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(sibling.getAttribute('aria-hidden')) + .withContext('Expected sibling to remain hidden while a dialog is open') + .toBe('true'); + + parentDialogRef.close(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(sibling.hasAttribute('aria-hidden')) + .withContext('Expected sibling to no longer be hidden') + .toBe(false); + wrapper.remove(); + }); + it('should close the top dialog via the escape key', () => { childDialog.open(PizzaMsg); fixture.detectChanges(); diff --git a/src/cdk/dialog/dialog.ts b/src/cdk/dialog/dialog.ts index c1f93089e21c..eaafcbcbe7fd 100644 --- a/src/cdk/dialog/dialog.ts +++ b/src/cdk/dialog/dialog.ts @@ -63,7 +63,7 @@ export class Dialog implements OnDestroy { private _openDialogsAtThisLevel: DialogRef[] = []; private readonly _afterAllClosedAtThisLevel = new Subject(); private readonly _afterOpenedAtThisLevel = new Subject(); - private _ariaHiddenElements = new Map(); + private readonly _ariaHiddenElementsAtThisLevel = new Map(); private _scrollStrategy = inject(DIALOG_SCROLL_STRATEGY); /** Keeps track of the currently-open dialogs. */ @@ -373,7 +373,9 @@ export class Dialog implements OnDestroy { // If all the dialogs were closed, remove/restore the `aria-hidden` // to a the siblings and emit to the `afterAllClosed` stream. if (!this.openDialogs.length) { - this._ariaHiddenElements.forEach((previousValue, element) => { + const ariaHiddenElements = this._getAriaHiddenElements(); + + ariaHiddenElements.forEach((previousValue, element) => { if (previousValue) { element.setAttribute('aria-hidden', previousValue); } else { @@ -381,7 +383,7 @@ export class Dialog implements OnDestroy { } }); - this._ariaHiddenElements.clear(); + ariaHiddenElements.clear(); if (emitEvent) { this._getAfterAllClosed().next(); @@ -395,6 +397,7 @@ export class Dialog implements OnDestroy { // Ensure that the overlay container is attached to the DOM. if (overlayContainer.parentElement) { const siblings = overlayContainer.parentElement.children; + const ariaHiddenElements = this._getAriaHiddenElements(); for (let i = siblings.length - 1; i > -1; i--) { const sibling = siblings[i]; @@ -406,7 +409,7 @@ export class Dialog implements OnDestroy { !sibling.hasAttribute('aria-live') && !sibling.hasAttribute('popover') ) { - this._ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden')); + ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden')); sibling.setAttribute('aria-hidden', 'true'); } } @@ -417,6 +420,15 @@ export class Dialog implements OnDestroy { const parent = this._parentDialog; return parent ? parent._getAfterAllClosed() : this._afterAllClosedAtThisLevel; } + + /** + * Gets the elements that were hidden from assistive technology while dialogs are open. + * Shared with the parent dialog, because the list of open dialogs is shared as well. + */ + private _getAriaHiddenElements(): Map { + const parent = this._parentDialog; + return parent ? parent._getAriaHiddenElements() : this._ariaHiddenElementsAtThisLevel; + } } /**