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

Commit

Permalink
fix(ngAnimate): ensure that parent class-based animations are never c…
Browse files Browse the repository at this point in the history
…losed 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
  • Loading branch information
matsko committed Jul 20, 2015
1 parent acc53ce commit 32d3cbb
Show file tree
Hide file tree
Showing 11 changed files with 875 additions and 254 deletions.
30 changes: 29 additions & 1 deletion src/ngAnimate/.jshintrc
Expand Up @@ -20,9 +20,30 @@
"isElement": false,

"ELEMENT_NODE": false,
"COMMENT_NODE": false,
"NG_ANIMATE_CLASSNAME": false,
"NG_ANIMATE_CHILDREN_DATA": false,

"ADD_CLASS_SUFFIX": false,
"REMOVE_CLASS_SUFFIX": false,
"EVENT_CLASS_PREFIX": false,
"ACTIVE_CLASS_SUFFIX": false,

"TRANSITION_DURATION_PROP": false,
"TRANSITION_DELAY_PROP": false,
"TRANSITION_PROP": false,
"PROPERTY_KEY": false,
"DURATION_KEY": false,
"DELAY_KEY": false,
"TIMING_KEY": false,
"ANIMATION_DURATION_PROP": false,
"ANIMATION_DELAY_PROP": false,
"ANIMATION_PROP": false,
"ANIMATION_ITERATION_COUNT_KEY": false,
"SAFE_FAST_FORWARD_DURATION_VALUE": false,
"TRANSITIONEND_EVENT": false,
"ANIMATIONEND_EVENT": false,

"assertArg": false,
"isPromiseLike": false,
"mergeClasses": false,
Expand All @@ -38,6 +59,13 @@
"removeFromArray": false,
"stripCommentsFromElement": false,
"extractElementNode": false,
"getDomNode": false
"getDomNode": false,

"applyGeneratedPreparationClasses": false,
"clearGeneratedClasses": false,
"blockTransitions": false,
"blockKeyframeAnimations": false,
"applyInlineStyle": false,
"concatWithSpace": false
}
}
117 changes: 21 additions & 96 deletions src/ngAnimate/animateCss.js
Expand Up @@ -208,55 +208,11 @@
* * `start` - The method to start the animation. This will return a `Promise` when called.
* * `end` - This method will cancel the animation and remove all applied CSS classes and styles.
*/

// Detect proper transitionend/animationend event names.
var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT;

// If unprefixed events are not supported but webkit-prefixed are, use the latter.
// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them.
// Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend`
// but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`.
// Register both events in case `window.onanimationend` is not supported because of that,
// do the same for `transitionend` as Safari is likely to exhibit similar behavior.
// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit
// therefore there is no reason to test anymore for other vendor prefixes:
// http://caniuse.com/#search=transition
if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) {
CSS_PREFIX = '-webkit-';
TRANSITION_PROP = 'WebkitTransition';
TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
} else {
TRANSITION_PROP = 'transition';
TRANSITIONEND_EVENT = 'transitionend';
}

if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) {
CSS_PREFIX = '-webkit-';
ANIMATION_PROP = 'WebkitAnimation';
ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend';
} else {
ANIMATION_PROP = 'animation';
ANIMATIONEND_EVENT = 'animationend';
}

var DURATION_KEY = 'Duration';
var PROPERTY_KEY = 'Property';
var DELAY_KEY = 'Delay';
var TIMING_KEY = 'TimingFunction';
var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
var ANIMATION_PLAYSTATE_KEY = 'PlayState';
var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
var CLOSING_TIME_BUFFER = 1.5;
var ONE_SECOND = 1000;
var BASE_TEN = 10;

var SAFE_FAST_FORWARD_DURATION_VALUE = 9999;

var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY;
var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY;

var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY;
var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY;
var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
var CLOSING_TIME_BUFFER = 1.5;

var DETECT_CSS_PROPERTIES = {
transitionDuration: TRANSITION_DURATION_PROP,
Expand All @@ -274,6 +230,15 @@ var DETECT_STAGGER_CSS_PROPERTIES = {
animationDelay: ANIMATION_DELAY_PROP
};

function getCssKeyframeDurationStyle(duration) {
return [ANIMATION_DURATION_PROP, duration + 's'];
}

function getCssDelayStyle(delay, isKeyframeAnimation) {
var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP;
return [prop, delay + 's'];
}

function computeCssStyles($window, element, properties) {
var styles = Object.create(null);
var detectedStyles = $window.getComputedStyle(element) || {};
Expand Down Expand Up @@ -330,37 +295,6 @@ function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
return [style, value];
}

function getCssKeyframeDurationStyle(duration) {
return [ANIMATION_DURATION_PROP, duration + 's'];
}

function getCssDelayStyle(delay, isKeyframeAnimation) {
var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP;
return [prop, delay + 's'];
}

function blockTransitions(node, duration) {
// we use a negative delay value since it performs blocking
// yet it doesn't kill any existing transitions running on the
// same element which makes this safe for class-based animations
var value = duration ? '-' + duration + 's' : '';
applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]);
return [TRANSITION_DELAY_PROP, value];
}

function blockKeyframeAnimations(node, applyBlock) {
var value = applyBlock ? 'paused' : '';
var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY;
applyInlineStyle(node, [key, value]);
return [key, value];
}

function applyInlineStyle(node, styleTuple) {
var prop = styleTuple[0];
var value = styleTuple[1];
node.style[prop] = value;
}

function createLocalCacheLookup() {
var cache = Object.create(null);
return {
Expand Down Expand Up @@ -392,10 +326,8 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
var gcsLookup = createLocalCacheLookup();
var gcsStaggerLookup = createLocalCacheLookup();

this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
'$document', '$sniffer', '$$rAF',
function($window, $$jqLite, $$AnimateRunner, $timeout,
$document, $sniffer, $$rAF) {
this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', '$$forceReflow', '$sniffer', '$$rAF',
function($window, $$jqLite, $$AnimateRunner, $timeout, $$forceReflow, $sniffer, $$rAF) {

var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);

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

var bod = getDomNode($document).body;
var cancelLastRAFRequest;
var rafWaitQueue = [];
function waitUntilQuiet(callback) {
Expand All @@ -465,20 +396,14 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
gcsLookup.flush();
gcsStaggerLookup.flush();

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

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

if (isStructural) {
structuralClassName = pendClasses(method, 'ng-', true);
structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true);
} else if (method) {
structuralClassName = method;
}

if (options.addClass) {
addRemoveClassName += pendClasses(options.addClass, '-add');
addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX);
}

if (options.removeClass) {
if (addRemoveClassName.length) {
addRemoveClassName += ' ';
}
addRemoveClassName += pendClasses(options.removeClass, '-remove');
addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX);
}

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

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

Expand Down
46 changes: 32 additions & 14 deletions src/ngAnimate/animateCssDriver.js
Expand Up @@ -9,8 +9,8 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out';
var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in';

this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$$body', '$sniffer',
function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $$body, $sniffer) {
this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$$body', '$sniffer', '$$jqLite',
function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $$body, $sniffer, $$jqLite) {

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

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

return function initDriverFn(animationDetails) {
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);

return function initDriverFn(animationDetails, onBeforeClassesAppliedCb) {
return animationDetails.from && animationDetails.to
? prepareFromToAnchorAnimation(animationDetails.from,
animationDetails.to,
animationDetails.classes,
animationDetails.anchors)
: prepareRegularAnimation(animationDetails);
: prepareRegularAnimation(animationDetails, onBeforeClassesAppliedCb);
};

function filterCssClasses(classes) {
Expand Down Expand Up @@ -170,8 +172,8 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
}

function prepareFromToAnchorAnimation(from, to, classes, anchors) {
var fromAnimation = prepareRegularAnimation(from);
var toAnimation = prepareRegularAnimation(to);
var fromAnimation = prepareRegularAnimation(from, noop);
var toAnimation = prepareRegularAnimation(to, noop);

var anchorAnimations = [];
forEach(anchors, function(anchor) {
Expand Down Expand Up @@ -222,24 +224,40 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
};
}

function prepareRegularAnimation(animationDetails) {
function prepareRegularAnimation(animationDetails, onBeforeClassesAppliedCb) {
var element = animationDetails.element;
var options = animationDetails.options || {};

// since the ng-EVENT, class-ADD and class-REMOVE classes are applied inside
// of the animateQueue pre and postDigest stages then there is no need to add
// then them here as well.
options.$$skipPreparationClasses = true;

// during the pre/post digest stages inside of animateQueue we also performed
// the blocking (transition:-9999s) so there is no point in doing that again.
options.skipBlocking = true;

if (animationDetails.structural) {
// structural animations ensure that the CSS classes are always applied
// before the detection starts.
options.structural = options.applyClassesEarly = true;
options.event = animationDetails.event;

// we special case the leave animation since we want to ensure that
// the element is removed as soon as the animation is over. Otherwise
// a flicker might appear or the element may not be removed at all
options.event = animationDetails.event;
if (options.event === 'leave') {
if (animationDetails.event === 'leave') {
options.onDone = options.domOperation;
}
} else {
options.event = null;
}

// we apply the classes right away since the pre-digest took care of the
// preparation classes.
onBeforeClassesAppliedCb(element);
applyAnimationClasses(element, options);

// We assign the preparationClasses as the actual animation event since
// the internals of $animateCss will just suffix the event token values
// with `-active` to trigger the animation.
if (options.preparationClasses) {
options.event = concatWithSpace(options.event, options.preparationClasses);
}

var animator = $animateCss(element, options);
Expand Down

0 comments on commit 32d3cbb

Please sign in to comment.