Skip to content

Commit 993104b

Browse files
ksheedlonecolas
authored andcommitted
Add polyfill for CSS @media prefers-reduced-motion
1 parent e8f5f53 commit 993104b

11 files changed

Lines changed: 107 additions & 4 deletions

File tree

packages/benchmarks/perf/mocks/react-native.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
* React Native mock for Node.js benchmarks
1010
*/
1111

12+
export const AccessibilityInfo = {
13+
addEventListener: () => ({
14+
remove: () => {}
15+
}),
16+
isReduceMotionEnabled: () => Promise.resolve(false)
17+
};
18+
1219
export const Animated = {
1320
createAnimatedComponent(c) {
1421
return c;

packages/react-strict-dom/src/native/css/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ type ResolveStyleOptions = $ReadOnly<{
123123
fontSize?: ?number,
124124
hover?: ?boolean,
125125
inheritedFontSize: ?number,
126+
prefersReducedMotion: boolean,
126127
viewportHeight: number,
127128
viewportScale: number | void,
128129
viewportWidth: number,
@@ -145,6 +146,7 @@ function resolveStyle(
145146
fontSize: _fontSize,
146147
hover,
147148
inheritedFontSize,
149+
prefersReducedMotion,
148150
viewportHeight,
149151
viewportScale = 1,
150152
viewportWidth
@@ -246,7 +248,8 @@ function resolveStyle(
246248
const matches = mediaQueryMatches(
247249
variant,
248250
viewportWidth,
249-
viewportHeight
251+
viewportHeight,
252+
prefersReducedMotion
250253
);
251254
if (matches) {
252255
activeVariant = variant;

packages/react-strict-dom/src/native/css/mediaQueryMatches.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ const MEDIA = '@media';
1414
export function mediaQueryMatches(
1515
query: string,
1616
width: number,
17-
height: number
17+
height: number,
18+
prefersReducedMotion: boolean
1819
): boolean {
1920
const q = query.split(MEDIA)[1];
2021
return mediaQuery.match(q, {
2122
width: width,
2223
height: height,
2324
orientation: width > height ? 'landscape' : 'portrait',
24-
'aspect-ratio': width / height
25+
'aspect-ratio': width / height,
26+
'prefers-reduced-motion': prefersReducedMotion ? 'reduce' : 'no-preference'
2527
});
2628
}

packages/react-strict-dom/src/native/modules/__tests__/mediaQuery-test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,38 @@ describe('mediaQuery.match()', function () {
8080
).toBe(false);
8181
});
8282

83+
test('prefers-reduced-motion: should return true for a correct match (===)', function () {
84+
expect(
85+
mediaQuery.match('(prefers-reduced-motion: reduce)', {
86+
'prefers-reduced-motion': 'reduce'
87+
})
88+
).toBe(true);
89+
});
90+
91+
test('prefers-reduced-motion: should return false for an incorrect match (===)', function () {
92+
expect(
93+
mediaQuery.match('(prefers-reduced-motion: reduce)', {
94+
'prefers-reduced-motion': 'no-preference'
95+
})
96+
).toBe(false);
97+
});
98+
99+
test('prefers-reduced-motion: should return true for matched default', function () {
100+
expect(
101+
mediaQuery.match('(prefers-reduced-motion)', {
102+
'prefers-reduced-motion': 'reduce'
103+
})
104+
).toBe(true);
105+
});
106+
107+
test('prefers-reduced-motion: should return false for not matching default', function () {
108+
expect(
109+
mediaQuery.match('(prefers-reduced-motion)', {
110+
'prefers-reduced-motion': 'no-preference'
111+
})
112+
).toBe(false);
113+
});
114+
83115
test('scan: should return true for a correct match (===)', function () {
84116
expect(
85117
mediaQuery.match('(scan: progressive)', { scan: 'progressive' })

packages/react-strict-dom/src/native/modules/mediaQuery.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,14 @@ function matchQuery(mediaQuery, values) {
8282
return false;
8383
}
8484

85+
// prefers-reduce-motion without a keyword value is equivalent to
86+
// prefers-reduced-motion: reduce.
87+
if (feature === 'prefers-reduced-motion' && !expValue) {
88+
expValue = 'reduce';
89+
}
90+
8591
switch (feature) {
92+
case 'prefers-reduced-motion':
8693
case 'orientation':
8794
case 'scan':
8895
return value.toLowerCase() === expValue.toLowerCase();

packages/react-strict-dom/src/native/modules/mediaQuery.js.flow

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ type Values = $ReadOnly<{
1313
height?: number,
1414
orientation?: 'landscape' | 'portrait',
1515
'aspect-ratio'?: number,
16-
direction?: 'ltr' | 'rtl'
16+
direction?: 'ltr' | 'rtl',
17+
'prefers-reduced-motion'?: 'reduce' | 'no-preference',
1718
}>;
1819

1920
declare export const mediaQuery : {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
*/
9+
10+
import * as ReactNative from '../react-native';
11+
12+
import { useEffect, useState } from 'react';
13+
14+
export function usePrefersReducedMotion(): boolean {
15+
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
16+
17+
useEffect(() => {
18+
// 1. Get the initial value of reduce motion
19+
ReactNative.AccessibilityInfo.isReduceMotionEnabled().then(
20+
(isReduceMotionEnabled) => {
21+
setPrefersReducedMotion(isReduceMotionEnabled);
22+
}
23+
);
24+
25+
// 2. Subscribe to changes in reduce motion
26+
const reduceMotionChangedSubscription =
27+
ReactNative.AccessibilityInfo.addEventListener(
28+
'reduceMotionChanged',
29+
(isReduceMotionEnabled) => {
30+
setPrefersReducedMotion(isReduceMotionEnabled);
31+
}
32+
);
33+
34+
return () => {
35+
reduceMotionChangedSubscription.remove();
36+
};
37+
}, []);
38+
39+
return prefersReducedMotion;
40+
}

packages/react-strict-dom/src/native/modules/useStyleProps.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import * as css from '../css';
1515
import * as ReactNative from '../react-native';
1616

1717
import { useInheritedStyles } from './ContextInheritedStyles';
18+
import { usePrefersReducedMotion } from './usePrefersReducedMotion';
1819
import { usePseudoStates } from './usePseudoStates';
1920
import { useStyleTransition } from './useStyleTransition';
2021
import { useViewportScale } from './ContextViewportScale';
@@ -120,6 +121,7 @@ export function useStyleProps(
120121
} = useInheritedStyles();
121122

122123
const { active, focus, hover, handlers } = usePseudoStates(flatStyle);
124+
const prefersReducedMotion = usePrefersReducedMotion();
123125

124126
// Get the computed style props
125127
const styleProps = css.props.call(
@@ -132,6 +134,7 @@ export function useStyleProps(
132134
hover,
133135
inheritedFontSize:
134136
typeof inheritedFontSize === 'number' ? inheritedFontSize : undefined,
137+
prefersReducedMotion,
135138
viewportHeight: height,
136139
viewportScale,
137140
viewportWidth: width

packages/react-strict-dom/src/native/react-native/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export {
11+
AccessibilityInfo,
1112
Animated,
1213
Easing,
1314
Image,

packages/react-strict-dom/tests/__mocks__/react-native/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
export const AccessibilityInfo = {
9+
addEventListener: jest.fn().mockReturnValue({ remove: jest.fn() }),
10+
isReduceMotionEnabled: jest.fn().mockReturnValue(Promise.resolve(false))
11+
};
12+
813
export const Animated = {
914
createAnimatedComponent(Component) {
1015
return `Animated.${Component}`;

0 commit comments

Comments
 (0)