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,29 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
192200 this . _screenReaderAnnounce ( ) ;
193201
194202 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+ ) ;
196213 } else {
197214 clearTimeout ( this . _enterFallback ) ;
198- this . _enterFallback = setTimeout ( ( ) => this . _completeEnter ( ) , 200 ) ;
215+ this . _enterFallback = setTimeout ( ( ) => this . onAnimationEnd ( ENTER_ANIMATION ) , 200 ) ;
199216 }
200217 }
201218 }
202219
203220 /** Begin animation of the snack bar exiting from view. */
204221 exit ( ) : Observable < void > {
222+ if ( this . _destroyed ) {
223+ return of ( undefined ) ;
224+ }
225+
205226 // It's common for snack bars to be opened by random outside calls like HTTP requests or
206227 // errors. Run inside the NgZone to ensure that it functions correctly.
207228 this . _ngZone . run ( ( ) => {
@@ -221,50 +242,32 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
221242 clearTimeout ( this . _announceTimeoutId ) ;
222243
223244 if ( this . _animationsDisabled ) {
224- this . _pendingNoopAnimation = true ;
245+ afterNextRender (
246+ ( ) => {
247+ this . _ngZone . run ( ( ) => {
248+ queueMicrotask ( ( ) => this . onAnimationEnd ( EXIT_ANIMATION ) ) ;
249+ } ) ;
250+ } ,
251+ {
252+ injector : this . _injector ,
253+ } ,
254+ ) ;
225255 } else {
226256 clearTimeout ( this . _exitFallback ) ;
227- this . _exitFallback = setTimeout ( ( ) => this . _completeExit ( ) , 200 ) ;
257+ this . _exitFallback = setTimeout ( ( ) => this . onAnimationEnd ( EXIT_ANIMATION ) , 200 ) ;
228258 }
229259 } ) ;
230260
231261 return this . _onExit ;
232262 }
233263
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-
249264 /** Makes sure the exit callbacks have been invoked when the element is destroyed. */
250265 ngOnDestroy ( ) {
251266 this . _destroyed = true ;
252267 this . _clearFromModals ( ) ;
253268 this . _completeExit ( ) ;
254269 }
255270
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- */
268271 private _completeExit ( ) {
269272 clearTimeout ( this . _exitFallback ) ;
270273 queueMicrotask ( ( ) => {
@@ -360,33 +363,40 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
360363 * announce it.
361364 */
362365 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- } ) ;
366+ if ( this . _announceTimeoutId ) {
367+ return ;
390368 }
369+
370+ this . _ngZone . runOutsideAngular ( ( ) => {
371+ this . _announceTimeoutId = setTimeout ( ( ) => {
372+ if ( this . _destroyed ) {
373+ return ;
374+ }
375+
376+ const element = this . _elementRef . nativeElement ;
377+ const inertElement = element . querySelector ( '[aria-hidden]' ) ;
378+ const liveElement = element . querySelector ( '[aria-live]' ) ;
379+
380+ if ( inertElement && liveElement ) {
381+ // If an element in the snack bar content is focused before being moved
382+ // track it and restore focus after moving to the live region.
383+ let focusedElement : HTMLElement | null = null ;
384+ if (
385+ this . _platform . isBrowser &&
386+ document . activeElement instanceof HTMLElement &&
387+ inertElement . contains ( document . activeElement )
388+ ) {
389+ focusedElement = document . activeElement ;
390+ }
391+
392+ inertElement . removeAttribute ( 'aria-hidden' ) ;
393+ liveElement . appendChild ( inertElement ) ;
394+ focusedElement ?. focus ( ) ;
395+
396+ this . _onAnnounce . next ( ) ;
397+ this . _onAnnounce . complete ( ) ;
398+ }
399+ } , this . _announceDelay ) ;
400+ } ) ;
391401 }
392402}
0 commit comments