@@ -14,11 +14,13 @@ import {
1414 Directive ,
1515 ElementRef ,
1616 EmbeddedViewRef ,
17+ inject ,
1718 NgZone ,
1819 OnDestroy ,
1920 ViewChild ,
2021 ViewEncapsulation ,
2122} from '@angular/core' ;
23+ import { DOCUMENT } from '@angular/common' ;
2224import { matSnackBarAnimations } from './snack-bar-animations' ;
2325import {
2426 BasePortalOutlet ,
@@ -34,12 +36,17 @@ import {AnimationEvent} from '@angular/animations';
3436import { take } from 'rxjs/operators' ;
3537import { MatSnackBarConfig } from './snack-bar-config' ;
3638
39+ let uniqueId = 0 ;
40+
3741/**
3842 * Base class for snack bar containers.
3943 * @docs -private
4044 */
4145@Directive ( )
4246export abstract class _MatSnackBarContainerBase extends BasePortalOutlet implements OnDestroy {
47+ private _document = inject ( DOCUMENT ) ;
48+ private _trackedModals = new Set < Element > ( ) ;
49+
4350 /** The number of milliseconds to wait before announcing the snack bar's content. */
4451 private readonly _announceDelay : number = 150 ;
4552
@@ -73,6 +80,9 @@ export abstract class _MatSnackBarContainerBase extends BasePortalOutlet impleme
7380 */
7481 _role ?: 'status' | 'alert' ;
7582
83+ /** Unique ID of the aria-live element. */
84+ readonly _liveElementId = `mat-snack-bar-container-live-${ uniqueId ++ } ` ;
85+
7686 constructor (
7787 private _ngZone : NgZone ,
7888 protected _elementRef : ElementRef < HTMLElement > ,
@@ -188,6 +198,7 @@ export abstract class _MatSnackBarContainerBase extends BasePortalOutlet impleme
188198 /** Makes sure the exit callbacks have been invoked when the element is destroyed. */
189199 ngOnDestroy ( ) {
190200 this . _destroyed = true ;
201+ this . _clearFromModals ( ) ;
191202 this . _completeExit ( ) ;
192203 }
193204
@@ -220,6 +231,54 @@ export abstract class _MatSnackBarContainerBase extends BasePortalOutlet impleme
220231 element . classList . add ( panelClasses ) ;
221232 }
222233 }
234+
235+ this . _exposeToModals ( ) ;
236+ }
237+
238+ /**
239+ * Some browsers won't expose the accessibility node of the live element if there is an
240+ * `aria-modal` and the live element is outside of it. This method works around the issue by
241+ * pointing the `aria-owns` of all modals to the live element.
242+ */
243+ private _exposeToModals ( ) {
244+ // TODO(crisbeto): consider de-duplicating this with the `LiveAnnouncer`.
245+ // Note that the selector here is limited to CDK overlays at the moment in order to reduce the
246+ // section of the DOM we need to look through. This should cover all the cases we support, but
247+ // the selector can be expanded if it turns out to be too narrow.
248+ const id = this . _liveElementId ;
249+ const modals = this . _document . querySelectorAll (
250+ 'body > .cdk-overlay-container [aria-modal="true"]' ,
251+ ) ;
252+
253+ for ( let i = 0 ; i < modals . length ; i ++ ) {
254+ const modal = modals [ i ] ;
255+ const ariaOwns = modal . getAttribute ( 'aria-owns' ) ;
256+ this . _trackedModals . add ( modal ) ;
257+
258+ if ( ! ariaOwns ) {
259+ modal . setAttribute ( 'aria-owns' , id ) ;
260+ } else if ( ariaOwns . indexOf ( id ) === - 1 ) {
261+ modal . setAttribute ( 'aria-owns' , ariaOwns + ' ' + id ) ;
262+ }
263+ }
264+ }
265+
266+ /** Clears the references to the live element from any modals it was added to. */
267+ private _clearFromModals ( ) {
268+ this . _trackedModals . forEach ( modal => {
269+ const ariaOwns = modal . getAttribute ( 'aria-owns' ) ;
270+
271+ if ( ariaOwns ) {
272+ const newValue = ariaOwns . replace ( this . _liveElementId , '' ) . trim ( ) ;
273+
274+ if ( newValue . length > 0 ) {
275+ modal . setAttribute ( 'aria-owns' , newValue ) ;
276+ } else {
277+ modal . removeAttribute ( 'aria-owns' ) ;
278+ }
279+ }
280+ } ) ;
281+ this . _trackedModals . clear ( ) ;
223282 }
224283
225284 /** Asserts that no content is already attached to the container. */
0 commit comments