@@ -14,11 +14,13 @@ import {
14
14
Directive ,
15
15
ElementRef ,
16
16
EmbeddedViewRef ,
17
+ inject ,
17
18
NgZone ,
18
19
OnDestroy ,
19
20
ViewChild ,
20
21
ViewEncapsulation ,
21
22
} from '@angular/core' ;
23
+ import { DOCUMENT } from '@angular/common' ;
22
24
import { matSnackBarAnimations } from './snack-bar-animations' ;
23
25
import {
24
26
BasePortalOutlet ,
@@ -34,12 +36,17 @@ import {AnimationEvent} from '@angular/animations';
34
36
import { take } from 'rxjs/operators' ;
35
37
import { MatSnackBarConfig } from './snack-bar-config' ;
36
38
39
+ let uniqueId = 0 ;
40
+
37
41
/**
38
42
* Base class for snack bar containers.
39
43
* @docs -private
40
44
*/
41
45
@Directive ( )
42
46
export abstract class _MatSnackBarContainerBase extends BasePortalOutlet implements OnDestroy {
47
+ private _document = inject ( DOCUMENT ) ;
48
+ private _trackedModals = new Set < Element > ( ) ;
49
+
43
50
/** The number of milliseconds to wait before announcing the snack bar's content. */
44
51
private readonly _announceDelay : number = 150 ;
45
52
@@ -73,6 +80,9 @@ export abstract class _MatSnackBarContainerBase extends BasePortalOutlet impleme
73
80
*/
74
81
_role ?: 'status' | 'alert' ;
75
82
83
+ /** Unique ID of the aria-live element. */
84
+ readonly _liveElementId = `mat-snack-bar-container-live-${ uniqueId ++ } ` ;
85
+
76
86
constructor (
77
87
private _ngZone : NgZone ,
78
88
protected _elementRef : ElementRef < HTMLElement > ,
@@ -188,6 +198,7 @@ export abstract class _MatSnackBarContainerBase extends BasePortalOutlet impleme
188
198
/** Makes sure the exit callbacks have been invoked when the element is destroyed. */
189
199
ngOnDestroy ( ) {
190
200
this . _destroyed = true ;
201
+ this . _clearFromModals ( ) ;
191
202
this . _completeExit ( ) ;
192
203
}
193
204
@@ -220,6 +231,54 @@ export abstract class _MatSnackBarContainerBase extends BasePortalOutlet impleme
220
231
element . classList . add ( panelClasses ) ;
221
232
}
222
233
}
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 ( ) ;
223
282
}
224
283
225
284
/** Asserts that no content is already attached to the container. */
0 commit comments