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

Commit 23da614

Browse files
committed
fix($animate): use $timeout to handle the delay within staggering animations
When transition-delay and animation-delay were used to drive the staggering animation the result was unpredictable at times due to the browser not being able to register the generated delay styles in time. This caused a hard to track down bug that didn't have a solid solution when styles were being used. This fix ensures that stagger delays are handled by the $timeout service. Closes #7228 Closes #7547 Closes #8297 Closes #8547 BREAKING CHANGE If any stagger code consisted of having BOTH transition staggers and delay staggers together then that will not work the same way. Angular will now instead choose the highest stagger delay value and set the timeout to wait for that before applying the active CSS class.
1 parent bf0f550 commit 23da614

File tree

2 files changed

+200
-124
lines changed

2 files changed

+200
-124
lines changed

src/ngAnimate/animate.js

+69-72
Original file line numberDiff line numberDiff line change
@@ -484,12 +484,12 @@ angular.module('ngAnimate', ['ng'])
484484
// the matching CSS class.
485485
if (status < 0) {
486486
//does it have the class or will it have the class
487-
if(hasClass || matchingAnimation.event == 'addClass') {
487+
if (hasClass || matchingAnimation.event == 'addClass') {
488488
toRemove.push(className);
489489
}
490490
} else if (status > 0) {
491491
//is the class missing or will it be removed?
492-
if(!hasClass || matchingAnimation.event == 'removeClass') {
492+
if (!hasClass || matchingAnimation.event == 'removeClass') {
493493
toAdd.push(className);
494494
}
495495
}
@@ -544,7 +544,7 @@ angular.module('ngAnimate', ['ng'])
544544
if (!classNameAdd) {
545545
className = classNameRemove;
546546
animationEvent = 'removeClass';
547-
} else if(!classNameRemove) {
547+
} else if (!classNameRemove) {
548548
className = classNameAdd;
549549
animationEvent = 'addClass';
550550
} else {
@@ -1101,6 +1101,7 @@ angular.module('ngAnimate', ['ng'])
11011101
var totalActiveAnimations = ngAnimateState.totalActive || 0;
11021102
var lastAnimation = ngAnimateState.last;
11031103
var skipAnimation = false;
1104+
11041105
if (totalActiveAnimations > 0) {
11051106
var animationsToCancel = [];
11061107
if (!runner.isClassBased) {
@@ -1384,6 +1385,7 @@ angular.module('ngAnimate', ['ng'])
13841385
var PROPERTY_KEY = 'Property';
13851386
var DELAY_KEY = 'Delay';
13861387
var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
1388+
var ANIMATION_PLAYSTATE_KEY = 'PlayState';
13871389
var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey';
13881390
var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data';
13891391
var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
@@ -1455,47 +1457,33 @@ angular.module('ngAnimate', ['ng'])
14551457
var transitionDelay = 0;
14561458
var animationDuration = 0;
14571459
var animationDelay = 0;
1458-
var transitionDelayStyle;
1459-
var animationDelayStyle;
1460-
var transitionDurationStyle;
1461-
var transitionPropertyStyle;
14621460

14631461
//we want all the styles defined before and after
14641462
forEach(element, function(element) {
14651463
if (element.nodeType == ELEMENT_NODE) {
14661464
var elementStyles = $window.getComputedStyle(element) || {};
14671465

1468-
transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY];
1469-
1466+
var transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY];
14701467
transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration);
14711468

1472-
transitionPropertyStyle = elementStyles[TRANSITION_PROP + PROPERTY_KEY];
1473-
1474-
transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY];
1475-
1469+
var transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY];
14761470
transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay);
14771471

1478-
animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY];
1479-
1480-
animationDelay = Math.max(parseMaxTime(animationDelayStyle), animationDelay);
1472+
var animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY];
1473+
animationDelay = Math.max(parseMaxTime(elementStyles[ANIMATION_PROP + DELAY_KEY]), animationDelay);
14811474

14821475
var aDuration = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]);
14831476

14841477
if (aDuration > 0) {
14851478
aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1;
14861479
}
1487-
14881480
animationDuration = Math.max(aDuration, animationDuration);
14891481
}
14901482
});
14911483
data = {
14921484
total : 0,
1493-
transitionPropertyStyle: transitionPropertyStyle,
1494-
transitionDurationStyle: transitionDurationStyle,
1495-
transitionDelayStyle: transitionDelayStyle,
14961485
transitionDelay: transitionDelay,
14971486
transitionDuration: transitionDuration,
1498-
animationDelayStyle: animationDelayStyle,
14991487
animationDelay: animationDelay,
15001488
animationDuration: animationDuration
15011489
};
@@ -1571,18 +1559,17 @@ angular.module('ngAnimate', ['ng'])
15711559
running : formerData.running || 0,
15721560
itemIndex : itemIndex,
15731561
blockTransition : blockTransition,
1574-
blockAnimation : blockAnimation,
15751562
closeAnimationFns : closeAnimationFns
15761563
});
15771564

15781565
var node = extractElementNode(element);
15791566

15801567
if (blockTransition) {
1581-
node.style[TRANSITION_PROP + PROPERTY_KEY] = 'none';
1568+
blockTransitions(node, true);
15821569
}
15831570

15841571
if (blockAnimation) {
1585-
node.style[ANIMATION_PROP] = 'none 0s';
1572+
blockAnimations(node, true);
15861573
}
15871574

15881575
return true;
@@ -1597,22 +1584,43 @@ angular.module('ngAnimate', ['ng'])
15971584
}
15981585

15991586
if (elementData.blockTransition) {
1600-
node.style[TRANSITION_PROP + PROPERTY_KEY] = '';
1601-
}
1602-
1603-
if (elementData.blockAnimation) {
1604-
node.style[ANIMATION_PROP] = '';
1587+
blockTransitions(node, false);
16051588
}
16061589

16071590
var activeClassName = '';
1591+
var pendingClassName = '';
16081592
forEach(className.split(' '), function(klass, i) {
1609-
activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
1593+
var prefix = (i > 0 ? ' ' : '') + klass;
1594+
activeClassName += prefix + '-active';
1595+
pendingClassName += prefix + '-pending';
16101596
});
16111597

1612-
element.addClass(activeClassName);
1598+
var style = '';
1599+
var appliedStyles = [];
1600+
var itemIndex = elementData.itemIndex;
1601+
var stagger = elementData.stagger;
1602+
var staggerTime = 0;
1603+
if (itemIndex > 0) {
1604+
var transitionStaggerDelay = 0;
1605+
if (stagger.transitionDelay > 0 && stagger.transitionDuration === 0) {
1606+
transitionStaggerDelay = stagger.transitionDelay * itemIndex;
1607+
}
1608+
1609+
var animationStaggerDelay = 0;
1610+
if (stagger.animationDelay > 0 && stagger.animationDuration === 0) {
1611+
animationStaggerDelay = stagger.animationDelay * itemIndex;
1612+
appliedStyles.push(CSS_PREFIX + 'animation-play-state');
1613+
}
1614+
1615+
staggerTime = Math.round(Math.max(transitionStaggerDelay, animationStaggerDelay) * 100) / 100;
1616+
}
1617+
1618+
if (!staggerTime) {
1619+
element.addClass(activeClassName);
1620+
}
1621+
16131622
var eventCacheKey = elementData.cacheKey + ' ' + activeClassName;
16141623
var timings = getElementAnimationDetails(element, eventCacheKey);
1615-
16161624
var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration);
16171625
if (maxDuration === 0) {
16181626
element.removeClass(activeClassName);
@@ -1622,57 +1630,43 @@ angular.module('ngAnimate', ['ng'])
16221630
}
16231631

16241632
var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay);
1625-
var stagger = elementData.stagger;
1626-
var itemIndex = elementData.itemIndex;
16271633
var maxDelayTime = maxDelay * ONE_SECOND;
16281634

1629-
var style = '', appliedStyles = [];
1630-
if (timings.transitionDuration > 0) {
1631-
var propertyStyle = timings.transitionPropertyStyle;
1632-
if (propertyStyle.indexOf('all') == -1) {
1633-
style += CSS_PREFIX + 'transition-property: ' + propertyStyle + ';';
1634-
style += CSS_PREFIX + 'transition-duration: ' + timings.transitionDurationStyle + ';';
1635-
appliedStyles.push(CSS_PREFIX + 'transition-property');
1636-
appliedStyles.push(CSS_PREFIX + 'transition-duration');
1637-
}
1638-
}
1639-
1640-
if (itemIndex > 0) {
1641-
if (stagger.transitionDelay > 0 && stagger.transitionDuration === 0) {
1642-
var delayStyle = timings.transitionDelayStyle;
1643-
style += CSS_PREFIX + 'transition-delay: ' +
1644-
prepareStaggerDelay(delayStyle, stagger.transitionDelay, itemIndex) + '; ';
1645-
appliedStyles.push(CSS_PREFIX + 'transition-delay');
1646-
}
1647-
1648-
if (stagger.animationDelay > 0 && stagger.animationDuration === 0) {
1649-
style += CSS_PREFIX + 'animation-delay: ' +
1650-
prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, itemIndex) + '; ';
1651-
appliedStyles.push(CSS_PREFIX + 'animation-delay');
1652-
}
1653-
}
1654-
16551635
if (appliedStyles.length > 0) {
16561636
//the element being animated may sometimes contain comment nodes in
16571637
//the jqLite object, so we're safe to use a single variable to house
16581638
//the styles since there is always only one element being animated
16591639
var oldStyle = node.getAttribute('style') || '';
1660-
node.setAttribute('style', oldStyle + '; ' + style);
1640+
if (oldStyle.charAt(oldStyle.length-1) !== ';') {
1641+
oldStyle += ';';
1642+
}
1643+
node.setAttribute('style', oldStyle + ' ' + style);
16611644
}
16621645

16631646
var startTime = Date.now();
16641647
var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT;
1648+
var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER;
1649+
var totalTime = (staggerTime + animationTime) * ONE_SECOND;
1650+
1651+
var staggerTimeout;
1652+
if (staggerTime > 0) {
1653+
element.addClass(pendingClassName);
1654+
staggerTimeout = $timeout(function() {
1655+
staggerTimeout = null;
1656+
element.addClass(activeClassName);
1657+
element.removeClass(pendingClassName);
1658+
if (timings.animationDuration > 0) {
1659+
blockAnimations(node, false);
1660+
}
1661+
}, staggerTime * ONE_SECOND, false);
1662+
}
16651663

16661664
element.on(css3AnimationEvents, onAnimationProgress);
16671665
elementData.closeAnimationFns.push(function() {
16681666
onEnd();
16691667
activeAnimationComplete();
16701668
});
16711669

1672-
var staggerTime = itemIndex * (Math.max(stagger.animationDelay, stagger.transitionDelay) || 0);
1673-
var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER;
1674-
var totalTime = (staggerTime + animationTime) * ONE_SECOND;
1675-
16761670
elementData.running++;
16771671
animationCloseHandler(element, totalTime);
16781672
return onEnd;
@@ -1683,6 +1677,10 @@ angular.module('ngAnimate', ['ng'])
16831677
function onEnd(cancelled) {
16841678
element.off(css3AnimationEvents, onAnimationProgress);
16851679
element.removeClass(activeClassName);
1680+
element.removeClass(pendingClassName);
1681+
if (staggerTimeout) {
1682+
$timeout.cancel(staggerTimeout);
1683+
}
16861684
animateClose(element, className);
16871685
var node = extractElementNode(element);
16881686
for (var i in appliedStyles) {
@@ -1712,13 +1710,12 @@ angular.module('ngAnimate', ['ng'])
17121710
}
17131711
}
17141712

1715-
function prepareStaggerDelay(delayStyle, staggerDelay, index) {
1716-
var style = '';
1717-
forEach(delayStyle.split(','), function(val, i) {
1718-
style += (i > 0 ? ',' : '') +
1719-
(index * staggerDelay + parseInt(val, 10)) + 's';
1720-
});
1721-
return style;
1713+
function blockTransitions(node, bool) {
1714+
node.style[TRANSITION_PROP + PROPERTY_KEY] = bool ? 'none' : '';
1715+
}
1716+
1717+
function blockAnimations(node, bool) {
1718+
node.style[ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY] = bool ? 'paused' : '';
17221719
}
17231720

17241721
function animateBefore(animationEvent, element, className, calculationDecorator) {

0 commit comments

Comments
 (0)