From b8e69a21a904dad8d5855afc2788491aa73d0460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 18 Nov 2015 16:45:52 -0800 Subject: [PATCH] fix(animate): ensure transition properties are removed once the animation is over --- modules/angular2/src/animate/animation.ts | 52 +++++-- .../src/animate/css_animation_options.ts | 3 + .../angular2/test/animate/animation_spec.ts | 133 ++++++++++++++++++ 3 files changed, 180 insertions(+), 8 deletions(-) create mode 100644 modules/angular2/test/animate/animation_spec.ts diff --git a/modules/angular2/src/animate/animation.ts b/modules/angular2/src/animate/animation.ts index 4e73758989..3cc42c3432 100644 --- a/modules/angular2/src/animate/animation.ts +++ b/modules/angular2/src/animate/animation.ts @@ -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()); diff --git a/modules/angular2/src/animate/css_animation_options.ts b/modules/angular2/src/animate/css_animation_options.ts index eab07b893a..cb1cccabd3 100644 --- a/modules/angular2/src/animate/css_animation_options.ts +++ b/modules/angular2/src/animate/css_animation_options.ts @@ -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; diff --git a/modules/angular2/test/animate/animation_spec.ts b/modules/angular2/test/animate/animation_spec.ts new file mode 100644 index 0000000000..65fb39f9b1 --- /dev/null +++ b/modules/angular2/test/animate/animation_spec.ts @@ -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('
'); }); + + 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'}); + }); + }); + }); +}