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

Commit 32d3cbb

Browse files
committed
fix(ngAnimate): ensure that parent class-based animations are never closed by their children
This fix ensures that a structural child animation will never close a parent class based early so that the CSS classes for the child are ready for it to perform its CSS animation. The reasoning for the past for this was because their is a one frame delay before the classes were applied. If a parent and a child animation happen at the same time then the animations may not be picked up for the element since the CSS classes may not have been applied yet. This fix ensures that parent CSS classes are applied in a synchronous manner without the need to run a one RAF wait. The solution to this was to apply the preparation classes during the pre-digest phase and then apply the CSS classes right after with a forced reflow paint. BREAKING CHANGE: CSS classes added/removed by ngAnimate are now applied synchronously once the first digest has passed. The previous behavior involved ngAnimate having to wait for one requestAnimationFrame before CSS classes were added/removed. The CSS classes are now applied directly after the first digest that is triggered after `$animate.addClass`, `$animate.removeClass` or `$animate.setClass` is called. If any of your code relies on waiting for one frame before checking for CSS classes on the element then please change this behavior. If a parent class-based animation, however, is run through a JavaScript animation which triggers an animation for `beforeAddClass` and/or `beforeRemoveClass` then the CSS classes will not be applied in time for the children (and the parent class-based animation will not be cancelled by any child animations). Closes #11975 Closes #12276
1 parent acc53ce commit 32d3cbb

11 files changed

+875
-254
lines changed

src/ngAnimate/.jshintrc

+29-1
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,30 @@
2020
"isElement": false,
2121

2222
"ELEMENT_NODE": false,
23+
"COMMENT_NODE": false,
2324
"NG_ANIMATE_CLASSNAME": false,
2425
"NG_ANIMATE_CHILDREN_DATA": false,
2526

27+
"ADD_CLASS_SUFFIX": false,
28+
"REMOVE_CLASS_SUFFIX": false,
29+
"EVENT_CLASS_PREFIX": false,
30+
"ACTIVE_CLASS_SUFFIX": false,
31+
32+
"TRANSITION_DURATION_PROP": false,
33+
"TRANSITION_DELAY_PROP": false,
34+
"TRANSITION_PROP": false,
35+
"PROPERTY_KEY": false,
36+
"DURATION_KEY": false,
37+
"DELAY_KEY": false,
38+
"TIMING_KEY": false,
39+
"ANIMATION_DURATION_PROP": false,
40+
"ANIMATION_DELAY_PROP": false,
41+
"ANIMATION_PROP": false,
42+
"ANIMATION_ITERATION_COUNT_KEY": false,
43+
"SAFE_FAST_FORWARD_DURATION_VALUE": false,
44+
"TRANSITIONEND_EVENT": false,
45+
"ANIMATIONEND_EVENT": false,
46+
2647
"assertArg": false,
2748
"isPromiseLike": false,
2849
"mergeClasses": false,
@@ -38,6 +59,13 @@
3859
"removeFromArray": false,
3960
"stripCommentsFromElement": false,
4061
"extractElementNode": false,
41-
"getDomNode": false
62+
"getDomNode": false,
63+
64+
"applyGeneratedPreparationClasses": false,
65+
"clearGeneratedClasses": false,
66+
"blockTransitions": false,
67+
"blockKeyframeAnimations": false,
68+
"applyInlineStyle": false,
69+
"concatWithSpace": false
4270
}
4371
}

src/ngAnimate/animateCss.js

+21-96
Original file line numberDiff line numberDiff line change
@@ -208,55 +208,11 @@
208208
* * `start` - The method to start the animation. This will return a `Promise` when called.
209209
* * `end` - This method will cancel the animation and remove all applied CSS classes and styles.
210210
*/
211-
212-
// Detect proper transitionend/animationend event names.
213-
var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT;
214-
215-
// If unprefixed events are not supported but webkit-prefixed are, use the latter.
216-
// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them.
217-
// Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend`
218-
// but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`.
219-
// Register both events in case `window.onanimationend` is not supported because of that,
220-
// do the same for `transitionend` as Safari is likely to exhibit similar behavior.
221-
// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit
222-
// therefore there is no reason to test anymore for other vendor prefixes:
223-
// http://caniuse.com/#search=transition
224-
if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) {
225-
CSS_PREFIX = '-webkit-';
226-
TRANSITION_PROP = 'WebkitTransition';
227-
TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
228-
} else {
229-
TRANSITION_PROP = 'transition';
230-
TRANSITIONEND_EVENT = 'transitionend';
231-
}
232-
233-
if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) {
234-
CSS_PREFIX = '-webkit-';
235-
ANIMATION_PROP = 'WebkitAnimation';
236-
ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend';
237-
} else {
238-
ANIMATION_PROP = 'animation';
239-
ANIMATIONEND_EVENT = 'animationend';
240-
}
241-
242-
var DURATION_KEY = 'Duration';
243-
var PROPERTY_KEY = 'Property';
244-
var DELAY_KEY = 'Delay';
245-
var TIMING_KEY = 'TimingFunction';
246-
var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
247-
var ANIMATION_PLAYSTATE_KEY = 'PlayState';
248-
var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
249-
var CLOSING_TIME_BUFFER = 1.5;
250211
var ONE_SECOND = 1000;
251212
var BASE_TEN = 10;
252213

253-
var SAFE_FAST_FORWARD_DURATION_VALUE = 9999;
254-
255-
var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY;
256-
var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY;
257-
258-
var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY;
259-
var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY;
214+
var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
215+
var CLOSING_TIME_BUFFER = 1.5;
260216

261217
var DETECT_CSS_PROPERTIES = {
262218
transitionDuration: TRANSITION_DURATION_PROP,
@@ -274,6 +230,15 @@ var DETECT_STAGGER_CSS_PROPERTIES = {
274230
animationDelay: ANIMATION_DELAY_PROP
275231
};
276232

233+
function getCssKeyframeDurationStyle(duration) {
234+
return [ANIMATION_DURATION_PROP, duration + 's'];
235+
}
236+
237+
function getCssDelayStyle(delay, isKeyframeAnimation) {
238+
var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP;
239+
return [prop, delay + 's'];
240+
}
241+
277242
function computeCssStyles($window, element, properties) {
278243
var styles = Object.create(null);
279244
var detectedStyles = $window.getComputedStyle(element) || {};
@@ -330,37 +295,6 @@ function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
330295
return [style, value];
331296
}
332297

333-
function getCssKeyframeDurationStyle(duration) {
334-
return [ANIMATION_DURATION_PROP, duration + 's'];
335-
}
336-
337-
function getCssDelayStyle(delay, isKeyframeAnimation) {
338-
var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP;
339-
return [prop, delay + 's'];
340-
}
341-
342-
function blockTransitions(node, duration) {
343-
// we use a negative delay value since it performs blocking
344-
// yet it doesn't kill any existing transitions running on the
345-
// same element which makes this safe for class-based animations
346-
var value = duration ? '-' + duration + 's' : '';
347-
applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]);
348-
return [TRANSITION_DELAY_PROP, value];
349-
}
350-
351-
function blockKeyframeAnimations(node, applyBlock) {
352-
var value = applyBlock ? 'paused' : '';
353-
var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY;
354-
applyInlineStyle(node, [key, value]);
355-
return [key, value];
356-
}
357-
358-
function applyInlineStyle(node, styleTuple) {
359-
var prop = styleTuple[0];
360-
var value = styleTuple[1];
361-
node.style[prop] = value;
362-
}
363-
364298
function createLocalCacheLookup() {
365299
var cache = Object.create(null);
366300
return {
@@ -392,10 +326,8 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
392326
var gcsLookup = createLocalCacheLookup();
393327
var gcsStaggerLookup = createLocalCacheLookup();
394328

395-
this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
396-
'$document', '$sniffer', '$$rAF',
397-
function($window, $$jqLite, $$AnimateRunner, $timeout,
398-
$document, $sniffer, $$rAF) {
329+
this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', '$$forceReflow', '$sniffer', '$$rAF',
330+
function($window, $$jqLite, $$AnimateRunner, $timeout, $$forceReflow, $sniffer, $$rAF) {
399331

400332
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
401333

@@ -452,7 +384,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
452384
return stagger || {};
453385
}
454386

455-
var bod = getDomNode($document).body;
456387
var cancelLastRAFRequest;
457388
var rafWaitQueue = [];
458389
function waitUntilQuiet(callback) {
@@ -465,20 +396,14 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
465396
gcsLookup.flush();
466397
gcsStaggerLookup.flush();
467398

468-
//the line below will force the browser to perform a repaint so
469-
//that all the animated elements within the animation frame will
470-
//be properly updated and drawn on screen. This is required to
471-
//ensure that the preparation animation is properly flushed so that
472-
//the active state picks up from there. DO NOT REMOVE THIS LINE.
473-
//DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH
474-
//WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND
475-
//WILL TAKE YEARS AWAY FROM YOUR LIFE.
476-
var width = bod.offsetWidth + 1;
399+
// DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
400+
// PLEASE EXAMINE THE `$$forceReflow` service to understand why.
401+
var pageWidth = $$forceReflow();
477402

478403
// we use a for loop to ensure that if the queue is changed
479404
// during this looping then it will consider new requests
480405
for (var i = 0; i < rafWaitQueue.length; i++) {
481-
rafWaitQueue[i](width);
406+
rafWaitQueue[i](pageWidth);
482407
}
483408
rafWaitQueue.length = 0;
484409
});
@@ -534,20 +459,20 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
534459
var addRemoveClassName = '';
535460

536461
if (isStructural) {
537-
structuralClassName = pendClasses(method, 'ng-', true);
462+
structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true);
538463
} else if (method) {
539464
structuralClassName = method;
540465
}
541466

542467
if (options.addClass) {
543-
addRemoveClassName += pendClasses(options.addClass, '-add');
468+
addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX);
544469
}
545470

546471
if (options.removeClass) {
547472
if (addRemoveClassName.length) {
548473
addRemoveClassName += ' ';
549474
}
550-
addRemoveClassName += pendClasses(options.removeClass, '-remove');
475+
addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX);
551476
}
552477

553478
// there may be a situation where a structural animation is combined together
@@ -563,7 +488,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
563488

564489
var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim();
565490
var fullClassName = classes + ' ' + preparationClasses;
566-
var activeClasses = pendClasses(preparationClasses, '-active');
491+
var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
567492
var hasToStyles = styles.to && Object.keys(styles.to).length > 0;
568493
var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0;
569494

src/ngAnimate/animateCssDriver.js

+32-14
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
99
var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out';
1010
var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in';
1111

12-
this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$$body', '$sniffer',
13-
function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $$body, $sniffer) {
12+
this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$$body', '$sniffer', '$$jqLite',
13+
function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $$body, $sniffer, $$jqLite) {
1414

1515
// only browsers that support these properties can render animations
1616
if (!$sniffer.animations && !$sniffer.transitions) return noop;
@@ -20,13 +20,15 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
2020

2121
var rootBodyElement = jqLite(bodyNode.parentNode === rootNode ? bodyNode : rootNode);
2222

23-
return function initDriverFn(animationDetails) {
23+
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
24+
25+
return function initDriverFn(animationDetails, onBeforeClassesAppliedCb) {
2426
return animationDetails.from && animationDetails.to
2527
? prepareFromToAnchorAnimation(animationDetails.from,
2628
animationDetails.to,
2729
animationDetails.classes,
2830
animationDetails.anchors)
29-
: prepareRegularAnimation(animationDetails);
31+
: prepareRegularAnimation(animationDetails, onBeforeClassesAppliedCb);
3032
};
3133

3234
function filterCssClasses(classes) {
@@ -170,8 +172,8 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
170172
}
171173

172174
function prepareFromToAnchorAnimation(from, to, classes, anchors) {
173-
var fromAnimation = prepareRegularAnimation(from);
174-
var toAnimation = prepareRegularAnimation(to);
175+
var fromAnimation = prepareRegularAnimation(from, noop);
176+
var toAnimation = prepareRegularAnimation(to, noop);
175177

176178
var anchorAnimations = [];
177179
forEach(anchors, function(anchor) {
@@ -222,24 +224,40 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
222224
};
223225
}
224226

225-
function prepareRegularAnimation(animationDetails) {
227+
function prepareRegularAnimation(animationDetails, onBeforeClassesAppliedCb) {
226228
var element = animationDetails.element;
227229
var options = animationDetails.options || {};
228230

231+
// since the ng-EVENT, class-ADD and class-REMOVE classes are applied inside
232+
// of the animateQueue pre and postDigest stages then there is no need to add
233+
// then them here as well.
234+
options.$$skipPreparationClasses = true;
235+
236+
// during the pre/post digest stages inside of animateQueue we also performed
237+
// the blocking (transition:-9999s) so there is no point in doing that again.
238+
options.skipBlocking = true;
239+
229240
if (animationDetails.structural) {
230-
// structural animations ensure that the CSS classes are always applied
231-
// before the detection starts.
232-
options.structural = options.applyClassesEarly = true;
241+
options.event = animationDetails.event;
233242

234243
// we special case the leave animation since we want to ensure that
235244
// the element is removed as soon as the animation is over. Otherwise
236245
// a flicker might appear or the element may not be removed at all
237-
options.event = animationDetails.event;
238-
if (options.event === 'leave') {
246+
if (animationDetails.event === 'leave') {
239247
options.onDone = options.domOperation;
240248
}
241-
} else {
242-
options.event = null;
249+
}
250+
251+
// we apply the classes right away since the pre-digest took care of the
252+
// preparation classes.
253+
onBeforeClassesAppliedCb(element);
254+
applyAnimationClasses(element, options);
255+
256+
// We assign the preparationClasses as the actual animation event since
257+
// the internals of $animateCss will just suffix the event token values
258+
// with `-active` to trigger the animation.
259+
if (options.preparationClasses) {
260+
options.event = concatWithSpace(options.event, options.preparationClasses);
243261
}
244262

245263
var animator = $animateCss(element, options);

0 commit comments

Comments
 (0)