From a7ccaf3587ffa4f2d0479657426c7c2f40062d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 25 Sep 2013 00:20:18 -0400 Subject: [PATCH 1/2] feat(browserTrigger): allow support for custom timeStamps in events --- src/ngScenario/browserTrigger.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ngScenario/browserTrigger.js b/src/ngScenario/browserTrigger.js index 3da6d5eae9b7..c614218d8068 100644 --- a/src/ngScenario/browserTrigger.js +++ b/src/ngScenario/browserTrigger.js @@ -101,7 +101,7 @@ } catch(e) { evnt = document.createEvent('TransitionEvent'); - evnt.initTransitionEvent(eventType, null, null, null, eventData.elapsedTime); + evnt.initTransitionEvent(eventType, null, null, null, eventData.elapsedTime || 0); } } } @@ -116,7 +116,7 @@ } catch(e) { evnt = document.createEvent('AnimationEvent'); - evnt.initAnimationEvent(eventType, null, null, null, eventData.elapsedTime); + evnt.initAnimationEvent(eventType, null, null, null, eventData.elapsedTime || 0); } } } @@ -128,6 +128,11 @@ pressed('shift'), pressed('meta'), 0, element); } + /* we're unable to change the timeStamp value directly so this + * is only here to allow for testing where the timeStamp value is + * read */ + evnt.$manualTimeStamp = eventData.timeStamp; + if(!evnt) return; var originalPreventDefault = evnt.preventDefault, From 0249531b669b1c51b47c641f5980982805a940a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 25 Sep 2013 00:20:25 -0400 Subject: [PATCH 2/2] fix(ngAnimate): ensure that delays are always considered before an animation closes Closes #4028 --- src/ngAnimate/animate.js | 27 ++++++-- test/ngAnimate/animateSpec.js | 113 ++++++++++++++++++++++------------ 2 files changed, 95 insertions(+), 45 deletions(-) diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 34a105e1230b..af607568662b 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -560,6 +560,7 @@ angular.module('ngAnimate', ['ng']) var durationKey = 'Duration', propertyKey = 'Property', + delayKey = 'Delay', animationIterationCountKey = 'IterationCount', ELEMENT_NODE = 1; @@ -593,6 +594,12 @@ angular.module('ngAnimate', ['ng']) if (element.nodeType == ELEMENT_NODE) { var elementStyles = $window.getComputedStyle(element) || {}; + var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]), + parseMaxTime(elementStyles[vendorTransitionProp + delayKey])); + + var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]), + parseMaxTime(elementStyles[vendorAnimationProp + delayKey])); + var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]), parseMaxTime(elementStyles[vendorTransitionProp + durationKey])); @@ -605,17 +612,18 @@ angular.module('ngAnimate', ['ng']) 1); } - transitionTime = Math.max(transitionDuration, transitionTime); - animationTime = Math.max(animationDuration, animationTime); + transitionTime = Math.max(transitionDelay + transitionDuration, transitionTime); + animationTime = Math.max(animationDelay + animationDuration, animationTime); } }); /* there is no point in performing a reflow if the animation timeout is empty (this would cause a flicker bug normally in the page */ - var totalTime = Math.max(transitionTime,animationTime); - if(totalTime > 0) { - var node = element[0]; + var maxTime = Math.max(transitionTime,animationTime) * 1000; + if(maxTime > 0) { + var node = element[0], + startTime = Date.now(); //temporarily disable the transition so that the enter styles //don't animate twice (this is here to avoid a bug in Chrome/FF). @@ -659,7 +667,14 @@ angular.module('ngAnimate', ['ng']) } function onAnimationProgress(event) { - (event.elapsedTime || (event.originalEvent && event.originalEvent.elapsedTime)) >= totalTime && done(); + event.stopPropagation(); + var ev = event.originalEvent || event; + /* $manualTimeStamp is a mocked timeStamp value which is set + * within browserTrigger(). This is only here so that tests can + * mock animations properly. Real events fallback to event.timeStamp. */ + if((ev.$manualTimeStamp || ev.timeStamp) - startTime >= maxTime) { + done(); + } } function parseMaxTime(str) { diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 466aa741094b..adedeac4bf1f 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -138,7 +138,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(child.hasClass('ng-enter')).toBe(true); expect(child.hasClass('ng-enter-active')).toBe(true); - browserTrigger(element, 'transitionend', { elapsedTime: 1 }); + browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000 }); } expect(element.contents().length).toBe(1); @@ -154,7 +154,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(child.hasClass('ng-leave')).toBe(true); expect(child.hasClass('ng-leave-active')).toBe(true); - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); } expect(element.contents().length).toBe(0); @@ -186,7 +186,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(child.hasClass('ng-hide-remove')).toBe(true); expect(child.hasClass('ng-hide-remove-active')).toBe(true); - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); } expect(child.hasClass('ng-hide-remove')).toBe(false); expect(child.hasClass('ng-hide-remove-active')).toBe(false); @@ -202,7 +202,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(child.hasClass('ng-hide-add')).toBe(true); expect(child.hasClass('ng-hide-add-active')).toBe(true); - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); } expect(child).toBeHidden(); })); @@ -221,7 +221,7 @@ describe("ngAnimate", function() { expect(child.attr('class')).toContain('ng-enter'); expect(child.attr('class')).toContain('ng-enter-active'); - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); //move element.append(after); @@ -230,26 +230,26 @@ describe("ngAnimate", function() { expect(child.attr('class')).toContain('ng-move'); expect(child.attr('class')).toContain('ng-move-active'); - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); //hide $animate.addClass(child, 'ng-hide'); expect(child.attr('class')).toContain('ng-hide-add'); expect(child.attr('class')).toContain('ng-hide-add-active'); - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); //show $animate.removeClass(child, 'ng-hide'); expect(child.attr('class')).toContain('ng-hide-remove'); expect(child.attr('class')).toContain('ng-hide-remove-active'); - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); //leave $animate.leave(child); $rootScope.$digest(); expect(child.attr('class')).toContain('ng-leave'); expect(child.attr('class')).toContain('ng-leave-active'); - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); })); it("should not run if animations are disabled", @@ -292,7 +292,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(element.children().length).toBe(1); //still animating - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); } $timeout.flush(2000); $timeout.flush(2000); @@ -309,7 +309,7 @@ describe("ngAnimate", function() { child.addClass('custom-delay ng-hide'); $animate.removeClass(child, 'ng-hide'); if($sniffer.transitions) { - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); } $timeout.flush(2000); @@ -373,7 +373,7 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'ng-hide'); if($sniffer.transitions) { - browserTrigger(element,'transitionend', { elapsedTime: 1 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000 }); } $timeout.flush(2000); $timeout.flush(20000); @@ -416,7 +416,7 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'ng-hide'); if ($sniffer.animations) { - browserTrigger(element,'animationend', { elapsedTime: 4 }); + browserTrigger(element,'animationend', { timeStamp: Date.now() + 4000 }); } expect(element).toBeShown(); })); @@ -439,7 +439,7 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'ng-hide'); if ($sniffer.animations) { - browserTrigger(element,'animationend', { elapsedTime: 6 }); + browserTrigger(element,'animationend', { timeStamp: Date.now() + 6000 }); } expect(element).toBeShown(); })); @@ -462,7 +462,7 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'ng-hide'); if ($sniffer.animations) { - browserTrigger(element,'animationend', { elapsedTime: 2 }); + browserTrigger(element,'animationend', { timeStamp: Date.now() + 2000 }); } expect(element).toBeShown(); })); @@ -487,7 +487,7 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'ng-hide'); if ($sniffer.transitions) { - browserTrigger(element,'animationend', { elapsedTime: 10 }); + browserTrigger(element,'animationend', { timeStamp : Date.now() + 20000 }); } expect(element).toBeShown(); })); @@ -533,7 +533,7 @@ describe("ngAnimate", function() { if($sniffer.animations) { //cleanup some pending animations expect(element.hasClass('ng-hide-add')).toBe(true); expect(element.hasClass('ng-hide-add-active')).toBe(true); - browserTrigger(element,'animationend', { elapsedTime: 2 }); + browserTrigger(element,'animationend', { timeStamp: Date.now() + 2000 }); } expect(element.hasClass('ng-hide-remove-active')).toBe(false); @@ -566,7 +566,7 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'ng-hide'); if ($sniffer.transitions) { - browserTrigger(element,'transitionend', { elapsedTime: 1 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000 }); } expect(element).toBeShown(); })); @@ -587,9 +587,10 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'ng-hide'); if ($sniffer.transitions) { expect(element).toBeHidden(); - browserTrigger(element,'transitionend', { elapsedTime: 1 }); - browserTrigger(element,'transitionend', { elapsedTime: 1 }); - browserTrigger(element,'transitionend', { elapsedTime: 2 }); + var now = Date.now(); + browserTrigger(element,'transitionend', { timeStamp: now + 1000 }); + browserTrigger(element,'transitionend', { timeStamp: now + 1000 }); + browserTrigger(element,'transitionend', { timeStamp: now + 2000 }); } expect(element).toBeShown(); })); @@ -620,9 +621,10 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'ng-hide'); if ($sniffer.transitions) { - browserTrigger(element,'transitionend', { elapsedTime: 0 }); - browserTrigger(element,'transitionend', { elapsedTime: 1 }); - browserTrigger(element,'transitionend', { elapsedTime: 1 }); + var now = Date.now(); + browserTrigger(element,'transitionend', { timeStamp: now + 1000 }); + browserTrigger(element,'transitionend', { timeStamp: now + 3000 }); + browserTrigger(element,'transitionend', { timeStamp: now + 3000 }); } expect(element).toBeShown(); })); @@ -644,7 +646,7 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'ng-hide'); if ($sniffer.transitions) { - browserTrigger(element,'animationend', { elapsedTime: 10 }); + browserTrigger(element,'animationend', { timeStamp: Date.now() + 11000 }); } expect(element).toBeShown(); })); @@ -664,7 +666,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(element.hasClass('ng-hide-remove')).toBe(true); expect(element.hasClass('ng-hide-remove-active')).toBe(true); - browserTrigger(element,'transitionend', { elapsedTime: 1 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000 }); } expect(element.hasClass('ng-hide-remove')).toBe(false); expect(element.hasClass('ng-hide-remove-active')).toBe(false); @@ -700,7 +702,7 @@ describe("ngAnimate", function() { if ($sniffer.transitions) { expect(element.hasClass('abc ng-enter')).toBe(true); expect(element.hasClass('abc ng-enter ng-enter-active')).toBe(true); - browserTrigger(element,'transitionend', { elapsedTime: 22 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 22000 }); } expect(element.hasClass('abc')).toBe(true); @@ -711,7 +713,7 @@ describe("ngAnimate", function() { if ($sniffer.transitions) { expect(element.hasClass('xyz')).toBe(true); expect(element.hasClass('xyz ng-enter ng-enter-active')).toBe(true); - browserTrigger(element,'transitionend', { elapsedTime: 11 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000 }); } expect(element.hasClass('xyz')).toBe(true); })); @@ -738,7 +740,7 @@ describe("ngAnimate", function() { expect(element.hasClass('one two ng-enter ng-enter-active')).toBe(true); expect(element.hasClass('one-active')).toBe(false); expect(element.hasClass('two-active')).toBe(false); - browserTrigger(element,'transitionend', { elapsedTime: 3 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000 }); } expect(element.hasClass('one two')).toBe(true); @@ -887,7 +889,7 @@ describe("ngAnimate", function() { }); if($sniffer.transitions) { - browserTrigger(element,'transitionend', { elapsedTime: 1 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000 }); } $timeout.flush(); expect(flag).toBe(true); @@ -1025,7 +1027,7 @@ describe("ngAnimate", function() { expect(element.hasClass('klass')).toBe(false); expect(element.hasClass('klass-add')).toBe(true); expect(element.hasClass('klass-add-active')).toBe(true); - browserTrigger(element,'transitionend', { elapsedTime: 3 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000 }); } //this cancels out the older animation @@ -1039,7 +1041,7 @@ describe("ngAnimate", function() { expect(element.hasClass('klass-add-active')).toBe(false); expect(element.hasClass('klass-remove')).toBe(true); - browserTrigger(element,'transitionend', { elapsedTime: 3 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000 }); } $timeout.flush(); @@ -1097,7 +1099,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(element.hasClass('klass-add')).toBe(true); expect(element.hasClass('klass-add-active')).toBe(true); - browserTrigger(element,'transitionend', { elapsedTime: 11 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000 }); expect(element.hasClass('klass-add')).toBe(false); expect(element.hasClass('klass-add-active')).toBe(false); } @@ -1111,7 +1113,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(element.hasClass('klass-remove')).toBe(true); expect(element.hasClass('klass-remove-active')).toBe(true); - browserTrigger(element,'transitionend', { elapsedTime: 11 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000 }); expect(element.hasClass('klass-remove')).toBe(false); expect(element.hasClass('klass-remove-active')).toBe(false); } @@ -1146,7 +1148,7 @@ describe("ngAnimate", function() { expect(element.hasClass('one-add-active')).toBe(true); expect(element.hasClass('two-add-active')).toBe(true); - browserTrigger(element,'transitionend', { elapsedTime: 7 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 7000 }); expect(element.hasClass('one-add')).toBe(false); expect(element.hasClass('one-add-active')).toBe(false); @@ -1190,7 +1192,7 @@ describe("ngAnimate", function() { expect(element.hasClass('one-remove-active')).toBe(true); expect(element.hasClass('two-remove-active')).toBe(true); - browserTrigger(element,'transitionend', { elapsedTime: 9 }); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 9000 }); expect(element.hasClass('one-remove')).toBe(false); expect(element.hasClass('one-remove-active')).toBe(false); @@ -1240,7 +1242,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(child.hasClass('ng-enter')).toBe(true); expect(child.hasClass('ng-enter-active')).toBe(true); - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); } expect(child.hasClass('ng-enter')).toBe(false); @@ -1262,7 +1264,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(child.hasClass('ng-enter')).toBe(true); expect(child.hasClass('ng-enter-active')).toBe(true); - browserTrigger(child,'transitionend', { elapsedTime: 8 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 9000 }); } expect(child.hasClass('ng-enter')).toBe(false); expect(child.hasClass('ng-enter-active')).toBe(false); @@ -1314,7 +1316,7 @@ describe("ngAnimate", function() { $timeout.flush(10); if($sniffer.transitions) { - browserTrigger(child,'transitionend', { elapsedTime: 1 }); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000 }); } expect(child.hasClass('i-was-animated')).toBe(true); @@ -1522,4 +1524,37 @@ describe("ngAnimate", function() { }); }); + it("should wait until both the duration and delay are complete to close off the animation", + inject(function($compile, $rootScope, $animate, $timeout, $sniffer) { + + if(!$sniffer.transitions) return; + + var element = html($compile('
')($rootScope)); + var child = html($compile('
')($rootScope)); + + ss.addRule('.animated.ng-enter', 'transition: width 1s, background 1s 1s;' + + vendorPrefix + 'transition: width 1s, background 1s 1s;'); + + $rootElement.append(element); + jqLite(document.body).append($rootElement); + + $animate.enter(child, element); + $rootScope.$digest(); + + expect(child.hasClass('ng-enter')).toBe(true); + expect(child.hasClass('ng-enter-active')).toBe(true); + + browserTrigger(child, 'transitionend', { timeStamp: Date.now() + 1000 }); + + expect(child.hasClass('ng-enter')).toBe(true); + expect(child.hasClass('ng-enter-active')).toBe(true); + + browserTrigger(child, 'transitionend', { timeStamp: Date.now() + 2000 }); + + expect(child.hasClass('ng-enter')).toBe(false); + expect(child.hasClass('ng-enter-active')).toBe(false); + + expect(element.contents().length).toBe(1); + })); + });