Skip to content

Commit 69a0ccf

Browse files
committed
Add enter and exit animation to MdSnackBar.
1 parent 178323c commit 69a0ccf

File tree

8 files changed

+157
-23
lines changed

8 files changed

+157
-23
lines changed

src/lib/core/animation/animation.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class AnimationCurves {
2+
static get standardCurve(): string { return 'cubic-bezier(0.4,0.0,0.2,1)'; }
3+
static get decelerationCurve(): string { return 'cubic-bezier(0.0,0.0,0.2,1)'; }
4+
static get accelerationCurve(): string { return 'cubic-bezier(0.4,0.0,1,1)'; }
5+
static get sharpCurve(): string { return 'cubic-bezier(0.4,0.0,0.6,1)'; }
6+
};
7+
8+
9+
export class AnimationDurations {
10+
static get complex(): string { return '375ms'; }
11+
static get entering(): string { return '225ms'; }
12+
static get exiting(): string { return '195ms'; }
13+
};

src/lib/core/core.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export {ComponentType} from './overlay/generic-component-type';
7575
// Keybindings
7676
export * from './keyboard/keycodes';
7777

78+
// Animation
79+
export * from './animation/animation';
80+
7881

7982
@NgModule({
8083
imports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule],

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ $md-snack-bar-max-width: 568px !default;
1616
min-width: $md-snack-bar-min-width;
1717
overflow: hidden;
1818
padding: $md-snack-bar-padding;
19+
// Initial transformation is applied to start snack bar out of view.
20+
transform: translateY(100%);
1921
}
Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
11
import {
22
Component,
33
ComponentRef,
4-
ViewChild
4+
ViewChild,
5+
trigger,
6+
state,
7+
style,
8+
transition,
9+
animate,
10+
AnimationTransitionEvent,
11+
NgZone
512
} from '@angular/core';
613
import {
714
BasePortalHost,
815
ComponentPortal,
916
TemplatePortal,
10-
PortalHostDirective
17+
PortalHostDirective,
18+
AnimationCurves,
19+
AnimationDurations,
1120
} from '../core';
1221
import {MdSnackBarConfig} from './snack-bar-config';
1322
import {MdSnackBarContentAlreadyAttached} from './snack-bar-errors';
23+
import {Observable} from 'rxjs/Observable';
24+
import {Subject} from 'rxjs/Subject';
1425

1526

27+
28+
export type SnackBarState = 'initial' | 'visible' | 'complete' | 'void';
29+
1630
/**
1731
* Internal component that wraps user-provided snack bar content.
1832
*/
@@ -22,17 +36,40 @@ import {MdSnackBarContentAlreadyAttached} from './snack-bar-errors';
2236
templateUrl: 'snack-bar-container.html',
2337
styleUrls: ['snack-bar-container.css'],
2438
host: {
25-
'role': 'alert'
26-
}
39+
'role': 'alert',
40+
'[@state]': 'animationState',
41+
'(@state.done)': 'markAsExited($event)'
42+
},
43+
animations: [
44+
trigger('state', [
45+
state('initial', style({transform: 'translateY(100%)'})),
46+
state('visible', style({transform: 'translateY(0%)'})),
47+
state('complete', style({transform: 'translateY(100%)'})),
48+
transition('visible => complete',
49+
animate(`${AnimationDurations.exiting} ${AnimationCurves.decelerationCurve}`)),
50+
transition('initial => visible, void => visible',
51+
animate(`${AnimationDurations.entering} ${AnimationCurves.accelerationCurve}`)),
52+
])
53+
],
2754
})
2855
export class MdSnackBarContainer extends BasePortalHost {
2956
/** The portal host inside of this container into which the snack bar content will be loaded. */
3057
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
3158

59+
/** Subject for notifying that the snack bar has exited from view. */
60+
private _onExit: Subject<any> = new Subject();
61+
62+
/** The state of the snack bar animations. */
63+
animationState: SnackBarState = 'initial';
64+
3265
/** The snack bar configuration. */
3366
snackBarConfig: MdSnackBarConfig;
3467

35-
/** Attach a portal as content to this snack bar container. */
68+
constructor(private _ngZone: NgZone) {
69+
super();
70+
}
71+
72+
/** Attach a component portal as content to this snack bar container. */
3673
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
3774
if (this._portalHost.hasAttached()) {
3875
throw new MdSnackBarContentAlreadyAttached();
@@ -41,7 +78,30 @@ export class MdSnackBarContainer extends BasePortalHost {
4178
return this._portalHost.attachComponentPortal(portal);
4279
}
4380

81+
/** Attach a template portal as content to this snack bar container. */
4482
attachTemplatePortal(portal: TemplatePortal): Map<string, any> {
4583
throw Error('Not yet implemented');
4684
}
85+
86+
/** Begin animation of the snack bar exiting from view. */
87+
exit(): Observable<void> {
88+
this.animationState = 'complete';
89+
return this._onExit.asObservable();
90+
}
91+
92+
/** Mark snack bar as exited from the view. */
93+
markAsExited(event: AnimationTransitionEvent) {
94+
if (event.fromState === 'visible' &&
95+
(event.toState === 'void' || event.toState === 'complete')) {
96+
this._ngZone.run(() => {
97+
this._onExit.next();
98+
this._onExit.complete();
99+
});
100+
}
101+
}
102+
103+
/** Begin animation of snack bar entrance into view. */
104+
enter(): void {
105+
this.animationState = 'visible';
106+
}
47107
}

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {OverlayRef} from '../core';
22
import {Observable} from 'rxjs/Observable';
33
import {Subject} from 'rxjs/Subject';
4+
import {MdSnackBarContainer} from './snack-bar-container';
45

56
// TODO(josephperrott): Implement onAction observable.
67

@@ -12,19 +13,28 @@ export class MdSnackBarRef<T> {
1213
/** The instance of the component making up the content of the snack bar. */
1314
readonly instance: T;
1415

16+
/** The instance of the component making up the content of the snack bar. */
17+
readonly containerInstance: MdSnackBarContainer;
18+
1519
/** Subject for notifying the user that the snack bar has closed. */
1620
private _afterClosed: Subject<any> = new Subject();
1721

18-
constructor(instance: T, private _overlayRef: OverlayRef) {
22+
constructor(instance: T,
23+
containerInstance: MdSnackBarContainer,
24+
private _overlayRef: OverlayRef) {
1925
// Sets the readonly instance of the snack bar content component.
2026
this.instance = instance;
27+
this.containerInstance = containerInstance;
2128
}
2229

2330
/** Dismisses the snack bar. */
2431
dismiss(): void {
2532
if (!this._afterClosed.closed) {
26-
this._overlayRef.dispose();
27-
this._afterClosed.complete();
33+
this.containerInstance.exit().subscribe(() => {
34+
this._overlayRef.dispose();
35+
this._afterClosed.next();
36+
this._afterClosed.complete();
37+
});
2838
}
2939
}
3040

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

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {OverlayContainer} from '../core';
1616
import {MdSnackBarConfig} from './snack-bar-config';
1717
import {SimpleSnackBar} from './simple-snack-bar';
1818

19+
// TODO(josephperrott): Update tests to mock waiting for time to complete for animations.
1920

2021
describe('MdSnackBar', () => {
2122
let snackBar: MdSnackBar;
@@ -56,7 +57,6 @@ describe('MdSnackBar', () => {
5657
snackBar.open(simpleMessage, simpleActionLabel, config);
5758

5859
let containerElement = overlayContainerElement.querySelector('snack-bar-container');
59-
6060
expect(containerElement.getAttribute('role'))
6161
.toBe('alert', 'Expected snack bar container to have role="alert"');
6262
});
@@ -120,10 +120,11 @@ describe('MdSnackBar', () => {
120120
.toBeGreaterThan(0, 'Expected overlay container element to have at least one child');
121121

122122
snackBarRef.dismiss();
123-
124-
expect(dismissed).toBeTruthy('Expected the snack bar to be dismissed');
125-
expect(overlayContainerElement.childElementCount)
126-
.toBe(0, 'Expected the overlay container element to have no child elements');
123+
snackBarRef.afterDismissed().subscribe(null, null, () => {
124+
expect(dismissed).toBeTruthy('Expected the snack bar to be dismissed');
125+
expect(overlayContainerElement.childElementCount)
126+
.toBe(0, 'Expected the overlay container element to have no child elements');
127+
});
127128
});
128129

129130
it('should open a custom component', () => {
@@ -136,7 +137,46 @@ describe('MdSnackBar', () => {
136137
expect(overlayContainerElement.textContent)
137138
.toBe('Burritos are on the way.',
138139
`Expected the overlay text content to be 'Burritos are on the way'`);
140+
});
141+
142+
it('should set the animation state to visible on entry', () => {
143+
let config = new MdSnackBarConfig(testViewContainerRef);
144+
let snackBarRef = snackBar.open(simpleMessage, null, config);
145+
146+
viewContainerFixture.detectChanges();
147+
expect(snackBarRef.containerInstance.animationState)
148+
.toBe('visible', `Expected the animation state would be 'visible'.`);
149+
});
150+
151+
it('should set the animation state to complete on exit', () => {
152+
let config = new MdSnackBarConfig(testViewContainerRef);
153+
let snackBarRef = snackBar.open(simpleMessage, null, config);
154+
snackBarRef.dismiss();
139155

156+
viewContainerFixture.detectChanges();
157+
expect(snackBarRef.containerInstance.animationState)
158+
.toBe('complete', `Expected the animation state would be 'complete'.`);
159+
});
160+
161+
it(`should set the old snack bar animation state to complete and the new snack bar animation
162+
state to visible on entry of new snack bar`, () => {
163+
let config = new MdSnackBarConfig(testViewContainerRef);
164+
let snackBarRef = snackBar.open(simpleMessage, null, config);
165+
166+
viewContainerFixture.detectChanges();
167+
expect(snackBarRef.containerInstance.animationState)
168+
.toBe('visible', `Expected the animation state would be 'visible'.`);
169+
170+
let config2 = new MdSnackBarConfig(testViewContainerRef);
171+
let snackBarRef2 = snackBar.open(simpleMessage, null, config2);
172+
173+
viewContainerFixture.detectChanges();
174+
snackBarRef.afterDismissed().subscribe(null, null, () => {
175+
expect(snackBarRef.containerInstance.animationState)
176+
.toBe('complete', `Expected the animation state would be 'complete'.`);
177+
expect(snackBarRef2.containerInstance.animationState)
178+
.toBe('visible', `Expected the animation state would be 'visible'.`);
179+
});
140180
});
141181
});
142182

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {SimpleSnackBar} from './simple-snack-bar';
2424
export {MdSnackBarRef} from './snack-bar-ref';
2525
export {MdSnackBarConfig} from './snack-bar-config';
2626

27-
// TODO(josephperrott): Animate entrance and exit of snack bars.
2827
// TODO(josephperrott): Automate dismiss after timeout.
2928

3029

@@ -45,14 +44,24 @@ export class MdSnackBar {
4544
*/
4645
openFromComponent<T>(component: ComponentType<T>,
4746
config: MdSnackBarConfig): MdSnackBarRef<T> {
48-
if (this._snackBarRef) {
49-
this._snackBarRef.dismiss();
50-
}
5147
let overlayRef = this._createOverlay();
5248
let snackBarContainer = this._attachSnackBarContainer(overlayRef, config);
5349
let mdSnackBarRef = this._attachSnackbarContent(component, snackBarContainer, overlayRef);
50+
51+
// If a snack bar is already in view, dismiss it and enter the new snack bar after exit
52+
// animation is complete.
53+
if (this._snackBarRef) {
54+
this._snackBarRef.afterDismissed().subscribe(() => {
55+
mdSnackBarRef.containerInstance.enter();
56+
});
57+
this._snackBarRef.dismiss();
58+
// If no snack bar is in view, enter the new snack bar.
59+
} else {
60+
mdSnackBarRef.containerInstance.enter();
61+
}
5462
this._live.announce(config.announcementMessage, config.politeness);
55-
return mdSnackBarRef;
63+
this._snackBarRef = mdSnackBarRef;
64+
return this._snackBarRef;
5665
}
5766

5867
/**
@@ -88,10 +97,7 @@ export class MdSnackBar {
8897
overlayRef: OverlayRef): MdSnackBarRef<T> {
8998
let portal = new ComponentPortal(component);
9099
let contentRef = container.attachComponentPortal(portal);
91-
let snackBarRef = <MdSnackBarRef<T>> new MdSnackBarRef(contentRef.instance, overlayRef);
92-
93-
this._snackBarRef = snackBarRef;
94-
return snackBarRef;
100+
return new MdSnackBarRef(contentRef.instance, container, overlayRef);
95101
}
96102

97103
/**

src/lib/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@
3030
"skipTemplateCodegen": true,
3131
"debug": true
3232
}
33-
}
33+
}

0 commit comments

Comments
 (0)