Skip to content

Commit bf8c558

Browse files
committed
fixup! fix(material/snack-bar): switch away from animations module
1 parent 392ae2c commit bf8c558

File tree

4 files changed

+108
-78
lines changed

4 files changed

+108
-78
lines changed

src/material/snack-bar/snack-bar-container.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ $_side-padding: 8px;
4545
.mat-snack-bar-container-animations-enabled {
4646
opacity: 0;
4747

48+
// Fallback in case the animation fails.
49+
&.mat-snack-bar-fallback-visible {
50+
opacity: 1;
51+
}
52+
4853
&.mat-snack-bar-container-enter {
4954
animation: _mat-snack-bar-enter 150ms cubic-bezier(0, 0, 0.2, 1) forwards;
5055
}

src/material/snack-bar/snack-bar-container.ts

Lines changed: 96 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@
77
*/
88

99
import {
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';
3334
import {_IdGenerator, AriaLivePoliteness} from '@angular/cdk/a11y';
3435
import {Platform} from '@angular/cdk/platform';
3536
import {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;
@@ -82,6 +86,9 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
8286
/** Whether the component has been destroyed. */
8387
private _destroyed = false;
8488

89+
/** Whether the process of exiting is currently running. */
90+
private _isExiting = false;
91+
8592
/** The portal outlet inside of this container into which the snack bar content will be loaded. */
8693
@ViewChild(CdkPortalOutlet, {static: true}) _portalOutlet: CdkPortalOutlet;
8794

@@ -173,11 +180,15 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
173180
};
174181

175182
/** 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) {
178185
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+
});
181192
}
182193
}
183194

@@ -192,16 +203,43 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
192203
this._screenReaderAnnounce();
193204

194205
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+
);
196216
} else {
197217
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);
199224
}
200225
}
201226
}
202227

203228
/** Begin animation of the snack bar exiting from view. */
204229
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+
205243
// It's common for snack bars to be opened by random outside calls like HTTP requests or
206244
// errors. Run inside the NgZone to ensure that it functions correctly.
207245
this._ngZone.run(() => {
@@ -221,55 +259,38 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
221259
clearTimeout(this._announceTimeoutId);
222260

223261
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+
);
225272
} else {
226273
clearTimeout(this._exitFallback);
227-
this._exitFallback = setTimeout(() => this._completeExit(), 200);
274+
this._exitFallback = setTimeout(() => this.onAnimationEnd(EXIT_ANIMATION), 200);
228275
}
229276
});
230277

231278
return this._onExit;
232279
}
233280

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-
249281
/** Makes sure the exit callbacks have been invoked when the element is destroyed. */
250282
ngOnDestroy() {
251283
this._destroyed = true;
252284
this._clearFromModals();
253285
this._completeExit();
254286
}
255287

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-
*/
268288
private _completeExit() {
269289
clearTimeout(this._exitFallback);
270290
queueMicrotask(() => {
271291
this._onExit.next();
272292
this._onExit.complete();
293+
this._isExiting = true;
273294
});
274295
}
275296

@@ -360,33 +381,40 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
360381
* announce it.
361382
*/
362383
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;
390386
}
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+
});
391419
}
392420
}

src/material/snack-bar/snack-bar.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ export class MatSnackBar implements OnDestroy {
242242
}
243243
});
244244

245+
// If a dismiss timeout is provided, set up dismiss based on after the snackbar is opened.
246+
if (config.duration && config.duration > 0) {
247+
snackBarRef.afterOpened().subscribe(() => snackBarRef._dismissAfter(config.duration!));
248+
}
249+
245250
if (this._openedSnackBarRef) {
246251
// If a snack bar is already in view, dismiss it and enter the
247252
// new snack bar after exit animation is complete.
@@ -253,11 +258,6 @@ export class MatSnackBar implements OnDestroy {
253258
// If no snack bar is in view, enter the new snack bar.
254259
snackBarRef.containerInstance.enter();
255260
}
256-
257-
// If a dismiss timeout is provided, set up dismiss based on after the snackbar is opened.
258-
if (config.duration && config.duration > 0) {
259-
snackBarRef.afterOpened().subscribe(() => snackBarRef._dismissAfter(config.duration!));
260-
}
261261
}
262262

263263
/**

tools/public_api_guard/material/snack-bar.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { ComponentPortal } from '@angular/cdk/portal';
1212
import { ComponentRef } from '@angular/core';
1313
import { ComponentType } from '@angular/cdk/overlay';
1414
import { Direction } from '@angular/cdk/bidi';
15-
import { DoCheck } from '@angular/core';
1615
import { DomPortal } from '@angular/cdk/portal';
1716
import { ElementRef } from '@angular/core';
1817
import { EmbeddedViewRef } from '@angular/core';
@@ -94,7 +93,7 @@ export class MatSnackBarConfig<D = any> {
9493
}
9594

9695
// @public
97-
export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, OnDestroy {
96+
export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy {
9897
constructor(...args: unknown[]);
9998
// (undocumented)
10099
protected _animationsDisabled: boolean;
@@ -108,10 +107,8 @@ export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, O
108107
_label: ElementRef;
109108
_live: AriaLivePoliteness;
110109
readonly _liveElementId: string;
111-
// (undocumented)
112-
ngDoCheck(): void;
113110
ngOnDestroy(): void;
114-
onAnimationEnd(event: AnimationEvent): void;
111+
onAnimationEnd(animationName: string): void;
115112
readonly _onAnnounce: Subject<void>;
116113
readonly _onEnter: Subject<void>;
117114
readonly _onExit: Subject<void>;

0 commit comments

Comments
 (0)