Skip to content

Commit 6ce6396

Browse files
josephperrottkara
authored andcommitted
feat(snackbar): add timeout for snackbar (#1856)
1 parent 8caf9a6 commit 6ce6396

File tree

8 files changed

+86
-15
lines changed

8 files changed

+86
-15
lines changed

src/demo-app/snack-bar/snack-bar-demo.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ <h1>SnackBar demo</h1>
1010
[(ngModel)]="actionButtonLabel"></md-input>
1111
</md-checkbox>
1212
</div>
13+
<div>
14+
<md-checkbox [(ngModel)]="setAutoHide">
15+
<p *ngIf="!setAutoHide">Auto hide after duration</p>
16+
<md-input type="number" class="demo-button-label-input"
17+
*ngIf="setAutoHide"
18+
placeholder="Auto Hide Duration in ms"
19+
[(ngModel)]="autoHide"></md-input>
20+
</md-checkbox>
21+
</div>
1322
</div>
1423

1524
<button md-raised-button (click)="open()">OPEN</button>

src/demo-app/snack-bar/snack-bar-demo.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component} from '@angular/core';
2-
import {MdSnackBar} from '@angular/material';
2+
import {MdSnackBar, MdSnackBarConfig} from '@angular/material';
33

44
@Component({
55
moduleId: module.id,
@@ -10,11 +10,15 @@ export class SnackBarDemo {
1010
message: string = 'Snack Bar opened.';
1111
actionButtonLabel: string = 'Retry';
1212
action: boolean = false;
13+
setAutoHide: boolean = true;
14+
autoHide: number = 0;
1315

1416
constructor(
1517
public snackBar: MdSnackBar) { }
1618

1719
open() {
18-
this.snackBar.open(this.message, this.action && this.actionButtonLabel);
20+
let config = new MdSnackBarConfig();
21+
config.duration = this.autoHide;
22+
this.snackBar.open(this.message, this.action && this.actionButtonLabel, config);
1923
}
2024
}

src/lib/snack-bar/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
| `viewContainerRef: ViewContainerRef` | The view container ref to attach the snack bar to. |
1616
| `role: AriaLivePoliteness = 'assertive'` | The politeness level to announce the snack bar with. |
1717
| `announcementMessage: string` | The message to announce with the snack bar. |
18+
| `duration: number` | The length of time in milliseconds to wait before autodismissing the snack bar. |
1819

1920

2021
### Example

src/lib/snack-bar/snack-bar-config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@ export class MdSnackBarConfig {
1313

1414
/** The view container to place the overlay for the snack bar into. */
1515
viewContainerRef?: ViewContainerRef = null;
16+
17+
/** The length of time in milliseconds to wait before automatically dismissing the snack bar. */
18+
duration?: number = 0;
1619
}

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const HIDE_ANIMATION = '195ms cubic-bezier(0.0,0.0,0.2,1)';
4141
host: {
4242
'role': 'alert',
4343
'[@state]': 'animationState',
44-
'(@state.done)': 'markAsExited($event)'
44+
'(@state.done)': 'onAnimationEnd($event)'
4545
},
4646
animations: [
4747
trigger('state', [
@@ -58,7 +58,10 @@ export class MdSnackBarContainer extends BasePortalHost {
5858
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
5959

6060
/** Subject for notifying that the snack bar has exited from view. */
61-
private _onExit: Subject<any> = new Subject();
61+
private onExit: Subject<any> = new Subject();
62+
63+
/** Subject for notifying that the snack bar has finished entering the view. */
64+
private onEnter: Subject<any> = new Subject();
6265

6366
/** The state of the snack bar animations. */
6467
animationState: SnackBarState = 'initial';
@@ -87,15 +90,21 @@ export class MdSnackBarContainer extends BasePortalHost {
8790
/** Begin animation of the snack bar exiting from view. */
8891
exit(): Observable<void> {
8992
this.animationState = 'complete';
90-
return this._onExit.asObservable();
93+
return this.onExit.asObservable();
9194
}
9295

93-
/** Mark snack bar as exited from the view. */
94-
markAsExited(event: AnimationTransitionEvent) {
96+
/** Handle end of animations, updating the state of the snackbar. */
97+
onAnimationEnd(event: AnimationTransitionEvent) {
9598
if (event.toState === 'void' || event.toState === 'complete') {
9699
this._ngZone.run(() => {
97-
this._onExit.next();
98-
this._onExit.complete();
100+
this.onExit.next();
101+
this.onExit.complete();
102+
});
103+
}
104+
if (event.toState === 'visible') {
105+
this._ngZone.run(() => {
106+
this.onEnter.next();
107+
this.onEnter.complete();
99108
});
100109
}
101110
}
@@ -104,4 +113,9 @@ export class MdSnackBarContainer extends BasePortalHost {
104113
enter(): void {
105114
this.animationState = 'visible';
106115
}
116+
117+
/** Returns an observable resolving when the enter animation completes. */
118+
_onEnter(): Observable<void> {
119+
return this.onEnter.asObservable();
120+
}
107121
}

src/lib/snack-bar/snack-bar-ref.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export class MdSnackBarRef<T> {
1919
/** Subject for notifying the user that the snack bar has closed. */
2020
private _afterClosed: Subject<any> = new Subject();
2121

22+
/** Subject for notifying the user that the snack bar has opened and appeared. */
23+
private _afterOpened: Subject<any>;
24+
2225
/** Subject for notifying the user that the snack bar action was called. */
2326
private _onAction: Subject<any> = new Subject();
2427

@@ -51,11 +54,24 @@ export class MdSnackBarRef<T> {
5154
}
5255
}
5356

57+
/** Marks the snackbar as opened */
58+
_open(): void {
59+
if (!this._afterOpened.closed) {
60+
this._afterOpened.next();
61+
this._afterOpened.complete();
62+
}
63+
}
64+
5465
/** Gets an observable that is notified when the snack bar is finished closing. */
5566
afterDismissed(): Observable<void> {
5667
return this._afterClosed.asObservable();
5768
}
5869

70+
/** Gets an observable that is notified when the snack bar has opened and appeared. */
71+
afterOpened(): Observable<void> {
72+
return this.containerInstance._onEnter();
73+
}
74+
5975
/** Gets an observable that is notified when the snack bar action is called. */
6076
onAction(): Observable<void> {
6177
return this._onAction.asObservable();

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '@angular/core/testing';
1010
import {NgModule, Component, Directive, ViewChild, ViewContainerRef} from '@angular/core';
1111
import {MdSnackBar, MdSnackBarModule} from './snack-bar';
12+
import {MdSnackBarConfig} from './snack-bar-config';
1213
import {OverlayContainer, MdLiveAnnouncer} from '../core';
1314
import {SimpleSnackBar} from './simple-snack-bar';
1415

@@ -280,13 +281,30 @@ describe('MdSnackBar', () => {
280281
viewContainerFixture.detectChanges();
281282
flushMicrotasks();
282283

283-
viewContainerFixture.whenStable().then(() => {
284-
expect(dismissObservableCompleted).toBeTruthy('Expected the snack bar to be dismissed');
285-
expect(actionObservableCompleted).toBeTruthy('Expected the snack bar to notify of action');
286-
});
284+
expect(dismissObservableCompleted).toBeTruthy('Expected the snack bar to be dismissed');
285+
expect(actionObservableCompleted).toBeTruthy('Expected the snack bar to notify of action');
287286

288287
tick(500);
289288
}));
289+
290+
it('should dismiss automatically after a specified timeout', fakeAsync(() => {
291+
let dismissObservableCompleted = false;
292+
let config = new MdSnackBarConfig();
293+
config.duration = 250;
294+
let snackBarRef = snackBar.open('content', 'test', config);
295+
snackBarRef.afterDismissed().subscribe(null, null, () => {
296+
dismissObservableCompleted = true;
297+
});
298+
299+
viewContainerFixture.detectChanges();
300+
flushMicrotasks();
301+
expect(dismissObservableCompleted).toBeFalsy('Expected the snack bar not to be dismissed');
302+
303+
tick(1000);
304+
viewContainerFixture.detectChanges();
305+
flushMicrotasks();
306+
expect(dismissObservableCompleted).toBeTruthy('Expected the snack bar to be dismissed');
307+
}));
290308
});
291309

292310
@Directive({selector: 'dir-with-view-container'})

src/lib/snack-bar/snack-bar.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import {MdSnackBarContainer} from './snack-bar-container';
2222
import {SimpleSnackBar} from './simple-snack-bar';
2323
import {extendObject} from '../core/util/object-extend';
2424

25-
// TODO(josephperrott): Automate dismiss after timeout.
26-
2725

2826
/**
2927
* Service to dispatch Material Design snack bar messages.
@@ -64,6 +62,14 @@ export class MdSnackBar {
6462
} else {
6563
snackBarRef.containerInstance.enter();
6664
}
65+
66+
// If a dismiss timeout is provided, set up dismiss based on after the snackbar is opened.
67+
if (config.duration > 0) {
68+
snackBarRef.afterOpened().subscribe(() => {
69+
setTimeout(() => snackBarRef.dismiss(), config.duration);
70+
});
71+
}
72+
6773
this._live.announce(config.announcementMessage, config.politeness);
6874
this._snackBarRef = snackBarRef;
6975
return this._snackBarRef;

0 commit comments

Comments
 (0)