Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 7d5d62d

Browse files
committed
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).
1 parent 524650a commit 7d5d62d

File tree

2 files changed

+102
-8
lines changed

2 files changed

+102
-8
lines changed

src/ngAnimate/animate.js

+35-6
Original file line numberDiff line numberDiff line change
@@ -1043,7 +1043,7 @@ angular.module('ngAnimate', ['ng'])
10431043
return parentID + '-' + extractElementNode(element).className;
10441044
}
10451045

1046-
function animateSetup(element, className) {
1046+
function animateSetup(element, className, calculationDecorator) {
10471047
var cacheKey = getCacheKey(element);
10481048
var eventCacheKey = cacheKey + ' ' + className;
10491049
var stagger = {};
@@ -1061,9 +1061,16 @@ angular.module('ngAnimate', ['ng'])
10611061
applyClasses && element.removeClass(staggerClassName);
10621062
}
10631063

1064+
/* the animation itself may need to add/remove special CSS classes
1065+
* before calculating the anmation styles */
1066+
calculationDecorator = calculationDecorator ||
1067+
function(fn) { return fn(); };
1068+
10641069
element.addClass(className);
10651070

1066-
var timings = getElementAnimationDetails(element, eventCacheKey);
1071+
var timings = calculationDecorator(function() {
1072+
return getElementAnimationDetails(element, eventCacheKey);
1073+
});
10671074

10681075
/* there is no point in performing a reflow if the animation
10691076
timeout is empty (this would cause a flicker bug normally
@@ -1228,8 +1235,8 @@ angular.module('ngAnimate', ['ng'])
12281235
return style;
12291236
}
12301237

1231-
function animateBefore(element, className) {
1232-
if(animateSetup(element, className)) {
1238+
function animateBefore(element, className, calculationDecorator) {
1239+
if(animateSetup(element, className, calculationDecorator)) {
12331240
return function(cancelled) {
12341241
cancelled && animateClose(element, className);
12351242
};
@@ -1324,7 +1331,18 @@ angular.module('ngAnimate', ['ng'])
13241331
},
13251332

13261333
beforeAddClass : function(element, className, animationCompleted) {
1327-
var cancellationMethod = animateBefore(element, suffixClasses(className, '-add'));
1334+
var cancellationMethod = animateBefore(element, suffixClasses(className, '-add'), function(fn) {
1335+
1336+
/* when a CSS class is added to an element then the transition style that
1337+
* is applied is the transition defined on the element when the CSS class
1338+
* is added at the time of the animation. This is how CSS3 functions
1339+
* outside of ngAnimate. */
1340+
element.addClass(className);
1341+
var timings = fn();
1342+
element.removeClass(className);
1343+
return timings;
1344+
});
1345+
13281346
if(cancellationMethod) {
13291347
afterReflow(element, function() {
13301348
unblockTransitions(element);
@@ -1341,7 +1359,18 @@ angular.module('ngAnimate', ['ng'])
13411359
},
13421360

13431361
beforeRemoveClass : function(element, className, animationCompleted) {
1344-
var cancellationMethod = animateBefore(element, suffixClasses(className, '-remove'));
1362+
var cancellationMethod = animateBefore(element, suffixClasses(className, '-remove'), function(fn) {
1363+
/* when classes are removed from an element then the transition style
1364+
* that is applied is the transition defined on the element without the
1365+
* CSS class being there. This is how CSS3 functions outside of ngAnimate.
1366+
* http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */
1367+
var klass = element.attr('class');
1368+
element.removeClass(className);
1369+
var timings = fn();
1370+
element.attr('class', klass);
1371+
return timings;
1372+
});
1373+
13451374
if(cancellationMethod) {
13461375
afterReflow(element, function() {
13471376
unblockTransitions(element);

test/ngAnimate/animateSpec.js

+67-2
Original file line numberDiff line numberDiff line change
@@ -2801,14 +2801,14 @@ describe("ngAnimate", function() {
28012801
$animate.removeClass(element, 'base-class one two');
28022802

28032803
//still true since we're before the reflow
2804-
expect(element.hasClass('base-class')).toBe(true);
2804+
expect(element.hasClass('base-class')).toBe(false);
28052805

28062806
//this will cancel the remove animation
28072807
$animate.addClass(element, 'base-class one two');
28082808

28092809
//the cancellation was a success and the class was added right away
28102810
//since there was no successive animation for the after animation
2811-
expect(element.hasClass('base-class')).toBe(true);
2811+
expect(element.hasClass('base-class')).toBe(false);
28122812

28132813
//the reflow...
28142814
$timeout.flush();
@@ -3048,5 +3048,70 @@ describe("ngAnimate", function() {
30483048
expect(leaveDone).toBe(true);
30493049
});
30503050
});
3051+
3052+
it('should respect the most relevant CSS transition property if defined in multiple classes',
3053+
inject(function($sniffer, $compile, $rootScope, $rootElement, $animate, $timeout) {
3054+
3055+
if (!$sniffer.transitions) return;
3056+
3057+
ss.addRule('.base-class', '-webkit-transition:1s linear all;' +
3058+
'transition:1s linear all;');
3059+
3060+
ss.addRule('.base-class.on', '-webkit-transition:5s linear all;' +
3061+
'transition:5s linear all;');
3062+
3063+
$animate.enabled(true);
3064+
3065+
var element = $compile('<div class="base-class"></div>')($rootScope);
3066+
$rootElement.append(element);
3067+
jqLite($document[0].body).append($rootElement);
3068+
3069+
var ready = false;
3070+
$animate.addClass(element, 'on', function() {
3071+
ready = true;
3072+
});
3073+
3074+
$timeout.flush(10);
3075+
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 1 });
3076+
$timeout.flush(1);
3077+
expect(ready).toBe(false);
3078+
3079+
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 5 });
3080+
$timeout.flush(1);
3081+
expect(ready).toBe(true);
3082+
3083+
ready = false;
3084+
$animate.removeClass(element, 'on', function() {
3085+
ready = true;
3086+
});
3087+
3088+
$timeout.flush(10);
3089+
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 1 });
3090+
$timeout.flush(1);
3091+
expect(ready).toBe(true);
3092+
}));
3093+
3094+
it('should not apply a transition upon removal of a class that has a transition',
3095+
inject(function($sniffer, $compile, $rootScope, $rootElement, $animate, $timeout) {
3096+
3097+
if (!$sniffer.transitions) return;
3098+
3099+
ss.addRule('.base-class.on', '-webkit-transition:5s linear all;' +
3100+
'transition:5s linear all;');
3101+
3102+
$animate.enabled(true);
3103+
3104+
var element = $compile('<div class="base-class on"></div>')($rootScope);
3105+
$rootElement.append(element);
3106+
jqLite($document[0].body).append($rootElement);
3107+
3108+
var ready = false;
3109+
$animate.removeClass(element, 'on', function() {
3110+
ready = true;
3111+
});
3112+
3113+
$timeout.flush(1);
3114+
expect(ready).toBe(true);
3115+
}));
30513116
});
30523117
});

0 commit comments

Comments
 (0)