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 6432f80936c6..39669a99491b 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 09e78af4c132..a68297e984fb 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 51701b4c16f8..0b6cacc8d6f0 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 d6fab470e8df..f8bcf0684579 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) {