From 85ccce86ddd5fd962a149eab05042f808883a8a3 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Wed, 10 Jul 2024 11:59:06 +0200 Subject: [PATCH] Honor user's reduced motion preference for backdrop animations Disable scroll parallax and autozoom if the userhas enabled a setting on their device to minimize the amount of non-essential motion. --- .../features/backdropAnimationEffects-spec.js | 55 +++++++++++++++++++ .../spec/frontend/v1/Backdrop/Effects-spec.js | 4 -- .../package/spec/support/matchMediaStub.js | 14 ++++- .../frontend/SectionViewTimelineProvider.js | 4 +- .../src/frontend/prefersReducedMotion.js | 3 + .../src/frontend/v1/Backdrop/Effects.js | 3 +- 6 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 entry_types/scrolled/package/src/frontend/prefersReducedMotion.js diff --git a/entry_types/scrolled/package/spec/frontend/features/backdropAnimationEffects-spec.js b/entry_types/scrolled/package/spec/frontend/features/backdropAnimationEffects-spec.js index f17540959..1081783e5 100644 --- a/entry_types/scrolled/package/spec/frontend/features/backdropAnimationEffects-spec.js +++ b/entry_types/scrolled/package/spec/frontend/features/backdropAnimationEffects-spec.js @@ -145,6 +145,33 @@ describe('backdrop animation effects', () => { expect(animateMock).not.toHaveBeenCalled(); }); + it('neither calls animate nor sets up view timeline when reduced motion is preferred', () => { + window.matchMedia.mockPrefersReducedMotion(); + + renderEntry({ + seed: { + imageFiles: [{permaId: 100}], + sections: [ + { + permaId: 1, + configuration: { + backdrop: {image: 100}, + backdropEffects: [ + { + name: 'scrollParallax', + value: 40 + } + ] + } + } + ] + } + }); + + expect(viewTimelines.length).toEqual(0); + expect(animateMock).not.toHaveBeenCalled(); + }); + it('supports auto zoom', () => { const {getSectionByPermaId} = renderEntry({ seed: { @@ -245,4 +272,32 @@ describe('backdrop animation effects', () => { expect(animateMock).not.toHaveBeenCalled(); }); + + it('does not autozoom if reduced motion is preferred', () => { + window.matchMedia.mockPrefersReducedMotion(); + + const {getSectionByPermaId} = renderEntry({ + seed: { + imageFiles: [{permaId: 100}], + sections: [ + { + permaId: 1, + configuration: { + backdrop: {image: 100}, + backdropEffects: [ + { + name: 'autoZoom', + value: 50 + } + ] + } + } + ] + } + }); + + getSectionByPermaId(1).simulateScrollingIntoView(); + + expect(animateMock).not.toHaveBeenCalled(); + }); }); diff --git a/entry_types/scrolled/package/spec/frontend/v1/Backdrop/Effects-spec.js b/entry_types/scrolled/package/spec/frontend/v1/Backdrop/Effects-spec.js index 13c06d442..4b80e3136 100644 --- a/entry_types/scrolled/package/spec/frontend/v1/Backdrop/Effects-spec.js +++ b/entry_types/scrolled/package/spec/frontend/v1/Backdrop/Effects-spec.js @@ -36,8 +36,4 @@ describe('Backdrop Effects getFilter', () => { expect(result).toEqual('grayscale(20%)'); }); - - it('sets up scroll animation ', () => { - - }); }); diff --git a/entry_types/scrolled/package/spec/support/matchMediaStub.js b/entry_types/scrolled/package/spec/support/matchMediaStub.js index 071c45dfe..3e17bd3db 100644 --- a/entry_types/scrolled/package/spec/support/matchMediaStub.js +++ b/entry_types/scrolled/package/spec/support/matchMediaStub.js @@ -1,4 +1,5 @@ let mockOrientation; +let mockPrefersReducedMotion; Object.defineProperty(window, 'matchMedia', { writable: true, @@ -17,6 +18,13 @@ Object.defineProperty(window, 'matchMedia', { matches: mockOrientation !== 'portrait' }; } + else if (query === '(prefers-reduced-motion)') { + return { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + matches: mockPrefersReducedMotion + }; + } else { return { addEventListener: jest.fn(), @@ -27,6 +35,10 @@ Object.defineProperty(window, 'matchMedia', { }) }); -beforeEach(() => mockOrientation = 'landscape'); +beforeEach(() => { + mockOrientation = 'landscape'; + mockPrefersReducedMotion = false; +}); window.matchMedia.mockPortrait = () => mockOrientation = 'portrait'; +window.matchMedia.mockPrefersReducedMotion = () => mockPrefersReducedMotion = true; diff --git a/entry_types/scrolled/package/src/frontend/SectionViewTimelineProvider.js b/entry_types/scrolled/package/src/frontend/SectionViewTimelineProvider.js index ba31e5652..427bcd7f9 100644 --- a/entry_types/scrolled/package/src/frontend/SectionViewTimelineProvider.js +++ b/entry_types/scrolled/package/src/frontend/SectionViewTimelineProvider.js @@ -1,5 +1,7 @@ import React, {useContext, useEffect, useState, useRef} from 'react'; +import {prefersReducedMotion} from './prefersReducedMotion'; + const SectionViewTimelineContext = React.createContext(); export function SectionViewTimelineProvider({backdrop, children}) { @@ -9,7 +11,7 @@ export function SectionViewTimelineProvider({backdrop, children}) { const isNeeded = backdrop?.effects?.some(effect => effect.name === 'scrollParallax'); useEffect(() => { - if (!isNeeded || !window.ViewTimeline) { + if (!isNeeded || !window.ViewTimeline || prefersReducedMotion()) { return; } diff --git a/entry_types/scrolled/package/src/frontend/prefersReducedMotion.js b/entry_types/scrolled/package/src/frontend/prefersReducedMotion.js new file mode 100644 index 000000000..281b8a26c --- /dev/null +++ b/entry_types/scrolled/package/src/frontend/prefersReducedMotion.js @@ -0,0 +1,3 @@ +export function prefersReducedMotion() { + return window.matchMedia('(prefers-reduced-motion)').matches; +} diff --git a/entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.js b/entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.js index da22a7672..05f71326f 100644 --- a/entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.js +++ b/entry_types/scrolled/package/src/frontend/v1/Backdrop/Effects.js @@ -6,6 +6,7 @@ import styles from '../../Backdrop.module.css'; import {useSectionViewTimeline} from '../../SectionViewTimelineProvider'; import {useSectionLifecycle} from '../../useSectionLifecycle'; import {useIsStaticPreview} from '../../useScrollPositionLifecycle'; +import {prefersReducedMotion} from '../../prefersReducedMotion'; export function Effects({file, children}) { const ref = useRef(); @@ -45,7 +46,7 @@ export function Effects({file, children}) { const y = file?.motifArea ? 50 - (file.motifArea.top + file.motifArea.height / 2) : 0; useIsomorphicLayoutEffect(() => { - if (autoZoomValue && isVisible) { + if (autoZoomValue && isVisible && !prefersReducedMotion()) { const animation = ref.current.animate( { transform: [