mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-05-12 00:43:10 +00:00
fix($animate): prevent race conditions for class-based animations when animating on the same CSS class
Closes #5588
This commit is contained in:
parent
7d5d62dafe
commit
4aa9df7a7a
2 changed files with 70 additions and 5 deletions
|
|
@ -659,12 +659,23 @@ angular.module('ngAnimate', ['ng'])
|
||||||
cleanup(element);
|
cleanup(element);
|
||||||
cancelAnimations(ngAnimateState.animations);
|
cancelAnimations(ngAnimateState.animations);
|
||||||
|
|
||||||
|
//in the event that the CSS is class is quickly added and removed back
|
||||||
|
//then we don't want to wait until after the reflow to add/remove the CSS
|
||||||
|
//class since both class animations may run into a race condition.
|
||||||
|
//The code below will check to see if that is occurring and will
|
||||||
|
//immediately remove the former class before the reflow so that the
|
||||||
|
//animation can snap back to the original animation smoothly
|
||||||
|
var isFullyClassBasedAnimation = isClassBased && !ngAnimateState.structural;
|
||||||
|
var isRevertingClassAnimation = isFullyClassBasedAnimation &&
|
||||||
|
ngAnimateState.className == className &&
|
||||||
|
animationEvent != ngAnimateState.event;
|
||||||
|
|
||||||
//if the class is removed during the reflow then it will revert the styles temporarily
|
//if the class is removed during the reflow then it will revert the styles temporarily
|
||||||
//back to the base class CSS styling causing a jump-like effect to occur. This check
|
//back to the base class CSS styling causing a jump-like effect to occur. This check
|
||||||
//here ensures that the domOperation is only performed after the reflow has commenced
|
//here ensures that the domOperation is only performed after the reflow has commenced
|
||||||
if(ngAnimateState.beforeComplete) {
|
if(ngAnimateState.beforeComplete || isRevertingClassAnimation) {
|
||||||
(ngAnimateState.done || noop)(true);
|
(ngAnimateState.done || noop)(true);
|
||||||
} else if(isClassBased && !ngAnimateState.structural) {
|
} else if(isFullyClassBasedAnimation) {
|
||||||
//class-based animations will compare element className values after cancelling the
|
//class-based animations will compare element className values after cancelling the
|
||||||
//previous animation to see if the element properties already contain the final CSS
|
//previous animation to see if the element properties already contain the final CSS
|
||||||
//class and if so then the animation will be skipped. Since the domOperation will
|
//class and if so then the animation will be skipped. Since the domOperation will
|
||||||
|
|
@ -812,10 +823,10 @@ angular.module('ngAnimate', ['ng'])
|
||||||
function cancelAnimations(animations) {
|
function cancelAnimations(animations) {
|
||||||
var isCancelledFlag = true;
|
var isCancelledFlag = true;
|
||||||
forEach(animations, function(animation) {
|
forEach(animations, function(animation) {
|
||||||
if(!animations.beforeComplete) {
|
if(!animation.beforeComplete) {
|
||||||
(animation.beforeEnd || noop)(isCancelledFlag);
|
(animation.beforeEnd || noop)(isCancelledFlag);
|
||||||
}
|
}
|
||||||
if(!animations.afterComplete) {
|
if(!animation.afterComplete) {
|
||||||
(animation.afterEnd || noop)(isCancelledFlag);
|
(animation.afterEnd || noop)(isCancelledFlag);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2673,10 +2673,16 @@ describe("ngAnimate", function() {
|
||||||
beforeAddClass : function(element, className, done) {
|
beforeAddClass : function(element, className, done) {
|
||||||
currentAnimation = 'addClass';
|
currentAnimation = 'addClass';
|
||||||
currentFn = done;
|
currentFn = done;
|
||||||
|
return function(cancelled) {
|
||||||
|
currentAnimation = cancelled ? null : currentAnimation;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeRemoveClass : function(element, className, done) {
|
beforeRemoveClass : function(element, className, done) {
|
||||||
currentAnimation = 'removeClass';
|
currentAnimation = 'removeClass';
|
||||||
currentFn = done;
|
currentFn = done;
|
||||||
|
return function(cancelled) {
|
||||||
|
currentAnimation = cancelled ? null : currentAnimation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -2690,10 +2696,12 @@ describe("ngAnimate", function() {
|
||||||
expect(currentAnimation).toBe('addClass');
|
expect(currentAnimation).toBe('addClass');
|
||||||
currentFn();
|
currentFn();
|
||||||
|
|
||||||
|
currentAnimation = null;
|
||||||
|
|
||||||
$animate.removeClass(element, 'on');
|
$animate.removeClass(element, 'on');
|
||||||
$animate.addClass(element, 'on');
|
$animate.addClass(element, 'on');
|
||||||
|
|
||||||
expect(currentAnimation).toBe('addClass');
|
expect(currentAnimation).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -3113,5 +3121,51 @@ describe("ngAnimate", function() {
|
||||||
$timeout.flush(1);
|
$timeout.flush(1);
|
||||||
expect(ready).toBe(true);
|
expect(ready).toBe(true);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should avoid skip animations if the same CSS class is added / removed synchronously before the reflow kicks in',
|
||||||
|
inject(function($sniffer, $compile, $rootScope, $rootElement, $animate, $timeout) {
|
||||||
|
|
||||||
|
if (!$sniffer.transitions) return;
|
||||||
|
|
||||||
|
ss.addRule('.water-class', '-webkit-transition:2s linear all;' +
|
||||||
|
'transition:2s linear all;');
|
||||||
|
|
||||||
|
$animate.enabled(true);
|
||||||
|
|
||||||
|
var element = $compile('<div class="water-class on"></div>')($rootScope);
|
||||||
|
$rootElement.append(element);
|
||||||
|
jqLite($document[0].body).append($rootElement);
|
||||||
|
|
||||||
|
var signature = '';
|
||||||
|
$animate.removeClass(element, 'on', function() {
|
||||||
|
signature += 'A';
|
||||||
|
});
|
||||||
|
$animate.addClass(element, 'on', function() {
|
||||||
|
signature += 'B';
|
||||||
|
});
|
||||||
|
|
||||||
|
$timeout.flush(1);
|
||||||
|
expect(signature).toBe('AB');
|
||||||
|
|
||||||
|
signature = '';
|
||||||
|
$animate.removeClass(element, 'on', function() {
|
||||||
|
signature += 'A';
|
||||||
|
});
|
||||||
|
$animate.addClass(element, 'on', function() {
|
||||||
|
signature += 'B';
|
||||||
|
});
|
||||||
|
$animate.removeClass(element, 'on', function() {
|
||||||
|
signature += 'C';
|
||||||
|
});
|
||||||
|
|
||||||
|
$timeout.flush(1);
|
||||||
|
expect(signature).toBe('AB');
|
||||||
|
|
||||||
|
$timeout.flush(10);
|
||||||
|
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2000 });
|
||||||
|
$timeout.flush(1);
|
||||||
|
|
||||||
|
expect(signature).toBe('ABC');
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue