From 7d5d62dafe11620082c79da35958f8014eeb008c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Thu, 2 Jan 2014 17:27:13 -0500 Subject: [PATCH] fix($animate): correctly detect and handle CSS transition changes during class addition and removal When a CSS class containing transition code is added to an element then an animation should kick off. ngAnimate doesn't do this. It only respects transition styles that are already present on the element or on the setup class (but not the addClass animation). --- src/ngAnimate/animate.js | 41 ++++++++++++++++++--- test/ngAnimate/animateSpec.js | 69 ++++++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 62f6381dcda5..26fe982d6b2a 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -1043,7 +1043,7 @@ angular.module('ngAnimate', ['ng']) return parentID + '-' + extractElementNode(element).className; } - function animateSetup(element, className) { + function animateSetup(element, className, calculationDecorator) { var cacheKey = getCacheKey(element); var eventCacheKey = cacheKey + ' ' + className; var stagger = {}; @@ -1061,9 +1061,16 @@ angular.module('ngAnimate', ['ng']) applyClasses && element.removeClass(staggerClassName); } + /* the animation itself may need to add/remove special CSS classes + * before calculating the anmation styles */ + calculationDecorator = calculationDecorator || + function(fn) { return fn(); }; + element.addClass(className); - var timings = getElementAnimationDetails(element, eventCacheKey); + var timings = calculationDecorator(function() { + return getElementAnimationDetails(element, eventCacheKey); + }); /* there is no point in performing a reflow if the animation timeout is empty (this would cause a flicker bug normally @@ -1228,8 +1235,8 @@ angular.module('ngAnimate', ['ng']) return style; } - function animateBefore(element, className) { - if(animateSetup(element, className)) { + function animateBefore(element, className, calculationDecorator) { + if(animateSetup(element, className, calculationDecorator)) { return function(cancelled) { cancelled && animateClose(element, className); }; @@ -1324,7 +1331,18 @@ angular.module('ngAnimate', ['ng']) }, beforeAddClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore(element, suffixClasses(className, '-add')); + var cancellationMethod = animateBefore(element, suffixClasses(className, '-add'), function(fn) { + + /* when a CSS class is added to an element then the transition style that + * is applied is the transition defined on the element when the CSS class + * is added at the time of the animation. This is how CSS3 functions + * outside of ngAnimate. */ + element.addClass(className); + var timings = fn(); + element.removeClass(className); + return timings; + }); + if(cancellationMethod) { afterReflow(element, function() { unblockTransitions(element); @@ -1341,7 +1359,18 @@ angular.module('ngAnimate', ['ng']) }, beforeRemoveClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore(element, suffixClasses(className, '-remove')); + var cancellationMethod = animateBefore(element, suffixClasses(className, '-remove'), function(fn) { + /* when classes are removed from an element then the transition style + * that is applied is the transition defined on the element without the + * CSS class being there. This is how CSS3 functions outside of ngAnimate. + * http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */ + var klass = element.attr('class'); + element.removeClass(className); + var timings = fn(); + element.attr('class', klass); + return timings; + }); + if(cancellationMethod) { afterReflow(element, function() { unblockTransitions(element); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index db40d5449ef6..99527cc4aa90 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -2801,14 +2801,14 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'base-class one two'); //still true since we're before the reflow - expect(element.hasClass('base-class')).toBe(true); + expect(element.hasClass('base-class')).toBe(false); //this will cancel the remove animation $animate.addClass(element, 'base-class one two'); //the cancellation was a success and the class was added right away //since there was no successive animation for the after animation - expect(element.hasClass('base-class')).toBe(true); + expect(element.hasClass('base-class')).toBe(false); //the reflow... $timeout.flush(); @@ -3048,5 +3048,70 @@ describe("ngAnimate", function() { expect(leaveDone).toBe(true); }); }); + + it('should respect the most relevant CSS transition property if defined in multiple classes', + inject(function($sniffer, $compile, $rootScope, $rootElement, $animate, $timeout) { + + if (!$sniffer.transitions) return; + + ss.addRule('.base-class', '-webkit-transition:1s linear all;' + + 'transition:1s linear all;'); + + ss.addRule('.base-class.on', '-webkit-transition:5s linear all;' + + 'transition:5s linear all;'); + + $animate.enabled(true); + + var element = $compile('
')($rootScope); + $rootElement.append(element); + jqLite($document[0].body).append($rootElement); + + var ready = false; + $animate.addClass(element, 'on', function() { + ready = true; + }); + + $timeout.flush(10); + browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 1 }); + $timeout.flush(1); + expect(ready).toBe(false); + + browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 5 }); + $timeout.flush(1); + expect(ready).toBe(true); + + ready = false; + $animate.removeClass(element, 'on', function() { + ready = true; + }); + + $timeout.flush(10); + browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 1 }); + $timeout.flush(1); + expect(ready).toBe(true); + })); + + it('should not apply a transition upon removal of a class that has a transition', + inject(function($sniffer, $compile, $rootScope, $rootElement, $animate, $timeout) { + + if (!$sniffer.transitions) return; + + ss.addRule('.base-class.on', '-webkit-transition:5s linear all;' + + 'transition:5s linear all;'); + + $animate.enabled(true); + + var element = $compile('
')($rootScope); + $rootElement.append(element); + jqLite($document[0].body).append($rootElement); + + var ready = false; + $animate.removeClass(element, 'on', function() { + ready = true; + }); + + $timeout.flush(1); + expect(ready).toBe(true); + })); }); });