Skip to content

Commit 392ae2c

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

File tree

4 files changed

+72
-50
lines changed

4 files changed

+72
-50
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,15 @@ $_side-padding: 8px;
4242
}
4343
}
4444

45-
.mat-snack-bar-animations-enabled {
46-
animation: _mat-snack-bar-enter 150ms cubic-bezier(0, 0, 0.2, 1);
45+
.mat-snack-bar-container-animations-enabled {
46+
opacity: 0;
4747

48-
&[mat-exit] {
49-
animation: _mat-snack-bar-exit 75ms cubic-bezier(0.4, 0, 1, 1);
48+
&.mat-snack-bar-container-enter {
49+
animation: _mat-snack-bar-enter 150ms cubic-bezier(0, 0, 0.2, 1) forwards;
50+
}
51+
52+
&.mat-snack-bar-container-exit {
53+
animation: _mat-snack-bar-exit 75ms cubic-bezier(0.4, 0, 1, 1) forwards;
5054
}
5155
}
5256

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

Lines changed: 60 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -51,30 +51,33 @@ import {MatSnackBarConfig} from './snack-bar-config';
5151
imports: [CdkPortalOutlet],
5252
host: {
5353
'class': 'mdc-snackbar mat-mdc-snack-bar-container',
54-
'[class.mat-snack-bar-animations-enabled]': '!_animationsDisabled',
55-
'(animationend)': '_animationDone($event)',
54+
'[class.mat-snack-bar-container-enter]': '_animationState === "visible"',
55+
'[class.mat-snack-bar-container-exit]': '_animationState === "hidden"',
56+
'[class.mat-snack-bar-container-animations-enabled]': '!_animationsDisabled',
57+
'(animationend)': 'onAnimationEnd($event)',
58+
'(animationcancel)': 'onAnimationEnd($event)',
5659
},
5760
})
58-
export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy, DoCheck {
61+
export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, OnDestroy {
5962
private _ngZone = inject(NgZone);
6063
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
61-
private _platform = inject(Platform);
6264
private _changeDetectorRef = inject(ChangeDetectorRef);
63-
private _enterFallback: ReturnType<typeof setTimeout> | undefined;
64-
private _exitFallback: ReturnType<typeof setTimeout> | undefined;
65+
private _platform = inject(Platform);
6566
protected _animationsDisabled =
6667
inject(ANIMATION_MODULE_TYPE, {optional: true}) === 'NoopAnimations';
67-
private _scheduleDelayedEnter: boolean;
68-
6968
snackBarConfig = inject(MatSnackBarConfig);
69+
7070
private _document = inject(DOCUMENT);
7171
private _trackedModals = new Set<Element>();
72+
private _enterFallback: ReturnType<typeof setTimeout> | undefined;
73+
private _exitFallback: ReturnType<typeof setTimeout> | undefined;
74+
private _pendingNoopAnimation: boolean;
7275

7376
/** The number of milliseconds to wait before announcing the snack bar's content. */
7477
private readonly _announceDelay: number = 150;
7578

7679
/** The timeout for announcing the snack bar's content. */
77-
private _announceTimeoutId: ReturnType<typeof setTimeout> | undefined;
80+
private _announceTimeoutId: ReturnType<typeof setTimeout>;
7881

7982
/** Whether the component has been destroyed. */
8083
private _destroyed = false;
@@ -91,6 +94,9 @@ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy,
9194
/** Subject for notifying that the snack bar has finished entering the view. */
9295
readonly _onEnter: Subject<void> = new Subject();
9396

97+
/** The state of the snack bar animations. */
98+
_animationState = 'void';
99+
94100
/** aria-live value for the live region. */
95101
_live: AriaLivePoliteness;
96102

@@ -167,32 +173,27 @@ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy,
167173
};
168174

169175
/** Handle end of animations, updating the state of the snackbar. */
170-
protected _animationDone(event: AnimationEvent) {
171-
if (event.animationName === '_mat-snack-bar-enter') {
172-
this._completeEnter();
173-
} else if (event.animationName === '_mat-snack-bar-exit') {
176+
onAnimationEnd(event: AnimationEvent) {
177+
if (event.animationName === '_mat-snack-bar-exit') {
174178
this._completeExit();
179+
} else if (event.animationName === '_mat-snack-bar-enter') {
180+
this._completeEnter();
175181
}
176182
}
177183

178184
/** Begin animation of snack bar entrance into view. */
179185
enter(): void {
180186
if (!this._destroyed) {
181-
// Previously this was used to ensure that the change-detection-based animation runs.
182-
// Now the animation doesn't require change detection, but there seem to be some internal
183-
// usages depending on it.
187+
this._animationState = 'visible';
188+
// _animationState lives in host bindings and `detectChanges` does not refresh host bindings
189+
// so we have to call `markForCheck` to ensure the host view is refreshed eventually.
184190
this._changeDetectorRef.markForCheck();
185191
this._changeDetectorRef.detectChanges();
186192
this._screenReaderAnnounce();
187193

188194
if (this._animationsDisabled) {
189-
// Delay the enter until the next change detection in an attempt to mimic the timing of
190-
// the old animation-based events. Ideally we would use an `afterNextRender` here, but
191-
// some tests throw a "Injector has already been destroyed" error.
192-
this._scheduleDelayedEnter = true;
195+
this._pendingNoopAnimation = true;
193196
} else {
194-
// Guarantees that the animation-related events will
195-
// fire even if something interrupts the animation.
196197
clearTimeout(this._enterFallback);
197198
this._enterFallback = setTimeout(() => this._completeEnter(), 200);
198199
}
@@ -201,35 +202,55 @@ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy,
201202

202203
/** Begin animation of the snack bar exiting from view. */
203204
exit(): Observable<void> {
204-
// Mark this element with an 'exit' attribute to indicate that the snackbar has
205-
// been dismissed and will soon be removed from the DOM. This is used by the snackbar
206-
// test harness.
207-
this._elementRef.nativeElement.setAttribute('mat-exit', '');
205+
// It's common for snack bars to be opened by random outside calls like HTTP requests or
206+
// errors. Run inside the NgZone to ensure that it functions correctly.
207+
this._ngZone.run(() => {
208+
// Note: this one transitions to `hidden`, rather than `void`, in order to handle the case
209+
// where multiple snack bars are opened in quick succession (e.g. two consecutive calls to
210+
// `MatSnackBar.open`).
211+
this._animationState = 'hidden';
212+
this._changeDetectorRef.markForCheck();
208213

209-
// Guarantees that the animation-related events will
210-
// fire even if something interrupts the animation.
211-
clearTimeout(this._exitFallback);
212-
this._exitFallback = setTimeout(
213-
() => this._completeExit(),
214-
this._animationsDisabled ? undefined : 150,
215-
);
214+
// Mark this element with an 'exit' attribute to indicate that the snackbar has
215+
// been dismissed and will soon be removed from the DOM. This is used by the snackbar
216+
// test harness.
217+
this._elementRef.nativeElement.setAttribute('mat-exit', '');
218+
219+
// If the snack bar hasn't been announced by the time it exits it wouldn't have been open
220+
// long enough to visually read it either, so clear the timeout for announcing.
221+
clearTimeout(this._announceTimeoutId);
222+
223+
if (this._animationsDisabled) {
224+
this._pendingNoopAnimation = true;
225+
} else {
226+
clearTimeout(this._exitFallback);
227+
this._exitFallback = setTimeout(() => this._completeExit(), 200);
228+
}
229+
});
216230

217231
return this._onExit;
218232
}
219233

220234
ngDoCheck(): void {
221-
if (this._scheduleDelayedEnter) {
222-
this._scheduleDelayedEnter = false;
223-
this._completeEnter();
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+
});
224246
}
225247
}
226248

249+
/** Makes sure the exit callbacks have been invoked when the element is destroyed. */
227250
ngOnDestroy() {
228-
clearTimeout(this._enterFallback);
229251
this._destroyed = true;
230252
this._clearFromModals();
231253
this._completeExit();
232-
this._onAnnounce.complete();
233254
}
234255

235256
private _completeEnter() {
@@ -245,11 +266,8 @@ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy,
245266
* removing an element which is in the middle of an animation.
246267
*/
247268
private _completeExit() {
248-
// If the snack bar hasn't been announced by the time it exits it wouldn't have been open
249-
// long enough to visually read it either, so clear the timeout for announcing.
250-
clearTimeout(this._announceTimeoutId);
251269
clearTimeout(this._exitFallback);
252-
this._ngZone.run(() => {
270+
queueMicrotask(() => {
253271
this._onExit.next();
254272
this._onExit.complete();
255273
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ describe('MatSnackBar', () => {
564564
viewContainerFixture.detectChanges();
565565
}
566566

567-
flush();
567+
flush(50);
568568
expect(overlayContainerElement.querySelectorAll('mat-snack-bar-container').length).toBe(1);
569569
}));
570570

tools/public_api_guard/material/snack-bar.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,11 @@ export class MatSnackBarConfig<D = any> {
9494
}
9595

9696
// @public
97-
export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy, DoCheck {
97+
export class MatSnackBarContainer extends BasePortalOutlet implements DoCheck, OnDestroy {
9898
constructor(...args: unknown[]);
99-
protected _animationDone(event: AnimationEvent): void;
10099
// (undocumented)
101100
protected _animationsDisabled: boolean;
101+
_animationState: string;
102102
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T>;
103103
// @deprecated
104104
attachDomPortal: (portal: DomPortal) => void;
@@ -110,8 +110,8 @@ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy,
110110
readonly _liveElementId: string;
111111
// (undocumented)
112112
ngDoCheck(): void;
113-
// (undocumented)
114113
ngOnDestroy(): void;
114+
onAnimationEnd(event: AnimationEvent): void;
115115
readonly _onAnnounce: Subject<void>;
116116
readonly _onEnter: Subject<void>;
117117
readonly _onExit: Subject<void>;

0 commit comments

Comments
 (0)