mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(snack bar): Add enter and exit animations. (#1320)
* feat(md-snack-bar): Create initial MdSnackBar. * Add enter and exit animation to MdSnackBar. * feat(md-snack-bar): Create initial MdSnackBar. * feat(md-snack-bar): Create initial MdSnackBar. * Add enter and exit animation to MdSnackBar.
This commit is contained in:
committed by
Jeremy Elbourn
parent
3c1e8d2d28
commit
6df29dcd5b
@@ -0,0 +1,13 @@
|
||||
export class AnimationCurves {
|
||||
static STANDARD_CURVE = 'cubic-bezier(0.4,0.0,0.2,1)';
|
||||
static DECELERATION_CURVE = 'cubic-bezier(0.0,0.0,0.2,1)';
|
||||
static ACCELERATION_CURVE = 'cubic-bezier(0.4,0.0,1,1)';
|
||||
static SHARP_CURVE = 'cubic-bezier(0.4,0.0,0.6,1)';
|
||||
};
|
||||
|
||||
|
||||
export class AnimationDurations {
|
||||
static COMPLEX = '375ms';
|
||||
static ENTERING = '225ms';
|
||||
static EXITING = '195ms';
|
||||
};
|
||||
@@ -79,6 +79,9 @@ export {ComponentType} from './overlay/generic-component-type';
|
||||
// Keybindings
|
||||
export * from './keyboard/keycodes';
|
||||
|
||||
// Animation
|
||||
export * from './animation/animation';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule, A11yModule],
|
||||
|
||||
@@ -10,11 +10,13 @@ $md-snack-bar-max-width: 568px !default;
|
||||
@include md-elevation(24);
|
||||
background: #323232;
|
||||
border-radius: 2px;
|
||||
box-sizing: content-box;
|
||||
display: block;
|
||||
height: $md-snack-bar-height;
|
||||
max-width: $md-snack-bar-max-width;
|
||||
min-width: $md-snack-bar-min-width;
|
||||
overflow: hidden;
|
||||
padding: $md-snack-bar-padding;
|
||||
box-sizing: content-box;
|
||||
// Initial transformation is applied to start snack bar out of view.
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentRef,
|
||||
ViewChild
|
||||
ViewChild,
|
||||
trigger,
|
||||
state,
|
||||
style,
|
||||
transition,
|
||||
animate,
|
||||
AnimationTransitionEvent,
|
||||
NgZone
|
||||
} from '@angular/core';
|
||||
import {
|
||||
BasePortalHost,
|
||||
ComponentPortal,
|
||||
TemplatePortal,
|
||||
PortalHostDirective
|
||||
PortalHostDirective,
|
||||
AnimationCurves,
|
||||
AnimationDurations,
|
||||
} from '../core';
|
||||
import {MdSnackBarConfig} from './snack-bar-config';
|
||||
import {MdSnackBarContentAlreadyAttached} from './snack-bar-errors';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
|
||||
|
||||
export type SnackBarState = 'initial' | 'visible' | 'complete' | 'void';
|
||||
|
||||
/**
|
||||
* Internal component that wraps user-provided snack bar content.
|
||||
*/
|
||||
@@ -22,17 +36,40 @@ import {MdSnackBarContentAlreadyAttached} from './snack-bar-errors';
|
||||
templateUrl: 'snack-bar-container.html',
|
||||
styleUrls: ['snack-bar-container.css'],
|
||||
host: {
|
||||
'role': 'alert'
|
||||
}
|
||||
'role': 'alert',
|
||||
'[@state]': 'animationState',
|
||||
'(@state.done)': 'markAsExited($event)'
|
||||
},
|
||||
animations: [
|
||||
trigger('state', [
|
||||
state('initial', style({transform: 'translateY(100%)'})),
|
||||
state('visible', style({transform: 'translateY(0%)'})),
|
||||
state('complete', style({transform: 'translateY(100%)'})),
|
||||
transition('visible => complete',
|
||||
animate(`${AnimationDurations.EXITING} ${AnimationCurves.DECELERATION_CURVE}`)),
|
||||
transition('initial => visible, void => visible',
|
||||
animate(`${AnimationDurations.ENTERING} ${AnimationCurves.ACCELERATION_CURVE}`)),
|
||||
])
|
||||
],
|
||||
})
|
||||
export class MdSnackBarContainer extends BasePortalHost {
|
||||
/** The portal host inside of this container into which the snack bar content will be loaded. */
|
||||
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
|
||||
|
||||
/** Subject for notifying that the snack bar has exited from view. */
|
||||
private _onExit: Subject<any> = new Subject();
|
||||
|
||||
/** The state of the snack bar animations. */
|
||||
animationState: SnackBarState = 'initial';
|
||||
|
||||
/** The snack bar configuration. */
|
||||
snackBarConfig: MdSnackBarConfig;
|
||||
|
||||
/** Attach a portal as content to this snack bar container. */
|
||||
constructor(private _ngZone: NgZone) {
|
||||
super();
|
||||
}
|
||||
|
||||
/** Attach a component portal as content to this snack bar container. */
|
||||
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
|
||||
if (this._portalHost.hasAttached()) {
|
||||
throw new MdSnackBarContentAlreadyAttached();
|
||||
@@ -41,7 +78,30 @@ export class MdSnackBarContainer extends BasePortalHost {
|
||||
return this._portalHost.attachComponentPortal(portal);
|
||||
}
|
||||
|
||||
/** Attach a template portal as content to this snack bar container. */
|
||||
attachTemplatePortal(portal: TemplatePortal): Map<string, any> {
|
||||
throw Error('Not yet implemented');
|
||||
}
|
||||
|
||||
/** Begin animation of the snack bar exiting from view. */
|
||||
exit(): Observable<void> {
|
||||
this.animationState = 'complete';
|
||||
return this._onExit.asObservable();
|
||||
}
|
||||
|
||||
/** Mark snack bar as exited from the view. */
|
||||
markAsExited(event: AnimationTransitionEvent) {
|
||||
if (event.fromState === 'visible' &&
|
||||
(event.toState === 'void' || event.toState === 'complete')) {
|
||||
this._ngZone.run(() => {
|
||||
this._onExit.next();
|
||||
this._onExit.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Begin animation of snack bar entrance into view. */
|
||||
enter(): void {
|
||||
this.animationState = 'visible';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {OverlayRef} from '../core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {MdSnackBarContainer} from './snack-bar-container';
|
||||
|
||||
// TODO(josephperrott): Implement onAction observable.
|
||||
|
||||
@@ -12,19 +13,28 @@ export class MdSnackBarRef<T> {
|
||||
/** The instance of the component making up the content of the snack bar. */
|
||||
readonly instance: T;
|
||||
|
||||
/** The instance of the component making up the content of the snack bar. */
|
||||
readonly containerInstance: MdSnackBarContainer;
|
||||
|
||||
/** Subject for notifying the user that the snack bar has closed. */
|
||||
private _afterClosed: Subject<any> = new Subject();
|
||||
|
||||
constructor(instance: T, private _overlayRef: OverlayRef) {
|
||||
constructor(instance: T,
|
||||
containerInstance: MdSnackBarContainer,
|
||||
private _overlayRef: OverlayRef) {
|
||||
// Sets the readonly instance of the snack bar content component.
|
||||
this.instance = instance;
|
||||
this.containerInstance = containerInstance;
|
||||
}
|
||||
|
||||
/** Dismisses the snack bar. */
|
||||
dismiss(): void {
|
||||
if (!this._afterClosed.closed) {
|
||||
this._overlayRef.dispose();
|
||||
this._afterClosed.complete();
|
||||
this.containerInstance.exit().subscribe(() => {
|
||||
this._overlayRef.dispose();
|
||||
this._afterClosed.next();
|
||||
this._afterClosed.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {OverlayContainer} from '../core';
|
||||
import {MdSnackBarConfig} from './snack-bar-config';
|
||||
import {SimpleSnackBar} from './simple-snack-bar';
|
||||
|
||||
// TODO(josephperrott): Update tests to mock waiting for time to complete for animations.
|
||||
|
||||
describe('MdSnackBar', () => {
|
||||
let snackBar: MdSnackBar;
|
||||
@@ -56,7 +57,6 @@ describe('MdSnackBar', () => {
|
||||
snackBar.open(simpleMessage, simpleActionLabel, config);
|
||||
|
||||
let containerElement = overlayContainerElement.querySelector('snack-bar-container');
|
||||
|
||||
expect(containerElement.getAttribute('role'))
|
||||
.toBe('alert', 'Expected snack bar container to have role="alert"');
|
||||
});
|
||||
@@ -102,7 +102,6 @@ describe('MdSnackBar', () => {
|
||||
expect(messageElement.tagName).toBe('SPAN', 'Expected snack bar message element to be <span>');
|
||||
expect(messageElement.textContent)
|
||||
.toBe(simpleMessage, `Expected the snack bar message to be '${simpleMessage}''`);
|
||||
|
||||
expect(overlayContainerElement.querySelector('button.md-simple-snackbar-action'))
|
||||
.toBeNull('Expected the query selection for action label to be null');
|
||||
});
|
||||
@@ -120,10 +119,11 @@ describe('MdSnackBar', () => {
|
||||
.toBeGreaterThan(0, 'Expected overlay container element to have at least one child');
|
||||
|
||||
snackBarRef.dismiss();
|
||||
|
||||
expect(dismissed).toBeTruthy('Expected the snack bar to be dismissed');
|
||||
expect(overlayContainerElement.childElementCount)
|
||||
.toBe(0, 'Expected the overlay container element to have no child elements');
|
||||
snackBarRef.afterDismissed().subscribe(null, null, () => {
|
||||
expect(dismissed).toBeTruthy('Expected the snack bar to be dismissed');
|
||||
expect(overlayContainerElement.childElementCount)
|
||||
.toBe(0, 'Expected the overlay container element to have no child elements');
|
||||
});
|
||||
});
|
||||
|
||||
it('should open a custom component', () => {
|
||||
@@ -136,7 +136,46 @@ describe('MdSnackBar', () => {
|
||||
expect(overlayContainerElement.textContent)
|
||||
.toBe('Burritos are on the way.',
|
||||
`Expected the overlay text content to be 'Burritos are on the way'`);
|
||||
});
|
||||
|
||||
it('should set the animation state to visible on entry', () => {
|
||||
let config = new MdSnackBarConfig(testViewContainerRef);
|
||||
let snackBarRef = snackBar.open(simpleMessage, null, config);
|
||||
|
||||
viewContainerFixture.detectChanges();
|
||||
expect(snackBarRef.containerInstance.animationState)
|
||||
.toBe('visible', `Expected the animation state would be 'visible'.`);
|
||||
});
|
||||
|
||||
it('should set the animation state to complete on exit', () => {
|
||||
let config = new MdSnackBarConfig(testViewContainerRef);
|
||||
let snackBarRef = snackBar.open(simpleMessage, null, config);
|
||||
snackBarRef.dismiss();
|
||||
|
||||
viewContainerFixture.detectChanges();
|
||||
expect(snackBarRef.containerInstance.animationState)
|
||||
.toBe('complete', `Expected the animation state would be 'complete'.`);
|
||||
});
|
||||
|
||||
it(`should set the old snack bar animation state to complete and the new snack bar animation
|
||||
state to visible on entry of new snack bar`, () => {
|
||||
let config = new MdSnackBarConfig(testViewContainerRef);
|
||||
let snackBarRef = snackBar.open(simpleMessage, null, config);
|
||||
|
||||
viewContainerFixture.detectChanges();
|
||||
expect(snackBarRef.containerInstance.animationState)
|
||||
.toBe('visible', `Expected the animation state would be 'visible'.`);
|
||||
|
||||
let config2 = new MdSnackBarConfig(testViewContainerRef);
|
||||
let snackBarRef2 = snackBar.open(simpleMessage, null, config2);
|
||||
|
||||
viewContainerFixture.detectChanges();
|
||||
snackBarRef.afterDismissed().subscribe(null, null, () => {
|
||||
expect(snackBarRef.containerInstance.animationState)
|
||||
.toBe('complete', `Expected the animation state would be 'complete'.`);
|
||||
expect(snackBarRef2.containerInstance.animationState)
|
||||
.toBe('visible', `Expected the animation state would be 'visible'.`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -21,10 +21,6 @@ import {MdSnackBarRef} from './snack-bar-ref';
|
||||
import {MdSnackBarContainer} from './snack-bar-container';
|
||||
import {SimpleSnackBar} from './simple-snack-bar';
|
||||
|
||||
export {MdSnackBarRef} from './snack-bar-ref';
|
||||
export {MdSnackBarConfig} from './snack-bar-config';
|
||||
|
||||
// TODO(josephperrott): Animate entrance and exit of snack bars.
|
||||
// TODO(josephperrott): Automate dismiss after timeout.
|
||||
|
||||
|
||||
@@ -45,14 +41,24 @@ export class MdSnackBar {
|
||||
*/
|
||||
openFromComponent<T>(component: ComponentType<T>,
|
||||
config: MdSnackBarConfig): MdSnackBarRef<T> {
|
||||
if (this._snackBarRef) {
|
||||
this._snackBarRef.dismiss();
|
||||
}
|
||||
let overlayRef = this._createOverlay();
|
||||
let snackBarContainer = this._attachSnackBarContainer(overlayRef, config);
|
||||
let mdSnackBarRef = this._attachSnackbarContent(component, snackBarContainer, overlayRef);
|
||||
|
||||
// If a snack bar is already in view, dismiss it and enter the new snack bar after exit
|
||||
// animation is complete.
|
||||
if (this._snackBarRef) {
|
||||
this._snackBarRef.afterDismissed().subscribe(() => {
|
||||
mdSnackBarRef.containerInstance.enter();
|
||||
});
|
||||
this._snackBarRef.dismiss();
|
||||
// If no snack bar is in view, enter the new snack bar.
|
||||
} else {
|
||||
mdSnackBarRef.containerInstance.enter();
|
||||
}
|
||||
this._live.announce(config.announcementMessage, config.politeness);
|
||||
return mdSnackBarRef;
|
||||
this._snackBarRef = mdSnackBarRef;
|
||||
return this._snackBarRef;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,10 +94,7 @@ export class MdSnackBar {
|
||||
overlayRef: OverlayRef): MdSnackBarRef<T> {
|
||||
let portal = new ComponentPortal(component);
|
||||
let contentRef = container.attachComponentPortal(portal);
|
||||
let snackBarRef = <MdSnackBarRef<T>> new MdSnackBarRef(contentRef.instance, overlayRef);
|
||||
|
||||
this._snackBarRef = snackBarRef;
|
||||
return snackBarRef;
|
||||
return new MdSnackBarRef(contentRef.instance, container, overlayRef);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user