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

Commit 4aa9df7

Browse files
committed
fix($animate): prevent race conditions for class-based animations when animating on the same CSS class
Closes #5588
1 parent 7d5d62d commit 4aa9df7

File tree

2 files changed

+70
-5
lines changed

2 files changed

+70
-5
lines changed

src/ngAnimate/animate.js

+15-4
Original file line numberDiff line numberDiff line change
@@ -659,12 +659,23 @@ angular.module('ngAnimate', ['ng'])
659659
cleanup(element);
660660
cancelAnimations(ngAnimateState.animations);
661661

662+
//in the event that the CSS is class is quickly added and removed back
663+
//then we don't want to wait until after the reflow to add/remove the CSS
664+
//class since both class animations may run into a race condition.
665+
//The code below will check to see if that is occurring and will
666+
//immediately remove the former class before the reflow so that the
667+
//animation can snap back to the original animation smoothly
668+
var isFullyClassBasedAnimation = isClassBased && !ngAnimateState.structural;
669+
var isRevertingClassAnimation = isFullyClassBasedAnimation &&
670+
ngAnimateState.className == className &&
671+
animationEvent != ngAnimateState.event;
672+
662673
//if the class is removed during the reflow then it will revert the styles temporarily
663674
//back to the base class CSS styling causing a jump-like effect to occur. This check
664675
//here ensures that the domOperation is only performed after the reflow has commenced
665-
if(ngAnimateState.beforeComplete) {
676+
if(ngAnimateState.beforeComplete || isRevertingClassAnimation) {
666677
(ngAnimateState.done || noop)(true);
667-
} else if(isClassBased && !ngAnimateState.structural) {
678+
} else if(isFullyClassBasedAnimation) {
668679
//class-based animations will compare element className values after cancelling the
669680
//previous animation to see if the element properties already contain the final CSS
670681
//class and if so then the animation will be skipped. Since the domOperation will
@@ -812,10 +823,10 @@ angular.module('ngAnimate', ['ng'])
812823
function cancelAnimations(animations) {
813824
var isCancelledFlag = true;
814825
forEach(animations, function(animation) {
815-
if(!animations.beforeComplete) {
826+
if(!animation.beforeComplete) {
816827
(animation.beforeEnd || noop)(isCancelledFlag);
817828
}
818-
if(!animations.afterComplete) {
829+
if(!animation.afterComplete) {
819830
(animation.afterEnd || noop)(isCancelledFlag);
820831
}
821832
});

test/ngAnimate/animateSpec.js

+55-1
Original file line numberDiff line numberDiff line change
@@ -2673,10 +2673,16 @@ describe("ngAnimate", function() {
26732673
beforeAddClass : function(element, className, done) {
26742674
currentAnimation = 'addClass';
26752675
currentFn = done;
2676+
return function(cancelled) {
2677+
currentAnimation = cancelled ? null : currentAnimation;
2678+
}
26762679
},
26772680
beforeRemoveClass : function(element, className, done) {
26782681
currentAnimation = 'removeClass';
26792682
currentFn = done;
2683+
return function(cancelled) {
2684+
currentAnimation = cancelled ? null : currentAnimation;
2685+
}
26802686
}
26812687
};
26822688
});
@@ -2690,10 +2696,12 @@ describe("ngAnimate", function() {
26902696
expect(currentAnimation).toBe('addClass');
26912697
currentFn();
26922698

2699+
currentAnimation = null;
2700+
26932701
$animate.removeClass(element, 'on');
26942702
$animate.addClass(element, 'on');
26952703

2696-
expect(currentAnimation).toBe('addClass');
2704+
expect(currentAnimation).toBe(null);
26972705
});
26982706
});
26992707

@@ -3113,5 +3121,51 @@ describe("ngAnimate", function() {
31133121
$timeout.flush(1);
31143122
expect(ready).toBe(true);
31153123
}));
3124+
3125+
it('should avoid skip animations if the same CSS class is added / removed synchronously before the reflow kicks in',
3126+
inject(function($sniffer, $compile, $rootScope, $rootElement, $animate, $timeout) {
3127+
3128+
if (!$sniffer.transitions) return;
3129+
3130+
ss.addRule('.water-class', '-webkit-transition:2s linear all;' +
3131+
'transition:2s linear all;');
3132+
3133+
$animate.enabled(true);
3134+
3135+
var element = $compile('<div class="water-class on"></div>')($rootScope);
3136+
$rootElement.append(element);
3137+
jqLite($document[0].body).append($rootElement);
3138+
3139+
var signature = '';
3140+
$animate.removeClass(element, 'on', function() {
3141+
signature += 'A';
3142+
});
3143+
$animate.addClass(element, 'on', function() {
3144+
signature += 'B';
3145+
});
3146+
3147+
$timeout.flush(1);
3148+
expect(signature).toBe('AB');
3149+
3150+
signature = '';
3151+
$animate.removeClass(element, 'on', function() {
3152+
signature += 'A';
3153+
});
3154+
$animate.addClass(element, 'on', function() {
3155+
signature += 'B';
3156+
});
3157+
$animate.removeClass(element, 'on', function() {
3158+
signature += 'C';
3159+
});
3160+
3161+
$timeout.flush(1);
3162+
expect(signature).toBe('AB');
3163+
3164+
$timeout.flush(10);
3165+
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2000 });
3166+
$timeout.flush(1);
3167+
3168+
expect(signature).toBe('ABC');
3169+
}));
31163170
});
31173171
});

0 commit comments

Comments
 (0)