From ceac435f02694b4e278b8d744ee7f71d90d4da71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Fri, 15 Jan 2016 09:16:08 -0800 Subject: [PATCH 1/2] fix(ngAnimate): only trigger animations if the document is not hidden Prior to this fix, ngAnimate would always trigger animations even if the browser tab or browser window is not in view. This issue was important to fix because browsers do not flush calls to requestAnimationFrame when they are not active. This fix ensures that ngAnimate will respect `document.hidden` in order to get around this. Closes #12842 --- src/ngAnimate/animateQueue.js | 3 ++- test/ngAnimate/animateSpec.js | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/ngAnimate/animateQueue.js b/src/ngAnimate/animateQueue.js index 6432f80936c..39669a99491 100644 --- a/src/ngAnimate/animateQueue.js +++ b/src/ngAnimate/animateQueue.js @@ -337,7 +337,8 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // this is a hard disable of all animations for the application or on // the element itself, therefore there is no need to continue further // past this point if not enabled - var skipAnimations = !animationsEnabled || disabledElementsLookup.get(node); + var doc = $document[0]; + var skipAnimations = !animationsEnabled || doc.hidden || disabledElementsLookup.get(node); var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {}; var hasExistingAnimation = !!existingAnimation.state; diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 09e78af4c13..a68297e984f 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -148,6 +148,31 @@ describe("animations", function() { expect(copiedOptions).toEqual(initialOptions); })); + it("should skip animations entirely if the document is not active", function() { + var doc; + + module(function($provide) { + doc = jqLite({ + body: document.body, + hidden: true + }); + $provide.value('$document', doc); + }); + + inject(function($animate, $rootScope) { + $animate.enter(element, parent); + $rootScope.$digest(); + expect(capturedAnimation).toBeFalsy(); + expect(element[0].parentNode).toEqual(parent[0]); + + doc[0].hidden = false; + + $animate.leave(element); + $rootScope.$digest(); + expect(capturedAnimation).toBeTruthy(); + }); + }); + it('should animate only the specified CSS className matched within $animateProvider.classNameFilter', function() { module(function($animateProvider) { $animateProvider.classNameFilter(/only-allow-this-animation/); From ab60b101a1701e7b06a3b73de935d9c72ab24282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Fri, 15 Jan 2016 09:43:50 -0800 Subject: [PATCH 2/2] fix(ngAnimate): ensure that animate promises resolve when the browser is hidden Prior to this fix any promise/callback chained on a call to the $animate methods would only flush if and when the browser page is visible. This fix ensures that a timeout will be used instead when the browser page is hidden. --- src/ng/animateRunner.js | 19 ++++++++++++++---- test/ng/animateRunnerSpec.js | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/ng/animateRunner.js b/src/ng/animateRunner.js index 51701b4c16f..0b6cacc8d6f 100644 --- a/src/ng/animateRunner.js +++ b/src/ng/animateRunner.js @@ -28,8 +28,8 @@ var $$AnimateAsyncRunFactoryProvider = function() { }; var $$AnimateRunnerFactoryProvider = function() { - this.$get = ['$q', '$sniffer', '$$animateAsyncRun', - function($q, $sniffer, $$animateAsyncRun) { + this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$document', '$timeout', + function($q, $sniffer, $$animateAsyncRun, $document, $timeout) { var INITIAL_STATE = 0; var DONE_PENDING_STATE = 1; @@ -74,8 +74,19 @@ var $$AnimateRunnerFactoryProvider = function() { function AnimateRunner(host) { this.setHost(host); + var rafTick = $$animateAsyncRun(); + var timeoutTick = function(fn) { + $timeout(fn, 0, false); + }; + this._doneCallbacks = []; - this._runInAnimationFrame = $$animateAsyncRun(); + this._tick = function(fn) { + if ($document[0].hidden) { + timeoutTick(fn); + } else { + rafTick(fn); + } + }; this._state = 0; } @@ -148,7 +159,7 @@ var $$AnimateRunnerFactoryProvider = function() { var self = this; if (self._state === INITIAL_STATE) { self._state = DONE_PENDING_STATE; - self._runInAnimationFrame(function() { + self._tick(function() { self._resolve(response); }); } diff --git a/test/ng/animateRunnerSpec.js b/test/ng/animateRunnerSpec.js index d6fab470e8d..f8bcf068457 100644 --- a/test/ng/animateRunnerSpec.js +++ b/test/ng/animateRunnerSpec.js @@ -162,6 +162,43 @@ describe("$$AnimateRunner", function() { expect(animationFailed).toBe(true); })); + it("should revert to using timeouts when the webpage is not visible", function() { + var doc; + + module(function($provide) { + doc = jqLite({ + body: document.body, + hidden: true + }); + $provide.value('$document', doc); + }); + + inject(function($$AnimateRunner, $rootScope, $$rAF, $timeout) { + var spy = jasmine.createSpy(); + var runner = new $$AnimateRunner(); + runner.done(spy); + runner.complete(true); + expect(spy).not.toHaveBeenCalled(); + $$rAF.flush(); + expect(spy).not.toHaveBeenCalled(); + $timeout.flush(); + expect(spy).toHaveBeenCalled(); + + doc[0].hidden = false; + + spy = jasmine.createSpy(); + runner = new $$AnimateRunner(); + runner.done(spy); + runner.complete(true); + expect(spy).not.toHaveBeenCalled(); + $$rAF.flush(); + expect(spy).toHaveBeenCalled(); + expect(function() { + $timeout.flush(); + }).toThrow(); + }); + }); + they("should expose the `finally` promise function to handle the final state when $prop", { 'rejected': 'cancel', 'resolved': 'end' }, function(method) { inject(function($$AnimateRunner, $rootScope) {