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; + } } /**