Skip to content

Commit 5483913

Browse files
committed
fix(material-experimental/mdc-snack-bar): avoid multiple snack bars on the page if opened in quick succession
Fixes an issue where opening a snack bar while the previous one was being animated caused the former to remain in the DOM. The problem was that MDC always waits for an animation, even if it is interrupted.
1 parent 26e6c1f commit 5483913

File tree

3 files changed

+51
-15
lines changed

3 files changed

+51
-15
lines changed

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

+29-14
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
} from '@angular/core';
3030
import {MatSnackBarConfig, _SnackBarContainer} from '@angular/material/snack-bar';
3131
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
32-
import {MDCSnackbarAdapter, MDCSnackbarFoundation} from '@material/snackbar';
32+
import {MDCSnackbarAdapter, MDCSnackbarFoundation, cssClasses} from '@material/snackbar';
3333
import {Platform} from '@angular/cdk/platform';
3434
import {Observable, Subject} from 'rxjs';
3535

@@ -98,10 +98,7 @@ export class MatSnackBarContainer
9898
addClass: (className: string) => this._setClass(className, true),
9999
removeClass: (className: string) => this._setClass(className, false),
100100
announce: () => {},
101-
notifyClosed: () => {
102-
this._onExit.next();
103-
this._mdcFoundation.destroy();
104-
},
101+
notifyClosed: () => this._finishExit(),
105102
notifyClosing: () => {},
106103
notifyOpened: () => this._onEnter.next(),
107104
notifyOpening: () => {},
@@ -184,16 +181,24 @@ export class MatSnackBarContainer
184181
}
185182

186183
exit(): Observable<void> {
187-
// It's common for snack bars to be opened by random outside calls like HTTP requests or
188-
// errors. Run inside the NgZone to ensure that it functions correctly.
189-
this._ngZone.run(() => {
190-
this._exiting = true;
191-
this._mdcFoundation.close();
184+
const classList = this._elementRef.nativeElement.classList;
185+
186+
// MDC won't complete the closing sequence if it starts while opening hasn't finished.
187+
// If that's the case, destroy immediately to ensure that our stream emits as expected.
188+
if (classList.contains(cssClasses.OPENING) || !classList.contains(cssClasses.OPEN)) {
189+
this._finishExit();
190+
} else {
191+
// It's common for snack bars to be opened by random outside calls like HTTP requests or
192+
// errors. Run inside the NgZone to ensure that it functions correctly.
193+
this._ngZone.run(() => {
194+
this._exiting = true;
195+
this._mdcFoundation.close();
196+
});
197+
}
192198

193-
// If the snack bar hasn't been announced by the time it exits it wouldn't have been open
194-
// long enough to visually read it either, so clear the timeout for announcing.
195-
clearTimeout(this._announceTimeoutId);
196-
});
199+
// If the snack bar hasn't been announced by the time it exits it wouldn't have been open
200+
// long enough to visually read it either, so clear the timeout for announcing.
201+
clearTimeout(this._announceTimeoutId);
197202

198203
return this._onExit;
199204
}
@@ -236,6 +241,16 @@ export class MatSnackBarContainer
236241
}
237242
}
238243

244+
/** Finishes the exit sequence of the container. */
245+
private _finishExit() {
246+
this._onExit.next();
247+
this._onExit.complete();
248+
249+
if (this._platform.isBrowser) {
250+
this._mdcFoundation.destroy();
251+
}
252+
}
253+
239254
/**
240255
* Starts a timeout to move the snack bar content to the live region so screen readers will
241256
* announce it.

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -288,15 +288,16 @@ describe('MatSnackBar', () => {
288288

289289
let snackBarRef = snackBar.open(simpleMessage, undefined, config);
290290
viewContainerFixture.detectChanges();
291+
flush();
291292
expect(overlayContainerElement.childElementCount)
292293
.withContext('Expected overlay container element to have at least one child')
293294
.toBeGreaterThan(0);
294295

295296
snackBarRef.afterDismissed().subscribe({complete: dismissCompleteSpy});
297+
const messageElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
296298

297299
snackBarRef.dismiss();
298300
viewContainerFixture.detectChanges();
299-
const messageElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
300301
expect(messageElement.hasAttribute('mat-exit'))
301302
.withContext('Expected the snackbar container to have the "exit" attribute upon dismiss')
302303
.toBe(true);
@@ -587,6 +588,16 @@ describe('MatSnackBar', () => {
587588
flush();
588589
}));
589590

591+
it('should only keep one snack bar in the DOM if multiple are opened at the same time', fakeAsync(() => {
592+
for (let i = 0; i < 10; i++) {
593+
snackBar.open('Snack time!', 'Chew');
594+
viewContainerFixture.detectChanges();
595+
}
596+
597+
flush();
598+
expect(overlayContainerElement.querySelectorAll('mat-snack-bar-container').length).toBe(1);
599+
}));
600+
590601
describe('with custom component', () => {
591602
it('should open a custom component', () => {
592603
const snackBarRef = snackBar.openFromComponent(BurritosNotification);

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

+10
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,16 @@ describe('MatSnackBar', () => {
651651
flush();
652652
}));
653653

654+
it('should only keep one snack bar in the DOM if multiple are opened at the same time', fakeAsync(() => {
655+
for (let i = 0; i < 10; i++) {
656+
snackBar.open('Snack time!', 'Chew');
657+
viewContainerFixture.detectChanges();
658+
}
659+
660+
flush();
661+
expect(overlayContainerElement.querySelectorAll('snack-bar-container').length).toBe(1);
662+
}));
663+
654664
describe('with custom component', () => {
655665
it('should open a custom component', () => {
656666
const snackBarRef = snackBar.openFromComponent(BurritosNotification);

0 commit comments

Comments
 (0)