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

Commit d881673

Browse files
committed
fix($animateCss): properly handle cancellation timeouts for follow-up animations
Prior to this fix if `$animateCss` was called multiple on the same element with new animation data then the preceeding fallback timout would cause the animation to cancel midway. This fix ensures that `$animateCss` can be triggered multiple times and only when the final timeout has passed then all animations will be closed. Closes #12490 Closes #12359
1 parent 0a75a3d commit d881673

File tree

2 files changed

+88
-6
lines changed

2 files changed

+88
-6
lines changed

src/ngAnimate/animateCss.js

+32-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
var ANIMATE_TIMER_KEY = '$$animateCss';
4+
35
/**
46
* @ngdoc service
57
* @name $animateCss
@@ -861,17 +863,41 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
861863
}
862864

863865
startTime = Date.now();
864-
element.on(events.join(' '), onAnimationProgress);
865-
$timeout(onAnimationExpired, maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime, false);
866+
var timerTime = maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime;
867+
var endTime = startTime + timerTime;
868+
869+
var animationsData = element.data(ANIMATE_TIMER_KEY) || [];
870+
var setupFallbackTimer = true;
871+
if (animationsData.length) {
872+
var currentTimerData = animationsData[0];
873+
setupFallbackTimer = endTime > currentTimerData.expectedEndTime;
874+
if (setupFallbackTimer) {
875+
$timeout.cancel(currentTimerData.timer);
876+
} else {
877+
animationsData.push(close);
878+
}
879+
}
866880

881+
if (setupFallbackTimer) {
882+
var timer = $timeout(onAnimationExpired, timerTime, false);
883+
animationsData[0] = {
884+
timer: timer,
885+
expectedEndTime: endTime
886+
};
887+
animationsData.push(close);
888+
element.data(ANIMATE_TIMER_KEY, animationsData);
889+
}
890+
891+
element.on(events.join(' '), onAnimationProgress);
867892
applyAnimationToStyles(element, options);
868893
}
869894

870895
function onAnimationExpired() {
871-
// although an expired animation is a failed animation, getting to
872-
// this outcome is very easy if the CSS code screws up. Therefore we
873-
// should still continue normally as if the animation completed correctly.
874-
close();
896+
var animationsData = element.data(ANIMATE_TIMER_KEY);
897+
for (var i = 1; i < animationsData.length; i++) {
898+
animationsData[i]();
899+
}
900+
element.removeData(ANIMATE_TIMER_KEY);
875901
}
876902

877903
function onAnimationProgress(event) {

test/ngAnimate/animateCssSpec.js

+56
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,62 @@ describe("ngAnimate $animateCss", function() {
11351135
expect(passed).toBe(true);
11361136
expect(failed).not.toBe(true);
11371137
}));
1138+
1139+
it("should close all stacked animations after the last timeout runs on the same element",
1140+
inject(function($animateCss, $$body, $rootElement, $timeout) {
1141+
1142+
var now = 0;
1143+
spyOn(Date, 'now').andCallFake(function() {
1144+
return now;
1145+
});
1146+
1147+
var cancelSpy = spyOn($timeout, 'cancel').andCallThrough();
1148+
var doneSpy = jasmine.createSpy();
1149+
1150+
ss.addRule('.elm', 'transition:1s linear all;');
1151+
ss.addRule('.elm.red', 'background:red;');
1152+
ss.addRule('.elm.blue', 'transition:2s linear all; background:blue;');
1153+
ss.addRule('.elm.green', 'background:green;');
1154+
1155+
var element = jqLite('<div class="elm"></div>');
1156+
$rootElement.append(element);
1157+
$$body.append($rootElement);
1158+
1159+
// timeout will be at 1500s
1160+
animate(element, 'red', doneSpy);
1161+
expect(doneSpy).not.toHaveBeenCalled();
1162+
1163+
fastForwardClock(500); //1000s left to go
1164+
1165+
// timeout will not be at 500 + 3000s = 3500s
1166+
animate(element, 'blue', doneSpy);
1167+
expect(doneSpy).not.toHaveBeenCalled();
1168+
expect(cancelSpy).toHaveBeenCalled();
1169+
1170+
cancelSpy.reset();
1171+
1172+
// timeout will not be set again since the former animation is longer
1173+
animate(element, 'green', doneSpy);
1174+
expect(doneSpy).not.toHaveBeenCalled();
1175+
expect(cancelSpy).not.toHaveBeenCalled();
1176+
1177+
// this will close the animations fully
1178+
fastForwardClock(3500);
1179+
expect(doneSpy).toHaveBeenCalled();
1180+
expect(doneSpy.callCount).toBe(3);
1181+
1182+
function fastForwardClock(time) {
1183+
now += time;
1184+
$timeout.flush(time);
1185+
}
1186+
1187+
function animate(element, klass, onDone) {
1188+
var animator = $animateCss(element, { addClass: klass }).start();
1189+
animator.done(onDone);
1190+
triggerAnimationStartFrame();
1191+
return animator;
1192+
}
1193+
}));
11381194
});
11391195

11401196
describe("getComputedStyle", function() {

0 commit comments

Comments
 (0)