Skip to content

Commit 91a3e3c

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

File tree

9 files changed

+155
-26
lines changed

9 files changed

+155
-26
lines changed

src/lib/core/animation/duration.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const animationDurations = {
2+
Complex: '375ms',
3+
Entering: '225ms',
4+
Exiting: '195ms',
5+
};

src/lib/core/animation/easing.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const animationCurves = {
2+
StandardCurve: 'cubic-bezier(0.4,0.0,0.2,1)',
3+
DecelerationCurve: 'cubic-bezier(0.0,0.0,0.2,1)',
4+
AccelerationCurve: 'cubic-bezier(0.4,0.0,1,1)',
5+
SharpCurve: 'cubic-bezier(0.4,0.0,0.6,1)',
6+
};

src/lib/core/core.ts

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

78+
// Animation
79+
export * from './animation/easing';
80+
export * from './animation/duration';
81+
7882

7983
@NgModule({
8084
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: 62 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 removed from view. */
60+
private _state: 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,27 @@ 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._state.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(() => this._state.complete());
97+
}
98+
}
99+
100+
/** Begin animation of snack bar entrance into view. */
101+
enter(): void {
102+
this.animationState = 'visible';
103+
}
47104
}

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

Lines changed: 12 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,27 @@ 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(null, null, () => {
34+
this._overlayRef.dispose();
35+
this._afterClosed.complete();
36+
});
2837
}
2938
}
3039

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: 17 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(null, null, () => {
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,9 @@ 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 <MdSnackBarRef<T>> new MdSnackBarRef(contentRef.instance,
101+
container,
102+
overlayRef);
95103
}
96104

97105
/**

src/lib/tsconfig.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@
1919
"../../node_modules/@types"
2020
],
2121
"types": [
22+
"jasmine"
2223
]
2324
},
24-
"exclude": [
25-
"**/*.spec.*",
26-
"system-config-spec.ts"
27-
],
25+
2826
"angularCompilerOptions": {
2927
"genDir": "../../dist/@angular/material",
3028
"skipTemplateCodegen": true,

0 commit comments

Comments
 (0)