diff --git a/src/lib/actions/motion.test.ts b/src/lib/actions/motion.test.ts new file mode 100644 index 0000000..d3e6b3a --- /dev/null +++ b/src/lib/actions/motion.test.ts @@ -0,0 +1,27 @@ +import { vi, describe, test, expect, beforeEach } from 'vitest'; + +// Control the reduced-motion preference the helper reads. `vi.hoisted` builds +// the stub before the (hoisted) mock factory runs so we can flip `.current` +// per test. +const { state } = vi.hoisted(() => ({ state: { current: false } })); +vi.mock('./reducedMotion.js', () => ({ reducedMotion: state })); + +import { motionSafe } from './motion.js'; + +describe('motionSafe', () => { + beforeEach(() => { + state.current = false; + }); + + test('passes transition params through unchanged when motion is allowed', () => { + const params = { y: -8, duration: 300 }; + expect(motionSafe(params)).toBe(params); + }); + + test('collapses to an instant cut under reduced motion — no leftover offsets', () => { + state.current = true; + // The whole point: a partial gate (zeroing duration but keeping `y`) would + // still animate position. motionSafe must drop everything but duration: 0. + expect(motionSafe({ y: -8, duration: 300 })).toEqual({ duration: 0 }); + }); +}); diff --git a/src/lib/actions/motion.ts b/src/lib/actions/motion.ts new file mode 100644 index 0000000..0bdac48 --- /dev/null +++ b/src/lib/actions/motion.ts @@ -0,0 +1,24 @@ +import { reducedMotion } from './reducedMotion.js'; + +/** Every Svelte transition params object accepts an optional `duration` (ms). */ +type MotionParams = { duration?: number }; + +/** + * Gate a Svelte transition's params on `prefers-reduced-motion`. + * + * Returns `{ duration: 0 }` (an instant cut — no travel, no fade) when the user + * has requested reduced motion, otherwise `params` unchanged. This keeps the + * reduced-motion contract in one place instead of each animated component + * re-deriving `reducedMotion.current ? { duration: 0 } : …` (and risking a + * partial gate, e.g. zeroing `duration` but leaving a `y` offset). + * + * Call inside a `$derived` so it re-runs when the preference changes: + * + * ```svelte + * const params = $derived(motionSafe({ y: -8, duration: 300 })); + * //
+ * ``` + */ +export function motionSafe(params: T): T | { duration: 0 } { + return reducedMotion.current ? { duration: 0 } : params; +} diff --git a/src/lib/components/FilterToolbar.svelte b/src/lib/components/FilterToolbar.svelte index 8521251..bc8ae58 100644 --- a/src/lib/components/FilterToolbar.svelte +++ b/src/lib/components/FilterToolbar.svelte @@ -1,7 +1,7 @@