Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cc6efa4
feat(native): evaluate prefers-reduced-motion so motion-reduce / moti…
YevheniiKotyrlo Oct 6, 2026
4f4cd69
fix(compiler): read a media feature's keyword value case-insensitively
YevheniiKotyrlo Oct 9, 2026
f950d7f
refactor(native): state the reduced-motion constraints in one line each
YevheniiKotyrlo Oct 9, 2026
aec778c
fix(native): evaluate a media feature in a boolean context as MQ4 does
YevheniiKotyrlo Oct 9, 2026
87e6064
refactor(native): leave the boolean context of other features to #426
YevheniiKotyrlo Oct 9, 2026
a08470c
test(native): an unknown media feature never matches in a boolean con…
YevheniiKotyrlo Oct 9, 2026
5987763
docs(compiler): cite css-values-4 §4.1 for keyword case
YevheniiKotyrlo Oct 9, 2026
4bc3035
fix(native): keep the reduced-motion preference unknown until Accessi…
YevheniiKotyrlo Oct 20, 2026
840b69c
fix(compiler): fold a custom media type to lowercase
YevheniiKotyrlo Oct 20, 2026
07c38bb
feat(native): let a script set the reduced-motion preference, as it s…
YevheniiKotyrlo Oct 20, 2026
a79174f
fix(native): keep the device's reduced-motion answer apart from a scr…
YevheniiKotyrlo Oct 20, 2026
7027d99
fix(jest): reset the window through the package's internal entry, as …
YevheniiKotyrlo Oct 20, 2026
aa9ee2b
fix: hold a reducedMotion override until set(null), and let a web ove…
YevheniiKotyrlo Oct 21, 2026
66aa22e
refactor(native): start a device preference unknown, and drop the cas…
YevheniiKotyrlo Oct 21, 2026
359ab87
fix(web): keep a reducedMotion override to the browser, as a server r…
YevheniiKotyrlo Oct 21, 2026
7e4c036
refactor(web): hold the reducedMotion override in a browserPreference…
YevheniiKotyrlo Oct 21, 2026
0baba44
fix: restore the device's answer between tests, and import without an…
YevheniiKotyrlo Oct 21, 2026
99fd91c
fix: reset to AccessibilityInfo's answer even when it arrives late, a…
YevheniiKotyrlo Oct 21, 2026
7f75f6a
fix(web): answer a matchMedia stub that returns no list as no matchMe…
YevheniiKotyrlo Oct 21, 2026
ee620e7
test: give the jest helper's reset a file of its own
YevheniiKotyrlo Oct 21, 2026
8951326
test(compiler): keep to the reduced-motion queries this change compiles
YevheniiKotyrlo Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/__tests__/compiler/compiler.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,24 @@ test("light-dark()", () => {
});
});

test.each([
[
"(prefers-reduced-motion: reduce)",
["=", "prefers-reduced-motion", "reduce"],
],
[
"(prefers-reduced-motion: no-preference)",
["=", "prefers-reduced-motion", "no-preference"],
],
["(prefers-reduced-motion)", ["!!", "prefers-reduced-motion"]],
])("@media %s compiles to its condition", (query, condition) => {
expect(
compile(`@media ${query} { .my-class { opacity: 0 } }`).stylesheet(),
).toStrictEqual({
s: [["my-class", [{ s: [2, 1], m: [condition], d: [{ opacity: 0 }] }]]],
});
});

test("media query nested in rules", () => {
const compiled = compile(`
.my-class {
Expand Down
24 changes: 24 additions & 0 deletions src/__tests__/native/jest-reset.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { reducedMotion } from "react-native-css/runtime";

import { resetBetweenTests } from "../../jest/reset";
import { deviceReducedMotion } from "../../native/reactivity";

describe("the react-native-css/jest reset", () => {
test("clears a test's override", () => {
reducedMotion.set("reduce");
expect(reducedMotion.get()).toBe("reduce");

resetBetweenTests();

expect(reducedMotion.get()).toBe("no-preference");
});

test("puts a device a test changed back to AccessibilityInfo's answer", () => {
deviceReducedMotion.set("reduce");
expect(reducedMotion.get()).toBe("reduce");

resetBetweenTests();

expect(reducedMotion.get()).toBe("no-preference");
});
});
194 changes: 192 additions & 2 deletions src/__tests__/native/media-query.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { PixelRatio } from "react-native";
import { act, render, screen } from "@testing-library/react-native";
import { View } from "react-native-css/components/View";
import { registerCSS, testID } from "react-native-css/jest";
import { colorScheme } from "react-native-css/runtime";
import { colorScheme, reducedMotion } from "react-native-css/runtime";

import { dimensions } from "../../native/reactivity";
import { deviceReducedMotion, dimensions } from "../../native/reactivity";

jest.mock("react-native", () => {
const RN = jest.requireActual("react-native");
Expand Down Expand Up @@ -283,3 +283,193 @@ describe("max-resolution", () => {
expect(component.props.style).toStrictEqual(undefined);
});
});

describe("a reduced-motion preference AccessibilityInfo has not answered", () => {
let answered: ReturnType<typeof deviceReducedMotion.get>;

beforeEach(() => {
answered = deviceReducedMotion.get();
deviceReducedMotion.set(undefined);
});

afterEach(() => {
act(() => {
deviceReducedMotion.set(answered);
});
});

test("a mounted view takes the device's answer when it arrives", () => {
registerCSS(`
.my-class { color: #000000; }
@media (prefers-reduced-motion: reduce) { .my-class { color: #020202; } }`);

render(<View testID={testID} className="my-class" />);
const component = screen.getByTestId(testID);
expect(component.props.style).toStrictEqual({ color: "#000" });

act(() => {
deviceReducedMotion.set("reduce");
});
expect(component.props.style).toStrictEqual({ color: "#020202" });
});

test.each([
"(prefers-reduced-motion: reduce)",
"(prefers-reduced-motion: no-preference)",
"(prefers-reduced-motion)",
])("leaves @media %s unmatched", (query) => {
registerCSS(
`.my-class { color: #000000; } @media ${query} { .my-class { color: #010101; } }`,
);

render(<View testID={testID} className="my-class" />);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: "#000",
});
});

test("a mounted view follows the reduced-motion preference as it changes", () => {
registerCSS(`
.my-class { color: #000000; }
@media (prefers-reduced-motion: no-preference) { .my-class { color: #010101; } }
@media (prefers-reduced-motion: reduce) { .my-class { color: #020202; } }`);

render(<View testID={testID} className="my-class" />);
const component = screen.getByTestId(testID);
expect(component.props.style).toStrictEqual({ color: "#000" });

act(() => {
reducedMotion.set("no-preference");
});
expect(component.props.style).toStrictEqual({ color: "#010101" });

act(() => {
reducedMotion.set("reduce");
});
expect(component.props.style).toStrictEqual({ color: "#020202" });

act(() => {
reducedMotion.set("no-preference");
});
expect(component.props.style).toStrictEqual({ color: "#010101" });
});
});

test("reducedMotion.set holds through a change of the device's setting, and set(null) follows the device again", () => {
registerCSS(`
.my-class { color: #000000; }
@media (prefers-reduced-motion: reduce) { .my-class { color: #020202; } }`);
const answered = deviceReducedMotion.get();

render(<View testID={testID} className="my-class" />);
const component = screen.getByTestId(testID);

try {
act(() => {
reducedMotion.set("no-preference");
deviceReducedMotion.set("reduce");
});
expect(reducedMotion.get()).toBe("no-preference");
expect(component.props.style).toStrictEqual({ color: "#000" });

act(() => {
reducedMotion.set(null);
});
expect(reducedMotion.get()).toBe("reduce");
expect(component.props.style).toStrictEqual({ color: "#020202" });
} finally {
act(() => {
deviceReducedMotion.set(answered);
});
}
});

test("reducedMotion.set overrides the device's preference, and set(null) follows the device again at once", () => {
registerCSS(`
.my-class { color: #000000; }
@media (prefers-reduced-motion: reduce) { .my-class { color: #020202; } }`);

render(<View testID={testID} className="my-class" />);
const component = screen.getByTestId(testID);
expect(reducedMotion.get()).toBe("no-preference");
expect(component.props.style).toStrictEqual({ color: "#000" });

act(() => {
reducedMotion.set("reduce");
});
expect(reducedMotion.get()).toBe("reduce");
expect(component.props.style).toStrictEqual({ color: "#020202" });

act(() => {
reducedMotion.set(null);
});
expect(reducedMotion.get()).toBe("no-preference");
expect(component.props.style).toStrictEqual({ color: "#000" });
});

// Chromium 153, Firefox 155 and WebKit 26.6 agree on every row: [query, reduce/light, reduce/dark, no-preference/light, no-preference/dark]
const BROWSER_MEDIA_ROWS = [
["(prefers-reduced-motion: reduce)", true, true, false, false],
["(prefers-reduced-motion: no-preference)", false, false, true, true],
["(prefers-reduced-motion)", true, true, false, false],
["not (prefers-reduced-motion)", false, false, true, true],
["(prefers-reduced-motion: bogus)", false, false, false, false],
["not all and (prefers-reduced-motion: reduce)", false, false, true, true],
[
"(prefers-reduced-motion: reduce) and (min-width: 1px)",
true,
true,
false,
false,
],
[
"(prefers-reduced-motion: reduce) and (prefers-color-scheme: dark)",
false,
true,
false,
false,
],
[
"(prefers-reduced-motion: reduce), (prefers-color-scheme: dark)",
true,
true,
false,
true,
],
["not (prefers-reduced-motion: reduce)", false, false, true, true],
["(prefers-color-scheme: dark)", false, true, false, true],
["(unknown-feature)", false, false, false, false],
] as const;

const MEDIA_STATES = [
["reduce", "light"],
["reduce", "dark"],
["no-preference", "light"],
["no-preference", "dark"],
] as const;

describe.each(BROWSER_MEDIA_ROWS)(
"@media %s matches as browsers match it",
(query, ...matches) => {
test.each(
MEDIA_STATES.map(
([motion, scheme], index) => [motion, scheme, matches[index]] as const,
),
)("under %s motion in the %s scheme: %s", (motion, scheme, expected) => {
act(() => {
reducedMotion.set(motion);
colorScheme.set(scheme);
});
registerCSS(
`.my-class { color: #000000; } @media ${query} { .my-class { color: #010101; } }`,
);

render(<View testID={testID} className="my-class" />);

expect(screen.getByTestId(testID).props.style).toStrictEqual({
color: expected ? "#010101" : "#000",
});
});
},
);
Loading