Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃悰 Stories: Disable animations when user prefers-reduced-motion #34081

Merged
merged 8 commits into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 2 additions & 10 deletions extensions/amp-story-panning-media/0.1/amp-story-panning-media.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {Services} from '../../../src/services';
import {closest, whenUpgradedToCustomElement} from '../../../src/dom';
import {deepEquals} from '../../../src/json';
import {dev, user} from '../../../src/log';
import {prefersReducedMotion} from '../../../src/utils/media-query-props';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Potentially followup with moving this to src/core.

import {setImportantStyles} from '../../../src/style';

/** @const {string} */
Expand Down Expand Up @@ -250,7 +251,7 @@ export class AmpStoryPanningMedia extends AMP.BaseElement {
];

// Don't animate if first instance of group or prefers-reduced-motion.
if (!startPos || this.prefersReducedMotion_()) {
if (!startPos || prefersReducedMotion(this.win)) {
this.storeService_.dispatch(Action.ADD_PANNING_MEDIA_STATE, {
[this.groupId_]: this.animateTo_,
});
Expand Down Expand Up @@ -352,15 +353,6 @@ export class AmpStoryPanningMedia extends AMP.BaseElement {
);
}

/**
* Whether the device opted in prefers-reduced-motion.
* @return {boolean}
* @private
*/
prefersReducedMotion_() {
return this.win.matchMedia('(prefers-reduced-motion: reduce)')?.matches;
}

/** @override */
isLayoutSupported(layout) {
return layout == Layout.FILL;
Expand Down
59 changes: 20 additions & 39 deletions extensions/amp-story/1.0/amp-story-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import {isExperimentOn} from '../../../src/experiments';
import {isPrerenderActivePage} from './prerender-active-page';
import {listen} from '../../../src/event-helper';
import {CSS as pageAttachmentCSS} from '../../../build/amp-story-open-page-attachment-0.1.css';
import {prefersReducedMotion} from '../../../src/utils/media-query-props';
import {px, toggle} from '../../../src/style';
import {renderPageAttachmentUI} from './amp-story-open-page-attachment';
import {renderPageDescription} from './semantic-render';
Expand Down Expand Up @@ -308,17 +309,20 @@ export class AmpStoryPage extends AMP.BaseElement {
* @private
*/
maybeCreateAnimationManager_() {
if (!this.animationManager_) {
if (!hasAnimations(this.element)) {
return;
}

this.animationManager_ = AnimationManager.create(
this.element,
this.getAmpDoc(),
this.getAmpDoc().getUrl()
);
if (this.animationManager_) {
return;
}
if (prefersReducedMotion(this.win)) {
return;
}
if (!hasAnimations(this.element)) {
return;
}
this.animationManager_ = AnimationManager.create(
this.element,
this.getAmpDoc(),
this.getAmpDoc().getUrl()
);
}

/** @override */
Expand Down Expand Up @@ -479,9 +483,7 @@ export class AmpStoryPage extends AMP.BaseElement {
if (this.state_ === PageState.PAUSED) {
this.advancement_.start();
this.playAllMedia_();
if (this.animationManager_) {
this.animationManager_.resumeAll();
}
this.animationManager_?.resumeAll();
}

this.state_ = state;
Expand All @@ -493,9 +495,7 @@ export class AmpStoryPage extends AMP.BaseElement {
const canResume = !this.storeService_.get(StateProperty.BOOKEND_STATE);
this.advancement_.stop(canResume);
this.pauseAllMedia_(false /** rewindToBeginning */);
if (this.animationManager_) {
this.animationManager_.pauseAll();
}
this.animationManager_?.pauseAll();
this.state_ = state;
break;
default:
Expand Down Expand Up @@ -533,9 +533,7 @@ export class AmpStoryPage extends AMP.BaseElement {
this.muteAllMedia();
}

if (this.animationManager_) {
this.animationManager_.cancelAll();
}
this.animationManager_?.cancelAll();
}

/**
Expand Down Expand Up @@ -564,9 +562,7 @@ export class AmpStoryPage extends AMP.BaseElement {
});
});
});
this.prefersReducedMotion_()
? this.maybeFinishAnimations_()
: this.maybeStartAnimations_();
gmajoulet marked this conversation as resolved.
Show resolved Hide resolved
this.maybeStartAnimations_();
this.checkPageHasAudio_();
this.checkPageHasElementWithPlayback_();
this.renderOpenAttachmentUI_();
Expand Down Expand Up @@ -1208,10 +1204,7 @@ export class AmpStoryPage extends AMP.BaseElement {
* @private
*/
maybeStartAnimations_() {
if (!this.animationManager_) {
return;
}
this.animationManager_.animateIn();
this.animationManager_?.animateIn();
}

/**
Expand All @@ -1231,23 +1224,11 @@ export class AmpStoryPage extends AMP.BaseElement {
});
}

/**
* Whether the device opted in prefers-reduced-motion.
* @return {boolean}
* @private
*/
prefersReducedMotion_() {
return this.win.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

/**
* @return {!Promise}
*/
maybeApplyFirstAnimationFrame() {
if (!this.animationManager_) {
return Promise.resolve();
}
return this.animationManager_.applyFirstFrame();
return Promise.resolve(this.animationManager_?.applyFirstFrame());
}

/**
Expand Down
30 changes: 21 additions & 9 deletions extensions/amp-story/1.0/test/test-amp-story-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import * as MediaQueryProps from '../../../../src/utils/media-query-props';
import * as VideoUtils from '../../../../src/utils/video';
import {Action, AmpStoryStoreService} from '../amp-story-store-service';
import {AmpAudio} from '../../../amp-audio/0.1/amp-audio';
Expand All @@ -29,6 +30,7 @@ import {
createElementWithAttributes,
scopedQuerySelectorAll,
} from '../../../../src/dom';
import {htmlFor} from '../../../../src/static-template';
import {installFriendlyIframeEmbed} from '../../../../src/friendly-iframe-embed';
import {registerServiceBuilder} from '../../../../src/service';
import {toggleExperiment} from '../../../../src/experiments';
Expand All @@ -38,6 +40,7 @@ const extensions = ['amp-story:1.0', 'amp-audio'];
describes.realWin('amp-story-page', {amp: {extensions}}, (env) => {
let win;
let element;
let html;
let gridLayerEl;
let page;
let storeService;
Expand All @@ -49,6 +52,8 @@ describes.realWin('amp-story-page', {amp: {extensions}}, (env) => {
win = env.win;
isPerformanceTrackingOn = false;

html = htmlFor(win.document);

const mediaPoolRoot = {
getElement: () => win.document.createElement('div'),
getMaxMediaElementCounts: () => ({
Expand Down Expand Up @@ -107,16 +112,27 @@ describes.realWin('amp-story-page', {amp: {extensions}}, (env) => {
});

it('should build the animation manager if an element is animated', async () => {
// Adding an element that has to be animated.
const animatedEl = win.document.createElement('div');
animatedEl.setAttribute('animate-in', 'fade-in');
const animatedEl = html`<div animate-in="fade-in"></div>`;

element.appendChild(animatedEl);
element.getAmpDoc = () => new AmpDocSingle(win);

page.buildCallback();
expect(page.animationManager_).to.exist;
});

it('should not build the animation manager if `prefers-reduced-motion` is on', async () => {
env.sandbox.stub(MediaQueryProps, 'prefersReducedMotion').returns(true);

const animatedEl = html`<div animate-in="fade-in"></div>`;

element.appendChild(animatedEl);
element.getAmpDoc = () => new AmpDocSingle(win);

page.buildCallback();
expect(page.animationManager_).to.be.null;
});

it('should set an active attribute when state becomes active', async () => {
page.buildCallback();
await page.layoutCallback();
Expand Down Expand Up @@ -165,9 +181,7 @@ describes.realWin('amp-story-page', {amp: {extensions}}, (env) => {
});

it('should start the animations if needed when state becomes active', async () => {
// Adding an element that has to be animated.
const animatedEl = win.document.createElement('div');
animatedEl.setAttribute('animate-in', 'fade-in');
const animatedEl = html`<div animate-in="fade-in"></div>`;
element.appendChild(animatedEl);

page.buildCallback();
Expand Down Expand Up @@ -429,9 +443,7 @@ describes.realWin('amp-story-page', {amp: {extensions}}, (env) => {
});

it('should stop the animations when state becomes not active', async () => {
// Adding an element that has to be animated.
const animatedEl = win.document.createElement('div');
animatedEl.setAttribute('animate-in', 'fade-in');
const animatedEl = html`<div animate-in="fade-in"></div>`;
element.appendChild(animatedEl);

page.buildCallback();
Expand Down
12 changes: 12 additions & 0 deletions src/utils/media-query-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,15 @@ function toggleOnChange(expr, callback, on) {
}
}
}

/**
* Detect prefers-reduced-motion.
* Native animations will not run when a device is set up to reduced motion.
* In that case, we need to disable all animation treatment, and whatever
* setup changes that depend on an animation running later on.
* @param {!Window} win
* @return {boolean}
*/
export function prefersReducedMotion(win) {
return !!win.matchMedia('(prefers-reduced-motion: reduce)')?.matches;
}