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

Commit 00b8f43

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 #12359
1 parent 5abf593 commit 00b8f43

File tree

2 files changed

+92
-6
lines changed

2 files changed

+92
-6
lines changed

Diff for: 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
@@ -858,17 +860,41 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
858860
}
859861

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

878+
if (setupFallbackTimer) {
879+
var timer = $timeout(onAnimationExpired, timerTime, false);
880+
animationsData[0] = {
881+
timer: timer,
882+
expectedEndTime: endTime
883+
};
884+
animationsData.push(close);
885+
element.data(ANIMATE_TIMER_KEY, animationsData);
886+
}
887+
888+
element.on(events.join(' '), onAnimationProgress);
864889
applyAnimationToStyles(element, options);
865890
}
866891

867892
function onAnimationExpired() {
868-
// although an expired animation is a failed animation, getting to
869-
// this outcome is very easy if the CSS code screws up. Therefore we
870-
// should still continue normally as if the animation completed correctly.
871-
close();
893+
var animationsData = element.data(ANIMATE_TIMER_KEY);
894+
for (var i = 1; i < animationsData.length; i++) {
895+
animationsData[i]();
896+
}
897+
element.removeData(ANIMATE_TIMER_KEY);
872898
}
873899

874900
function onAnimationProgress(event) {

Diff for: test/ngAnimate/animateCssSpec.js

+60
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() {
@@ -2657,6 +2713,10 @@ describe("ngAnimate $animateCss", function() {
26572713
}));
26582714
});
26592715

2716+
describe('multiple calls', function() {
2717+
2718+
});
2719+
26602720
describe('SVG', function() {
26612721
it('should properly apply transitions on an SVG element',
26622722
inject(function($animateCss, $rootScope, $compile, $$body, $rootElement) {

0 commit comments

Comments
 (0)