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 ;
@@ -173,11 +177,15 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
173
177
} ;
174
178
175
179
/** Handle end of animations, updating the state of the snackbar. */
176
- onAnimationEnd ( event : AnimationEvent ) {
177
- if ( event . animationName === '_mat-snack-bar-exit' ) {
180
+ onAnimationEnd ( animationName : string ) {
181
+ if ( animationName === EXIT_ANIMATION ) {
178
182
this . _completeExit ( ) ;
179
- } else if ( event . animationName === '_mat-snack-bar-enter' ) {
180
- this . _completeEnter ( ) ;
183
+ } else if ( animationName === ENTER_ANIMATION ) {
184
+ clearTimeout ( this . _enterFallback ) ;
185
+ this . _ngZone . run ( ( ) => {
186
+ this . _onEnter . next ( ) ;
187
+ this . _onEnter . complete ( ) ;
188
+ } ) ;
181
189
}
182
190
}
183
191
@@ -192,16 +200,34 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
192
200
this . _screenReaderAnnounce ( ) ;
193
201
194
202
if ( this . _animationsDisabled ) {
195
- this . _pendingNoopAnimation = true ;
203
+ afterNextRender (
204
+ ( ) => {
205
+ this . _ngZone . run ( ( ) => {
206
+ queueMicrotask ( ( ) => this . onAnimationEnd ( ENTER_ANIMATION ) ) ;
207
+ } ) ;
208
+ } ,
209
+ {
210
+ injector : this . _injector ,
211
+ } ,
212
+ ) ;
196
213
} else {
197
214
clearTimeout ( this . _enterFallback ) ;
198
- this . _enterFallback = setTimeout ( ( ) => this . _completeEnter ( ) , 200 ) ;
215
+ this . _enterFallback = setTimeout ( ( ) => {
216
+ // The snack bar will stay invisible if it fails to animate. Add a fallback class so it
217
+ // becomes visible. This can happen in some apps that do `* {animation: none !important}`.
218
+ this . _elementRef . nativeElement . classList . add ( 'mat-snack-bar-fallback-visible' ) ;
219
+ this . onAnimationEnd ( ENTER_ANIMATION ) ;
220
+ } , 200 ) ;
199
221
}
200
222
}
201
223
}
202
224
203
225
/** Begin animation of the snack bar exiting from view. */
204
226
exit ( ) : Observable < void > {
227
+ if ( this . _destroyed ) {
228
+ return of ( undefined ) ;
229
+ }
230
+
205
231
// It's common for snack bars to be opened by random outside calls like HTTP requests or
206
232
// errors. Run inside the NgZone to ensure that it functions correctly.
207
233
this . _ngZone . run ( ( ) => {
@@ -221,50 +247,32 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
221
247
clearTimeout ( this . _announceTimeoutId ) ;
222
248
223
249
if ( this . _animationsDisabled ) {
224
- this . _pendingNoopAnimation = true ;
250
+ afterNextRender (
251
+ ( ) => {
252
+ this . _ngZone . run ( ( ) => {
253
+ queueMicrotask ( ( ) => this . onAnimationEnd ( EXIT_ANIMATION ) ) ;
254
+ } ) ;
255
+ } ,
256
+ {
257
+ injector : this . _injector ,
258
+ } ,
259
+ ) ;
225
260
} else {
226
261
clearTimeout ( this . _exitFallback ) ;
227
- this . _exitFallback = setTimeout ( ( ) => this . _completeExit ( ) , 200 ) ;
262
+ this . _exitFallback = setTimeout ( ( ) => this . onAnimationEnd ( EXIT_ANIMATION ) , 200 ) ;
228
263
}
229
264
} ) ;
230
265
231
266
return this . _onExit ;
232
267
}
233
268
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
269
/** Makes sure the exit callbacks have been invoked when the element is destroyed. */
250
270
ngOnDestroy ( ) {
251
271
this . _destroyed = true ;
252
272
this . _clearFromModals ( ) ;
253
273
this . _completeExit ( ) ;
254
274
}
255
275
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
276
private _completeExit ( ) {
269
277
clearTimeout ( this . _exitFallback ) ;
270
278
queueMicrotask ( ( ) => {
@@ -360,33 +368,40 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
360
368
* announce it.
361
369
*/
362
370
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
- } ) ;
371
+ if ( this . _announceTimeoutId ) {
372
+ return ;
390
373
}
374
+
375
+ this . _ngZone . runOutsideAngular ( ( ) => {
376
+ this . _announceTimeoutId = setTimeout ( ( ) => {
377
+ if ( this . _destroyed ) {
378
+ return ;
379
+ }
380
+
381
+ const element = this . _elementRef . nativeElement ;
382
+ const inertElement = element . querySelector ( '[aria-hidden]' ) ;
383
+ const liveElement = element . querySelector ( '[aria-live]' ) ;
384
+
385
+ if ( inertElement && liveElement ) {
386
+ // If an element in the snack bar content is focused before being moved
387
+ // track it and restore focus after moving to the live region.
388
+ let focusedElement : HTMLElement | null = null ;
389
+ if (
390
+ this . _platform . isBrowser &&
391
+ document . activeElement instanceof HTMLElement &&
392
+ inertElement . contains ( document . activeElement )
393
+ ) {
394
+ focusedElement = document . activeElement ;
395
+ }
396
+
397
+ inertElement . removeAttribute ( 'aria-hidden' ) ;
398
+ liveElement . appendChild ( inertElement ) ;
399
+ focusedElement ?. focus ( ) ;
400
+
401
+ this . _onAnnounce . next ( ) ;
402
+ this . _onAnnounce . complete ( ) ;
403
+ }
404
+ } , this . _announceDelay ) ;
405
+ } ) ;
391
406
}
392
407
}
0 commit comments