fix(animate): ensure transition properties are removed once the animation is over

This commit is contained in:
Matias Niemelä 2015-11-18 16:45:52 -08:00 committed by vsavkin
parent 3fd898e91f
commit b8e69a21a9
3 changed files with 180 additions and 8 deletions

View File

@ -34,6 +34,8 @@ export class Animation {
private _stringPrefix: string = '';
private _temporaryStyles: {[key: string]: string} = {};
/** total amount of time that the animation should take including delay */
get totalTime(): number {
let delay = this.computedDelay != null ? this.computedDelay : 0;
@ -65,10 +67,25 @@ export class Animation {
*/
setup(): void {
if (this.data.fromStyles != null) this.applyStyles(this.data.fromStyles);
if (this.data.duration != null)
if (this.data.duration != null) {
this._temporaryStyles['transitionDuration'] = this._readStyle('transitionDuration');
this.applyStyles({'transitionDuration': this.data.duration.toString() + 'ms'});
if (this.data.delay != null)
}
if (this.data.delay != null) {
this._temporaryStyles['transitionDelay'] = this._readStyle('transitionDelay');
this.applyStyles({'transitionDelay': this.data.delay.toString() + 'ms'});
}
if (!StringMapWrapper.isEmpty(this.data.animationStyles)) {
// it's important that we setup a list of the styles and their
// initial inline style values prior to applying the animation
// styles such that we can restore the values after the animation
// has been completed.
StringMapWrapper.keys(this.data.animationStyles)
.forEach((prop) => { this._temporaryStyles[prop] = this._readStyle(prop); });
this.applyStyles(this.data.animationStyles);
}
}
/**
@ -98,12 +115,8 @@ export class Animation {
*/
applyStyles(styles: {[key: string]: any}): void {
StringMapWrapper.forEach(styles, (value, key) => {
var dashCaseKey = camelCaseToDashCase(key);
if (isPresent(DOM.getStyle(this.element, dashCaseKey))) {
DOM.setStyle(this.element, dashCaseKey, value.toString());
} else {
DOM.setStyle(this.element, this._stringPrefix + dashCaseKey, value.toString());
}
var prop = this._formatStyleProp(key);
DOM.setStyle(this.element, prop, value.toString());
});
}
@ -123,6 +136,26 @@ export class Animation {
for (let i = 0, len = classes.length; i < len; i++) DOM.removeClass(this.element, classes[i]);
}
private _readStyle(prop: string): string {
return DOM.getStyle(this.element, this._formatStyleProp(prop));
}
private _formatStyleProp(prop: string): string {
prop = camelCaseToDashCase(prop);
return prop.indexOf('animation') >= 0 ? this._stringPrefix + prop : prop;
}
private _removeAndRestoreStyles(styles: {[key: string]: string}): void {
StringMapWrapper.forEach(styles, (value, prop) => {
prop = this._formatStyleProp(prop);
if (value.length > 0) {
DOM.setStyle(this.element, prop, value);
} else {
DOM.removeStyle(this.element, prop);
}
});
}
/**
* Adds events to track when animations have finished
*/
@ -147,6 +180,9 @@ export class Animation {
*/
handleAnimationCompleted(): void {
this.removeClasses(this.data.animationClasses);
this._removeAndRestoreStyles(this._temporaryStyles);
this._temporaryStyles = {};
this.callbacks.forEach(callback => callback());
this.callbacks = [];
this.eventClearFunctions.forEach(fn => fn());

View File

@ -14,6 +14,9 @@ export class CssAnimationOptions {
/** classes to be added for the duration of the animation */
animationClasses: string[] = [];
/** styles to be applied for the duration of the animation */
animationStyles: {[key: string]: string} = {};
/** override the duration of the animation (in milliseconds) */
duration: number;

View File

@ -0,0 +1,133 @@
import {
el,
describe,
ddescribe,
beforeEach,
it,
iit,
expect,
inject,
SpyObject
} from 'angular2/testing_internal';
import {CssAnimationOptions} from 'angular2/src/animate/css_animation_options';
import {Animation} from 'angular2/src/animate/animation';
import {BrowserDetails} from 'angular2/src/animate/browser_details';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
export function main() {
describe("Animation", () => {
var element;
beforeEach(() => { element = el('<div></div>'); });
describe('transition-duration', () => {
it('should only be applied for the duration of the animation', () => {
var data = new CssAnimationOptions();
data.duration = 1000;
expect(element).not.toHaveCssStyle('transition-duration');
new Animation(element, data, new BrowserDetails());
expect(element).toHaveCssStyle({'transition-duration': '1000ms'});
});
it('should be removed once the animation is over', () => {
var data = new CssAnimationOptions();
data.duration = 1000;
var animation = new Animation(element, data, new BrowserDetails());
expect(element).toHaveCssStyle({'transition-duration': '1000ms'});
animation.handleAnimationCompleted();
expect(element).not.toHaveCssStyle('transition-duration');
});
it('should be restore the pre-existing transition-duration once the animation is over if present',
() => {
DOM.setStyle(element, 'transition-duration', '5s');
expect(element).toHaveCssStyle({'transition-duration': '5s'});
var data = new CssAnimationOptions();
data.duration = 1000;
var animation = new Animation(element, data, new BrowserDetails());
expect(element).toHaveCssStyle({'transition-duration': '1000ms'});
animation.handleAnimationCompleted();
expect(element).toHaveCssStyle({'transition-duration': '5s'});
});
});
describe('transition-delay', () => {
it('should only be applied for the delay of the animation', () => {
var data = new CssAnimationOptions();
data.delay = 1000;
expect(element).not.toHaveCssStyle('transition-delay');
new Animation(element, data, new BrowserDetails());
expect(element).toHaveCssStyle({'transition-delay': '1000ms'});
});
it('should be removed once the animation is over', () => {
var data = new CssAnimationOptions();
data.delay = 1000;
var animation = new Animation(element, data, new BrowserDetails());
expect(element).toHaveCssStyle({'transition-delay': '1000ms'});
animation.handleAnimationCompleted();
expect(element).not.toHaveCssStyle('transition-delay');
});
it('should be restore the pre-existing transition-delay once the animation is over if present',
() => {
DOM.setStyle(element, 'transition-delay', '5s');
expect(element).toHaveCssStyle({'transition-delay': '5s'});
var data = new CssAnimationOptions();
data.delay = 1000;
var animation = new Animation(element, data, new BrowserDetails());
expect(element).toHaveCssStyle({'transition-delay': '1000ms'});
animation.handleAnimationCompleted();
expect(element).toHaveCssStyle({'transition-delay': '5s'});
});
});
describe('temporary animation styles', () => {
it('should be applied temporarily for the duration of the animation', () => {
var data = new CssAnimationOptions();
data.duration = 1000;
data.animationStyles = {'width': '100px', 'opacity': '0.5'};
var animation = new Animation(element, data, new BrowserDetails());
expect(element)
.toHaveCssStyle({'opacity': '0.5', 'width': '100px', 'transition-duration': '1000ms'});
animation.handleAnimationCompleted();
expect(element).not.toHaveCssStyle('width');
expect(element).not.toHaveCssStyle('opacity');
expect(element).not.toHaveCssStyle('transition-duration');
});
it('should be restored back to the original styles on the element', () => {
DOM.setStyle(element, 'height', '555px');
var data = new CssAnimationOptions();
data.duration = 1000;
data.animationStyles = {'width': '100px', 'height': '999px'};
var animation = new Animation(element, data, new BrowserDetails());
expect(element).toHaveCssStyle({'width': '100px', 'height': '999px'});
animation.handleAnimationCompleted();
expect(element).not.toHaveCssStyle('width');
expect(element).toHaveCssStyle({'height': '555px'});
});
});
});
}