diff --git a/src/material/legacy-snack-bar/snack-bar-container.html b/src/material/legacy-snack-bar/snack-bar-container.html index 334df5db9bdb..50527ba22269 100644 --- a/src/material/legacy-snack-bar/snack-bar-container.html +++ b/src/material/legacy-snack-bar/snack-bar-container.html @@ -4,4 +4,4 @@ -
+
diff --git a/src/material/snack-bar/snack-bar-container.html b/src/material/snack-bar/snack-bar-container.html index 1370cc843b28..5f47966bc1ff 100644 --- a/src/material/snack-bar/snack-bar-container.html +++ b/src/material/snack-bar/snack-bar-container.html @@ -10,6 +10,6 @@ -
+
diff --git a/src/material/snack-bar/snack-bar-container.ts b/src/material/snack-bar/snack-bar-container.ts index b13012b2149b..1a792f7e79d0 100644 --- a/src/material/snack-bar/snack-bar-container.ts +++ b/src/material/snack-bar/snack-bar-container.ts @@ -14,11 +14,13 @@ import { Directive, ElementRef, EmbeddedViewRef, + inject, NgZone, OnDestroy, ViewChild, ViewEncapsulation, } from '@angular/core'; +import {DOCUMENT} from '@angular/common'; import {matSnackBarAnimations} from './snack-bar-animations'; import { BasePortalOutlet, @@ -34,12 +36,17 @@ import {AnimationEvent} from '@angular/animations'; import {take} from 'rxjs/operators'; import {MatSnackBarConfig} from './snack-bar-config'; +let uniqueId = 0; + /** * Base class for snack bar containers. * @docs-private */ @Directive() export abstract class _MatSnackBarContainerBase extends BasePortalOutlet implements OnDestroy { + private _document = inject(DOCUMENT); + private _trackedModals = new Set(); + /** The number of milliseconds to wait before announcing the snack bar's content. */ private readonly _announceDelay: number = 150; @@ -73,6 +80,9 @@ export abstract class _MatSnackBarContainerBase extends BasePortalOutlet impleme */ _role?: 'status' | 'alert'; + /** Unique ID of the aria-live element. */ + readonly _liveElementId = `mat-snack-bar-container-live-${uniqueId++}`; + constructor( private _ngZone: NgZone, protected _elementRef: ElementRef, @@ -188,6 +198,7 @@ export abstract class _MatSnackBarContainerBase extends BasePortalOutlet impleme /** Makes sure the exit callbacks have been invoked when the element is destroyed. */ ngOnDestroy() { this._destroyed = true; + this._clearFromModals(); this._completeExit(); } @@ -220,6 +231,54 @@ export abstract class _MatSnackBarContainerBase extends BasePortalOutlet impleme element.classList.add(panelClasses); } } + + this._exposeToModals(); + } + + /** + * Some browsers won't expose the accessibility node of the live element if there is an + * `aria-modal` and the live element is outside of it. This method works around the issue by + * pointing the `aria-owns` of all modals to the live element. + */ + private _exposeToModals() { + // TODO(crisbeto): consider de-duplicating this with the `LiveAnnouncer`. + // Note that the selector here is limited to CDK overlays at the moment in order to reduce the + // section of the DOM we need to look through. This should cover all the cases we support, but + // the selector can be expanded if it turns out to be too narrow. + const id = this._liveElementId; + const modals = this._document.querySelectorAll( + 'body > .cdk-overlay-container [aria-modal="true"]', + ); + + for (let i = 0; i < modals.length; i++) { + const modal = modals[i]; + const ariaOwns = modal.getAttribute('aria-owns'); + this._trackedModals.add(modal); + + if (!ariaOwns) { + modal.setAttribute('aria-owns', id); + } else if (ariaOwns.indexOf(id) === -1) { + modal.setAttribute('aria-owns', ariaOwns + ' ' + id); + } + } + } + + /** Clears the references to the live element from any modals it was added to. */ + private _clearFromModals() { + this._trackedModals.forEach(modal => { + const ariaOwns = modal.getAttribute('aria-owns'); + + if (ariaOwns) { + const newValue = ariaOwns.replace(this._liveElementId, '').trim(); + + if (newValue.length > 0) { + modal.setAttribute('aria-owns', newValue); + } else { + modal.removeAttribute('aria-owns'); + } + } + }); + this._trackedModals.clear(); } /** Asserts that no content is already attached to the container. */ diff --git a/tools/public_api_guard/material/snack-bar.md b/tools/public_api_guard/material/snack-bar.md index 082ace03460d..1cacb9a99de3 100644 --- a/tools/public_api_guard/material/snack-bar.md +++ b/tools/public_api_guard/material/snack-bar.md @@ -142,6 +142,7 @@ export abstract class _MatSnackBarContainerBase extends BasePortalOutlet impleme enter(): void; exit(): Observable; _live: AriaLivePoliteness; + readonly _liveElementId: string; ngOnDestroy(): void; onAnimationEnd(event: AnimationEvent_2): void; readonly _onAnnounce: Subject;