77 */
88
99import {
10+ afterNextRender ,
1011 ANIMATION_MODULE_TYPE ,
1112 ChangeDetectionStrategy ,
1213 ChangeDetectorRef ,
1314 Component ,
1415 ComponentRef ,
15- DoCheck ,
1616 ElementRef ,
1717 EmbeddedViewRef ,
1818 inject ,
19+ Injector ,
1920 NgZone ,
2021 OnDestroy ,
2122 ViewChild ,
@@ -29,11 +30,14 @@ import {
2930 DomPortal ,
3031 TemplatePortal ,
3132} from '@angular/cdk/portal' ;
32- import { Observable , Subject } from 'rxjs' ;
33+ import { Observable , Subject , of } from 'rxjs' ;
3334import { _IdGenerator , AriaLivePoliteness } from '@angular/cdk/a11y' ;
3435import { Platform } from '@angular/cdk/platform' ;
3536import { MatSnackBarConfig } from './snack-bar-config' ;
3637
38+ const ENTER_ANIMATION = '_mat-snack-bar-enter' ;
39+ const EXIT_ANIMATION = '_mat-snack-bar-exit' ;
40+
3741/**
3842 * Internal component that wraps user-provided snack bar content.
3943 * @docs -private
@@ -54,15 +58,16 @@ import {MatSnackBarConfig} from './snack-bar-config';
5458 '[class.mat-snack-bar-container-enter]' : '_animationState === "visible"' ,
5559 '[class.mat-snack-bar-container-exit]' : '_animationState === "hidden"' ,
5660 '[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 )' ,
5963 } ,
6064} )
61- export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck , OnDestroy {
65+ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy {
6266 private _ngZone = inject ( NgZone ) ;
6367 private _elementRef = inject < ElementRef < HTMLElement > > ( ElementRef ) ;
6468 private _changeDetectorRef = inject ( ChangeDetectorRef ) ;
6569 private _platform = inject ( Platform ) ;
70+ private _injector = inject ( Injector ) ;
6671 protected _animationsDisabled =
6772 inject ( ANIMATION_MODULE_TYPE , { optional : true } ) === 'NoopAnimations' ;
6873 snackBarConfig = inject ( MatSnackBarConfig ) ;
@@ -71,7 +76,6 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
7176 private _trackedModals = new Set < Element > ( ) ;
7277 private _enterFallback : ReturnType < typeof setTimeout > | undefined ;
7378 private _exitFallback : ReturnType < typeof setTimeout > | undefined ;
74- private _pendingNoopAnimation : boolean ;
7579
7680 /** The number of milliseconds to wait before announcing the snack bar's content. */
7781 private readonly _announceDelay : number = 150 ;
@@ -173,11 +177,15 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
173177 } ;
174178
175179 /** 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 ) {
178182 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+ } ) ;
181189 }
182190 }
183191
@@ -192,16 +200,22 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
192200 this . _screenReaderAnnounce ( ) ;
193201
194202 if ( this . _animationsDisabled ) {
195- this . _pendingNoopAnimation = true ;
203+ afterNextRender ( ( ) => this . _ngZone . run ( ( ) => this . onAnimationEnd ( ENTER_ANIMATION ) ) , {
204+ injector : this . _injector ,
205+ } ) ;
196206 } else {
197207 clearTimeout ( this . _enterFallback ) ;
198- this . _enterFallback = setTimeout ( ( ) => this . _completeEnter ( ) , 200 ) ;
208+ this . _enterFallback = setTimeout ( ( ) => this . onAnimationEnd ( ENTER_ANIMATION ) , 200 ) ;
199209 }
200210 }
201211 }
202212
203213 /** Begin animation of the snack bar exiting from view. */
204214 exit ( ) : Observable < void > {
215+ if ( this . _destroyed ) {
216+ return of ( undefined ) ;
217+ }
218+
205219 // It's common for snack bars to be opened by random outside calls like HTTP requests or
206220 // errors. Run inside the NgZone to ensure that it functions correctly.
207221 this . _ngZone . run ( ( ) => {
@@ -221,50 +235,25 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
221235 clearTimeout ( this . _announceTimeoutId ) ;
222236
223237 if ( this . _animationsDisabled ) {
224- this . _pendingNoopAnimation = true ;
238+ afterNextRender ( ( ) => this . _ngZone . run ( ( ) => this . onAnimationEnd ( EXIT_ANIMATION ) ) , {
239+ injector : this . _injector ,
240+ } ) ;
225241 } else {
226242 clearTimeout ( this . _exitFallback ) ;
227- this . _exitFallback = setTimeout ( ( ) => this . _completeExit ( ) , 200 ) ;
243+ this . _exitFallback = setTimeout ( ( ) => this . onAnimationEnd ( EXIT_ANIMATION ) , 200 ) ;
228244 }
229245 } ) ;
230246
231247 return this . _onExit ;
232248 }
233249
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-
249250 /** Makes sure the exit callbacks have been invoked when the element is destroyed. */
250251 ngOnDestroy ( ) {
251252 this . _destroyed = true ;
252253 this . _clearFromModals ( ) ;
253254 this . _completeExit ( ) ;
254255 }
255256
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- */
268257 private _completeExit ( ) {
269258 clearTimeout ( this . _exitFallback ) ;
270259 queueMicrotask ( ( ) => {
@@ -360,33 +349,40 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
360349 * announce it.
361350 */
362351 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- } ) ;
352+ if ( this . _announceTimeoutId ) {
353+ return ;
390354 }
355+
356+ this . _ngZone . runOutsideAngular ( ( ) => {
357+ this . _announceTimeoutId = setTimeout ( ( ) => {
358+ if ( this . _destroyed ) {
359+ return ;
360+ }
361+
362+ const element = this . _elementRef . nativeElement ;
363+ const inertElement = element . querySelector ( '[aria-hidden]' ) ;
364+ const liveElement = element . querySelector ( '[aria-live]' ) ;
365+
366+ if ( inertElement && liveElement ) {
367+ // If an element in the snack bar content is focused before being moved
368+ // track it and restore focus after moving to the live region.
369+ let focusedElement : HTMLElement | null = null ;
370+ if (
371+ this . _platform . isBrowser &&
372+ document . activeElement instanceof HTMLElement &&
373+ inertElement . contains ( document . activeElement )
374+ ) {
375+ focusedElement = document . activeElement ;
376+ }
377+
378+ inertElement . removeAttribute ( 'aria-hidden' ) ;
379+ liveElement . appendChild ( inertElement ) ;
380+ focusedElement ?. focus ( ) ;
381+
382+ this . _onAnnounce . next ( ) ;
383+ this . _onAnnounce . complete ( ) ;
384+ }
385+ } , this . _announceDelay ) ;
386+ } ) ;
391387 }
392388}
0 commit comments