Skip to content

Commit

Permalink
fix($animate): avoid hanging animations if the active CSS transition …
Browse files Browse the repository at this point in the history
…class is missing

Closes angular#4732
Closes angular#4490
  • Loading branch information
matsko authored and jamesdaily committed Jan 27, 2014
1 parent bd519b2 commit b3637aa
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 17 deletions.
11 changes: 11 additions & 0 deletions css/angular.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,14 @@
ng\:form {
display: block;
}

/* The styles below ensure that the CSS transition will ALWAYS
* animate and close. A nasty bug occurs with CSS transitions where
* when the active class isn't set, or if the active class doesn't
* contain any styles to transition to, then, if ngAnimate is used,
* it will appear as if the webpage is broken due to the forever hanging
* animations. The clip (!ie) and zoom (ie) CSS properties are used
* since they trigger a transition without making the browser
* animate anything and they're both highly underused CSS properties */
.ng-animate { clip:rect(1px, auto, auto, 0); -ms-zoom:1.0001; }
.ng-animate-active { clip:rect(0, auto, auto, 0); -ms-zoom:1; }
51 changes: 36 additions & 15 deletions src/ngAnimate/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -790,16 +790,22 @@ angular.module('ngAnimate', ['ng'])
if(!data) {
var transitionDuration = 0, transitionDelay = 0,
animationDuration = 0, animationDelay = 0,
transitionDelayStyle, animationDelayStyle;
transitionDelayStyle, animationDelayStyle,
transitionDurationStyle,
transitionPropertyStyle;

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

transitionDuration = Math.max(parseMaxTime(elementStyles[transitionProp + durationKey]), transitionDuration);
transitionDurationStyle = elementStyles[transitionProp + durationKey];

transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration);

if(!onlyCheckTransition) {
transitionPropertyStyle = elementStyles[transitionProp + propertyKey];

transitionDelayStyle = elementStyles[transitionProp + delayKey];

transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay);
Expand All @@ -820,12 +826,14 @@ angular.module('ngAnimate', ['ng'])
});
data = {
total : 0,
transitionPropertyStyle: transitionPropertyStyle,
transitionDurationStyle: transitionDurationStyle,
transitionDelayStyle: transitionDelayStyle,
transitionDelay : transitionDelay,
transitionDuration : transitionDuration,
transitionDelay: transitionDelay,
transitionDuration: transitionDuration,
animationDelayStyle: animationDelayStyle,
animationDelay : animationDelay,
animationDuration : animationDuration
animationDelay: animationDelay,
animationDuration: animationDuration
};
if(cacheKey) {
lookupCache[cacheKey] = data;
Expand Down Expand Up @@ -896,7 +904,7 @@ angular.module('ngAnimate', ['ng'])
node.style[transitionProp + propertyKey] = 'none';
}

var activeClassName = '';
var activeClassName = 'ng-animate-active ';
forEach(className.split(' '), function(klass, i) {
activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
});
Expand All @@ -910,25 +918,38 @@ angular.module('ngAnimate', ['ng'])
return;
}

var applyFallbackStyle, style = '';
if(timings.transitionDuration > 0) {
node.style[transitionProp + propertyKey] = '';

var propertyStyle = timings.transitionPropertyStyle;
if(propertyStyle.indexOf('all') == -1) {
applyFallbackStyle = true;
var fallbackProperty = $sniffer.msie ? '-ms-zoom' : 'clip';
style += prefix + 'transition-property: ' + propertyStyle + ', ' + fallbackProperty + '; ';
style += prefix + 'transition-duration: ' + timings.transitionDurationStyle + ', ' + timings.transitionDuration + 's; ';
}
}

if(ii > 0) {
var staggerStyle = '';
if(stagger.transitionDelay > 0 && stagger.transitionDuration === 0) {
staggerStyle += prefix + 'transition-delay: ' +
prepareStaggerDelay(timings.transitionDelayStyle, stagger.transitionDelay, ii) + '; ';
var delayStyle = timings.transitionDelayStyle;
if(applyFallbackStyle) {
delayStyle += ', ' + timings.transitionDelay + 's';
}

style += prefix + 'transition-delay: ' +
prepareStaggerDelay(delayStyle, stagger.transitionDelay, ii) + '; ';
}

if(stagger.animationDelay > 0 && stagger.animationDuration === 0) {
staggerStyle += prefix + 'animation-delay: ' +
prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, ii) + '; ';
style += prefix + 'animation-delay: ' +
prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, ii) + '; ';
}
}

if(staggerStyle.length > 0) {
formerStyle = applyStyle(node, staggerStyle);
}
if(style.length > 0) {
formerStyle = applyStyle(node, style);
}

element.addClass(activeClassName);
Expand Down
58 changes: 56 additions & 2 deletions test/ngAnimate/animateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,59 @@ describe("ngAnimate", function() {
});

describe("Transitions", function() {
it("should only apply the fallback transition property unless all properties are being animated",
inject(function($compile, $rootScope, $animate, $sniffer, $timeout) {

if (!$sniffer.animations) return;

ss.addRule('.all.ng-enter', '-webkit-transition:1s linear all;' +
'transition:1s linear all');

ss.addRule('.one.ng-enter', '-webkit-transition:1s linear color;' +
'transition:1s linear color');

var element = $compile('<div></div>')($rootScope);
var child = $compile('<div class="all">...</div>')($rootScope);
$rootElement.append(element);
var body = jqLite($document[0].body);
body.append($rootElement);

$animate.enter(child, element);
$rootScope.$digest();
$timeout.flush();

expect(child.attr('style') || '').not.toContain('transition-property');
expect(child.hasClass('ng-animate')).toBe(true);
expect(child.hasClass('ng-animate-active')).toBe(true);

browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 });
$timeout.flush();

expect(child.hasClass('ng-animate')).toBe(false);
expect(child.hasClass('ng-animate-active')).toBe(false);

child.remove();

var child2 = $compile('<div class="one">...</div>')($rootScope);

$animate.enter(child2, element);
$rootScope.$digest();
$timeout.flush();

//IE removes the -ms- prefix when placed on the style
var fallbackProperty = $sniffer.msie ? 'zoom' : 'clip';
var regExp = new RegExp("transition-property:\\s+color\\s*,\\s*" + fallbackProperty + "\\s*;");
expect(child2.attr('style') || '').toMatch(regExp);
expect(child2.hasClass('ng-animate')).toBe(true);
expect(child2.hasClass('ng-animate-active')).toBe(true);

browserTrigger(child2,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 });
$timeout.flush();

expect(child2.hasClass('ng-animate')).toBe(false);
expect(child2.hasClass('ng-animate-active')).toBe(false);
}));

it("should skip transitions if disabled and run when enabled",
inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {

Expand Down Expand Up @@ -1013,7 +1066,7 @@ describe("ngAnimate", function() {
$rootScope.$digest();
$timeout.flush();

expect(elements[0].attr('style')).toBeFalsy();
expect(elements[0].attr('style')).not.toContain('transition-delay');
expect(elements[1].attr('style')).toMatch(/transition-delay: 2\.1\d*s,\s*4\.1\d*s/);
expect(elements[2].attr('style')).toMatch(/transition-delay: 2\.2\d*s,\s*4\.2\d*s/);
expect(elements[3].attr('style')).toMatch(/transition-delay: 2\.3\d*s,\s*4\.3\d*s/);
Expand All @@ -1030,7 +1083,7 @@ describe("ngAnimate", function() {
ss.addRule('.ani.ng-enter, .ani.ng-leave',
'-webkit-animation:my_animation 1s 1s, your_animation 1s 2s;' +
'animation:my_animation 1s 1s, your_animation 1s 2s;' +
'-webkit-transition:1s linear all 0s;' +
'-webkit-transition:1s linear all 1s;' +
'transition:1s linear all 1s;');

ss.addRule('.ani.ng-enter-stagger, .ani.ng-leave-stagger',
Expand Down Expand Up @@ -1731,6 +1784,7 @@ describe("ngAnimate", function() {
expect(child.css(propertyKey)).toBe('background-color');
child.remove();

child = $compile('<div class="ani">...</div>')($rootScope);
child.attr('class','trans');
$animate.enter(child, element);
$rootScope.$digest();
Expand Down

0 comments on commit b3637aa

Please sign in to comment.