Skip to content

Commit 7c5c900

Browse files
committed
refactor(material-experimental/mdc-snack-bar): remove MDC adapter usage
Reworks the MDC snack bar so that it reuses the logic from our existing implementation instead of going through the MDC adapter. **Note:** these changes mimic MDC's logic rather than reuse our existing container with MDC's styling, despite the latter being cleaner and easier to maintain. We do it this way, because reusing our container ends up breaking tests using harnesses, because the harness ends up waiting for the container to be dismissed before resuming execution. This is the alternate approach for future reference: crisbeto@9a373d2
1 parent 988faa6 commit 7c5c900

File tree

4 files changed

+67
-60
lines changed

4 files changed

+67
-60
lines changed

src/material-experimental/mdc-snack-bar/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ ng_module(
2525
"//src/material-experimental/mdc-core",
2626
"//src/material/snack-bar",
2727
"@npm//@angular/core",
28-
"@npm//@material/snackbar",
2928
],
3029
)
3130

src/material-experimental/mdc-snack-bar/snack-bar-container.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="mdc-snackbar__surface" #surface>
1+
<div class="mdc-snackbar__surface">
22
<!--
33
This outer label wrapper will have the class `mdc-snackbar__label` applied if
44
the attached template/component does not contain it.

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

Lines changed: 64 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,20 @@ import {
2828
} from '@angular/core';
2929
import {MatSnackBarConfig, _SnackBarContainer} from '@angular/material/snack-bar';
3030
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
31-
import {MDCSnackbarAdapter, MDCSnackbarFoundation, cssClasses} from '@material/snackbar';
3231
import {Platform} from '@angular/cdk/platform';
3332
import {Observable, Subject} from 'rxjs';
3433

35-
/**
36-
* The MDC label class that should wrap the label content of the snack bar.
37-
* @docs-private
38-
*/
39-
const MDC_SNACKBAR_LABEL_CLASS = 'mdc-snackbar__label';
34+
/** The MDC label class that should wrap the label content of the snack bar. */
35+
const LABEL_CLASS = 'mdc-snackbar__label';
36+
37+
/** Class added after the snack bar is done opening. */
38+
const OPEN_CLASS = 'mdc-snackbar--open';
39+
40+
/** Class added while the snack bar is opening. */
41+
const OPENING_CLASS = 'mdc-snackbar--opening';
42+
43+
/** Class added while the snack bar is closing. */
44+
const CLOSING_CLASS = 'mdc-snackbar--closing';
4045

4146
/**
4247
* Internal component that wraps user-provided snack bar content.
@@ -72,6 +77,15 @@ export class MatSnackBarContainer
7277
/** The timeout for announcing the snack bar's content. */
7378
private _announceTimeoutId: number;
7479

80+
/** ID of the timeout for the opening/closing animation. */
81+
private _animationTimeoutId: number;
82+
83+
/** ID of the animation frame that is used to delay the opening sequence. */
84+
private _animationFrameId: number;
85+
86+
/** Whether the component is in the process of being opened. */
87+
private _isOpening = false;
88+
7589
/** Subject for notifying that the snack bar has announced to screen readers. */
7690
readonly _onAnnounce: Subject<void> = new Subject();
7791

@@ -93,24 +107,9 @@ export class MatSnackBarContainer
93107
*/
94108
_role?: 'status' | 'alert';
95109

96-
private _mdcAdapter: MDCSnackbarAdapter = {
97-
addClass: (className: string) => this._setClass(className, true),
98-
removeClass: (className: string) => this._setClass(className, false),
99-
announce: () => {},
100-
notifyClosed: () => this._finishExit(),
101-
notifyClosing: () => {},
102-
notifyOpened: () => this._onEnter.next(),
103-
notifyOpening: () => {},
104-
};
105-
106-
_mdcFoundation = new MDCSnackbarFoundation(this._mdcAdapter);
107-
108110
/** The portal outlet inside of this container into which the snack bar content will be loaded. */
109111
@ViewChild(CdkPortalOutlet, {static: true}) _portalOutlet: CdkPortalOutlet;
110112

111-
/** Element that acts as the MDC surface container which should contain the label and actions. */
112-
@ViewChild('surface', {static: true}) _surface: ElementRef;
113-
114113
/**
115114
* Element that will have the `mdc-snackbar__label` class applied if the attached component
116115
* or template does not have it. This ensures that the appropriate structure, typography, and
@@ -147,50 +146,66 @@ export class MatSnackBarContainer
147146
this._role = 'alert';
148147
}
149148
}
150-
151-
// `MatSnackBar` will use the config's timeout to determine when the snack bar should be closed.
152-
// Set this to `-1` to mark it as indefinitely open so that MDC does not close itself.
153-
this._mdcFoundation.setTimeoutMs(-1);
154-
}
155-
156-
/** Makes sure the exit callbacks have been invoked when the element is destroyed. */
157-
ngOnDestroy() {
158-
this._mdcFoundation.close();
159149
}
160150

161151
enter() {
162-
// MDC uses some browser APIs that will throw during server-side rendering.
163-
if (this._platform.isBrowser) {
164-
this._ngZone.run(() => {
165-
this._mdcFoundation.open();
166-
this._screenReaderAnnounce();
167-
});
152+
if (typeof requestAnimationFrame !== 'function') {
153+
return;
168154
}
155+
156+
this._isOpening = true;
157+
clearTimeout(this._animationTimeoutId);
158+
cancelAnimationFrame(this._animationFrameId);
159+
this._setClass(CLOSING_CLASS, false);
160+
this._setClass(OPENING_CLASS, true);
161+
this._screenReaderAnnounce();
162+
163+
// Wait a frame once display is no longer "none", to establish basis for animation
164+
this._animationFrameId = requestAnimationFrame(() => {
165+
this._setClass(OPEN_CLASS, true);
166+
167+
this._animationTimeoutId = setTimeout(() => {
168+
this._setClass(OPENING_CLASS, false);
169+
this._ngZone.run(() => this._onEnter.next());
170+
this._isOpening = false;
171+
}, 150);
172+
});
169173
}
170174

171175
exit(): Observable<void> {
172-
const classList = this._elementRef.nativeElement.classList;
176+
// If the snack bar hasn't been announced by the time it exits it wouldn't have been open
177+
// long enough to visually read it either, so clear the timeout for announcing.
178+
clearTimeout(this._announceTimeoutId);
179+
clearTimeout(this._animationTimeoutId);
173180

174-
// MDC won't complete the closing sequence if it starts while opening hasn't finished.
175-
// If that's the case, destroy immediately to ensure that our stream emits as expected.
176-
if (classList.contains(cssClasses.OPENING) || !classList.contains(cssClasses.OPEN)) {
181+
if (typeof cancelAnimationFrame === 'function') {
182+
cancelAnimationFrame(this._animationFrameId);
183+
}
184+
185+
// If the opening sequence is still in progress, close immediately.
186+
if (this._isOpening) {
177187
this._finishExit();
178188
} else {
179189
// It's common for snack bars to be opened by random outside calls like HTTP requests or
180190
// errors. Run inside the NgZone to ensure that it functions correctly.
181191
this._ngZone.run(() => {
182192
this._exiting = true;
183-
this._mdcFoundation.close();
193+
this._setClass(CLOSING_CLASS, true);
194+
this._setClass(OPEN_CLASS, false);
195+
this._setClass(OPENING_CLASS, false);
196+
this._animationTimeoutId = setTimeout(() => this._finishExit(), 75);
184197
});
185198
}
186199

187-
// If the snack bar hasn't been announced by the time it exits it wouldn't have been open
188-
// long enough to visually read it either, so clear the timeout for announcing.
189-
clearTimeout(this._announceTimeoutId);
190-
191200
return this._onExit;
192201
}
193202

203+
ngOnDestroy() {
204+
if (!this._exiting) {
205+
this.exit();
206+
}
207+
}
208+
194209
/** Attach a component portal as content to this snack bar container. */
195210
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
196211
this._assertNotAttached();
@@ -235,12 +250,9 @@ export class MatSnackBarContainer
235250

236251
/** Finishes the exit sequence of the container. */
237252
private _finishExit() {
253+
clearTimeout(this._animationTimeoutId);
238254
this._onExit.next();
239255
this._onExit.complete();
240-
241-
if (this._platform.isBrowser) {
242-
this._mdcFoundation.destroy();
243-
}
244256
}
245257

246258
/**
@@ -284,10 +296,10 @@ export class MatSnackBarContainer
284296
// component's label container, which will apply MDC's label styles to the attached view.
285297
const label = this._label.nativeElement;
286298

287-
if (!label.querySelector(`.${MDC_SNACKBAR_LABEL_CLASS}`)) {
288-
label.classList.add(MDC_SNACKBAR_LABEL_CLASS);
299+
if (!label.querySelector(`.${LABEL_CLASS}`)) {
300+
label.classList.add(LABEL_CLASS);
289301
} else {
290-
label.classList.remove(MDC_SNACKBAR_LABEL_CLASS);
302+
label.classList.remove(LABEL_CLASS);
291303
}
292304
}
293305
}

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
SimpleSnackBar,
1818
MatSnackBar,
1919
MatSnackBarConfig,
20-
MatSnackBarContainer,
2120
MatSnackBarModule,
2221
MatSnackBarRef,
2322
} from './index';
@@ -568,18 +567,15 @@ describe('MatSnackBar', () => {
568567
}));
569568

570569
it('should dismiss the open snack bar on destroy', fakeAsync(() => {
571-
const snackBarRef = snackBar.open(simpleMessage);
570+
snackBar.open(simpleMessage);
572571
viewContainerFixture.detectChanges();
573572
expect(overlayContainerElement.childElementCount).toBeGreaterThan(0);
574573

575-
const foundation = (snackBarRef.containerInstance as MatSnackBarContainer)._mdcFoundation;
576-
spyOn(foundation, 'destroy').and.callThrough();
577-
578574
snackBar.ngOnDestroy();
575+
viewContainerFixture.detectChanges();
579576
flush();
580577

581578
expect(overlayContainerElement.childElementCount).toBe(0);
582-
expect(foundation.destroy).toHaveBeenCalled();
583579
}));
584580

585581
it('should cap the timeout to the maximum accepted delay in setTimeout', fakeAsync(() => {

0 commit comments

Comments
 (0)