7
7
*/
8
8
9
9
import {
10
+ afterNextRender ,
10
11
ANIMATION_MODULE_TYPE ,
11
12
ChangeDetectionStrategy ,
12
13
ChangeDetectorRef ,
13
14
Component ,
14
15
ComponentRef ,
15
- DoCheck ,
16
16
ElementRef ,
17
17
EmbeddedViewRef ,
18
18
inject ,
19
+ Injector ,
19
20
NgZone ,
20
21
OnDestroy ,
21
22
ViewChild ,
@@ -29,11 +30,14 @@ import {
29
30
DomPortal ,
30
31
TemplatePortal ,
31
32
} from '@angular/cdk/portal' ;
32
- import { Observable , Subject } from 'rxjs' ;
33
+ import { Observable , Subject , of } from 'rxjs' ;
33
34
import { _IdGenerator , AriaLivePoliteness } from '@angular/cdk/a11y' ;
34
35
import { Platform } from '@angular/cdk/platform' ;
35
36
import { MatSnackBarConfig } from './snack-bar-config' ;
36
37
38
+ const ENTER_ANIMATION = '_mat-snack-bar-enter' ;
39
+ const EXIT_ANIMATION = '_mat-snack-bar-exit' ;
40
+
37
41
/**
38
42
* Internal component that wraps user-provided snack bar content.
39
43
* @docs -private
@@ -54,15 +58,16 @@ import {MatSnackBarConfig} from './snack-bar-config';
54
58
'[class.mat-snack-bar-container-enter]' : '_animationState === "visible"' ,
55
59
'[class.mat-snack-bar-container-exit]' : '_animationState === "hidden"' ,
56
60
'[class.mat-snack-bar-container-animations-enabled]' : '!_animationsDisabled' ,
57
- '(animationend)' : 'onAnimationEnd($event)' ,
58
- '(animationcancel)' : 'onAnimationEnd($event)' ,
61
+ '(animationend)' : 'onAnimationEnd($event.animationName )' ,
62
+ '(animationcancel)' : 'onAnimationEnd($event.animationName )' ,
59
63
} ,
60
64
} )
61
- export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck , OnDestroy {
65
+ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy {
62
66
private _ngZone = inject ( NgZone ) ;
63
67
private _elementRef = inject < ElementRef < HTMLElement > > ( ElementRef ) ;
64
68
private _changeDetectorRef = inject ( ChangeDetectorRef ) ;
65
69
private _platform = inject ( Platform ) ;
70
+ private _injector = inject ( Injector ) ;
66
71
protected _animationsDisabled =
67
72
inject ( ANIMATION_MODULE_TYPE , { optional : true } ) === 'NoopAnimations' ;
68
73
snackBarConfig = inject ( MatSnackBarConfig ) ;
@@ -71,7 +76,6 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
71
76
private _trackedModals = new Set < Element > ( ) ;
72
77
private _enterFallback : ReturnType < typeof setTimeout > | undefined ;
73
78
private _exitFallback : ReturnType < typeof setTimeout > | undefined ;
74
- private _pendingNoopAnimation : boolean ;
75
79
76
80
/** The number of milliseconds to wait before announcing the snack bar's content. */
77
81
private readonly _announceDelay : number = 150 ;
@@ -82,6 +86,9 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
82
86
/** Whether the component has been destroyed. */
83
87
private _destroyed = false ;
84
88
89
+ /** Whether the process of exiting is currently running. */
90
+ private _isExiting = false ;
91
+
85
92
/** The portal outlet inside of this container into which the snack bar content will be loaded. */
86
93
@ViewChild ( CdkPortalOutlet , { static : true } ) _portalOutlet : CdkPortalOutlet ;
87
94
@@ -173,11 +180,15 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
173
180
} ;
174
181
175
182
/** Handle end of animations, updating the state of the snackbar. */
176
- onAnimationEnd ( event : AnimationEvent ) {
177
- if ( event . animationName === '_mat-snack-bar-exit' ) {
183
+ onAnimationEnd ( animationName : string ) {
184
+ if ( animationName === EXIT_ANIMATION ) {
178
185
this . _completeExit ( ) ;
179
- } else if ( event . animationName === '_mat-snack-bar-enter' ) {
180
- this . _completeEnter ( ) ;
186
+ } else if ( animationName === ENTER_ANIMATION ) {
187
+ clearTimeout ( this . _enterFallback ) ;
188
+ this . _ngZone . run ( ( ) => {
189
+ this . _onEnter . next ( ) ;
190
+ this . _onEnter . complete ( ) ;
191
+ } ) ;
181
192
}
182
193
}
183
194
@@ -192,16 +203,43 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
192
203
this . _screenReaderAnnounce ( ) ;
193
204
194
205
if ( this . _animationsDisabled ) {
195
- this . _pendingNoopAnimation = true ;
206
+ afterNextRender (
207
+ ( ) => {
208
+ this . _ngZone . run ( ( ) => {
209
+ queueMicrotask ( ( ) => this . onAnimationEnd ( ENTER_ANIMATION ) ) ;
210
+ } ) ;
211
+ } ,
212
+ {
213
+ injector : this . _injector ,
214
+ } ,
215
+ ) ;
196
216
} else {
197
217
clearTimeout ( this . _enterFallback ) ;
198
- this . _enterFallback = setTimeout ( ( ) => this . _completeEnter ( ) , 200 ) ;
218
+ this . _enterFallback = setTimeout ( ( ) => {
219
+ // The snack bar will stay invisible if it fails to animate. Add a fallback class so it
220
+ // becomes visible. This can happen in some apps that do `* {animation: none !important}`.
221
+ this . _elementRef . nativeElement . classList . add ( 'mat-snack-bar-fallback-visible' ) ;
222
+ this . onAnimationEnd ( ENTER_ANIMATION ) ;
223
+ } , 200 ) ;
199
224
}
200
225
}
201
226
}
202
227
203
228
/** Begin animation of the snack bar exiting from view. */
204
229
exit ( ) : Observable < void > {
230
+ if ( this . _destroyed ) {
231
+ return of ( undefined ) ;
232
+ }
233
+
234
+ // It's important not to re-enter here, because `afterNextRender` needs a non-destroyed injector
235
+ // which might happen between the time `exit` starts and the component is actually destroyed.
236
+ // This appears to happen in some internal tests when TestBed is being torn down.
237
+ if ( this . _isExiting ) {
238
+ return this . _onExit ;
239
+ }
240
+
241
+ this . _isExiting = true ;
242
+
205
243
// It's common for snack bars to be opened by random outside calls like HTTP requests or
206
244
// errors. Run inside the NgZone to ensure that it functions correctly.
207
245
this . _ngZone . run ( ( ) => {
@@ -221,55 +259,38 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
221
259
clearTimeout ( this . _announceTimeoutId ) ;
222
260
223
261
if ( this . _animationsDisabled ) {
224
- this . _pendingNoopAnimation = true ;
262
+ afterNextRender (
263
+ ( ) => {
264
+ this . _ngZone . run ( ( ) => {
265
+ queueMicrotask ( ( ) => this . onAnimationEnd ( EXIT_ANIMATION ) ) ;
266
+ } ) ;
267
+ } ,
268
+ {
269
+ injector : this . _injector ,
270
+ } ,
271
+ ) ;
225
272
} else {
226
273
clearTimeout ( this . _exitFallback ) ;
227
- this . _exitFallback = setTimeout ( ( ) => this . _completeExit ( ) , 200 ) ;
274
+ this . _exitFallback = setTimeout ( ( ) => this . onAnimationEnd ( EXIT_ANIMATION ) , 200 ) ;
228
275
}
229
276
} ) ;
230
277
231
278
return this . _onExit ;
232
279
}
233
280
234
- ngDoCheck ( ) : void {
235
- // Aims to mimic the timing of when the snack back was using the animations
236
- // module since many internal tests depend on the old timing.
237
- if ( this . _pendingNoopAnimation ) {
238
- this . _pendingNoopAnimation = false ;
239
- queueMicrotask ( ( ) => {
240
- if ( this . _animationState === 'visible' ) {
241
- this . _completeEnter ( ) ;
242
- } else {
243
- this . _completeExit ( ) ;
244
- }
245
- } ) ;
246
- }
247
- }
248
-
249
281
/** Makes sure the exit callbacks have been invoked when the element is destroyed. */
250
282
ngOnDestroy ( ) {
251
283
this . _destroyed = true ;
252
284
this . _clearFromModals ( ) ;
253
285
this . _completeExit ( ) ;
254
286
}
255
287
256
- private _completeEnter ( ) {
257
- clearTimeout ( this . _enterFallback ) ;
258
- this . _ngZone . run ( ( ) => {
259
- this . _onEnter . next ( ) ;
260
- this . _onEnter . complete ( ) ;
261
- } ) ;
262
- }
263
-
264
- /**
265
- * Removes the element in a microtask. Helps prevent errors where we end up
266
- * removing an element which is in the middle of an animation.
267
- */
268
288
private _completeExit ( ) {
269
289
clearTimeout ( this . _exitFallback ) ;
270
290
queueMicrotask ( ( ) => {
271
291
this . _onExit . next ( ) ;
272
292
this . _onExit . complete ( ) ;
293
+ this . _isExiting = true ;
273
294
} ) ;
274
295
}
275
296
@@ -360,33 +381,40 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
360
381
* announce it.
361
382
*/
362
383
private _screenReaderAnnounce ( ) {
363
- if ( ! this . _announceTimeoutId ) {
364
- this . _ngZone . runOutsideAngular ( ( ) => {
365
- this . _announceTimeoutId = setTimeout ( ( ) => {
366
- const inertElement = this . _elementRef . nativeElement . querySelector ( '[aria-hidden]' ) ;
367
- const liveElement = this . _elementRef . nativeElement . querySelector ( '[aria-live]' ) ;
368
-
369
- if ( inertElement && liveElement ) {
370
- // If an element in the snack bar content is focused before being moved
371
- // track it and restore focus after moving to the live region.
372
- let focusedElement : HTMLElement | null = null ;
373
- if (
374
- this . _platform . isBrowser &&
375
- document . activeElement instanceof HTMLElement &&
376
- inertElement . contains ( document . activeElement )
377
- ) {
378
- focusedElement = document . activeElement ;
379
- }
380
-
381
- inertElement . removeAttribute ( 'aria-hidden' ) ;
382
- liveElement . appendChild ( inertElement ) ;
383
- focusedElement ?. focus ( ) ;
384
-
385
- this . _onAnnounce . next ( ) ;
386
- this . _onAnnounce . complete ( ) ;
387
- }
388
- } , this . _announceDelay ) ;
389
- } ) ;
384
+ if ( this . _announceTimeoutId ) {
385
+ return ;
390
386
}
387
+
388
+ this . _ngZone . runOutsideAngular ( ( ) => {
389
+ this . _announceTimeoutId = setTimeout ( ( ) => {
390
+ if ( this . _destroyed ) {
391
+ return ;
392
+ }
393
+
394
+ const element = this . _elementRef . nativeElement ;
395
+ const inertElement = element . querySelector ( '[aria-hidden]' ) ;
396
+ const liveElement = element . querySelector ( '[aria-live]' ) ;
397
+
398
+ if ( inertElement && liveElement ) {
399
+ // If an element in the snack bar content is focused before being moved
400
+ // track it and restore focus after moving to the live region.
401
+ let focusedElement : HTMLElement | null = null ;
402
+ if (
403
+ this . _platform . isBrowser &&
404
+ document . activeElement instanceof HTMLElement &&
405
+ inertElement . contains ( document . activeElement )
406
+ ) {
407
+ focusedElement = document . activeElement ;
408
+ }
409
+
410
+ inertElement . removeAttribute ( 'aria-hidden' ) ;
411
+ liveElement . appendChild ( inertElement ) ;
412
+ focusedElement ?. focus ( ) ;
413
+
414
+ this . _onAnnounce . next ( ) ;
415
+ this . _onAnnounce . complete ( ) ;
416
+ }
417
+ } , this . _announceDelay ) ;
418
+ } ) ;
391
419
}
392
420
}
0 commit comments