From c5c4bea9dae1234e038400f539ae5fee7e28b056 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 25 Nov 2024 12:10:34 +0100 Subject: [PATCH] feat: provide `MediaQuery` / `prefersReducedMotion` closes #5346 --- packages/svelte/src/motion/index.js | 8 +++ .../svelte/src/reactivity/index-client.js | 1 + .../svelte/src/reactivity/index-server.js | 4 ++ packages/svelte/src/reactivity/media-query.js | 49 +++++++++++++++++++ packages/svelte/types/index.d.ts | 14 ++++++ 5 files changed, 76 insertions(+) create mode 100644 packages/svelte/src/reactivity/media-query.js diff --git a/packages/svelte/src/motion/index.js b/packages/svelte/src/motion/index.js index 10f52502d372..295e32eb0d65 100644 --- a/packages/svelte/src/motion/index.js +++ b/packages/svelte/src/motion/index.js @@ -1,2 +1,10 @@ +import { MediaQuery } from 'svelte/reactivity'; + export * from './spring.js'; export * from './tweened.js'; + +/** + * A media query that matches if the user has requested reduced motion. + * @type {MediaQuery} + */ +export const prefersReducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)'); diff --git a/packages/svelte/src/reactivity/index-client.js b/packages/svelte/src/reactivity/index-client.js index 2757688a5958..d11d5195e741 100644 --- a/packages/svelte/src/reactivity/index-client.js +++ b/packages/svelte/src/reactivity/index-client.js @@ -3,3 +3,4 @@ export { SvelteSet } from './set.js'; export { SvelteMap } from './map.js'; export { SvelteURL } from './url.js'; export { SvelteURLSearchParams } from './url-search-params.js'; +export { MediaQuery } from './media-query.js'; diff --git a/packages/svelte/src/reactivity/index-server.js b/packages/svelte/src/reactivity/index-server.js index 6240469ec36f..71202f41bc12 100644 --- a/packages/svelte/src/reactivity/index-server.js +++ b/packages/svelte/src/reactivity/index-server.js @@ -3,3 +3,7 @@ export const SvelteSet = globalThis.Set; export const SvelteMap = globalThis.Map; export const SvelteURL = globalThis.URL; export const SvelteURLSearchParams = globalThis.URLSearchParams; + +export class MediaQuery { + matches = false; +} diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js new file mode 100644 index 000000000000..acb965d182da --- /dev/null +++ b/packages/svelte/src/reactivity/media-query.js @@ -0,0 +1,49 @@ +import { get, tick } from '../internal/client/runtime.js'; +import { set, source } from '../internal/client/reactivity/sources.js'; +import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js'; + +/** + * Creates a media query and provides a `matches` property that reflects its current state. + */ +export class MediaQuery { + #matches = source(false); + #subscribers = 0; + #query; + /** @type {any} */ + #listener; + + get matches() { + if (effect_tracking()) { + render_effect(() => { + if (this.#subscribers === 0) { + this.#listener = () => set(this.#matches, this.#query.matches); + this.#query.addEventListener('change', this.#listener); + } + + this.#subscribers += 1; + + return () => { + tick().then(() => { + // Only count down after timeout, else we would reach 0 before our own render effect reruns, + // but reach 1 again when the tick callback of the prior teardown runs. That would mean we + // re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up. + this.#subscribers -= 1; + + if (this.#subscribers === 0) { + this.#query.removeEventListener('change', this.#listener); + } + }); + }; + }); + } + + return get(this.#matches); + } + + /** @param {string} query */ + constructor(query) { + this.#query = window.matchMedia(query); + console.log('MediaQuery.constructor', query, this.#query); + this.#matches.v = this.#query.matches; + } +} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 01119e748572..fe432fc3edf2 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1630,6 +1630,7 @@ declare module 'svelte/legacy' { } declare module 'svelte/motion' { + import type { MediaQuery } from 'svelte/reactivity'; export interface Spring extends Readable { set: (new_value: T, opts?: SpringUpdateOpts) => Promise; update: (fn: Updater, opts?: SpringUpdateOpts) => Promise; @@ -1676,6 +1677,10 @@ declare module 'svelte/motion' { easing?: (t: number) => number; interpolate?: (a: T, b: T) => (t: number) => T; } + /** + * A media query that matches if the user has requested reduced motion. + * */ + export const prefersReducedMotion: MediaQuery; /** * The spring function in Svelte creates a store whose value is animated, with a motion that simulates the behavior of a spring. This means when the value changes, instead of transitioning at a steady rate, it "bounces" like a spring would, depending on the physics parameters provided. This adds a level of realism to the transitions and can enhance the user experience. * @@ -1720,6 +1725,15 @@ declare module 'svelte/reactivity' { [REPLACE](params: URLSearchParams): void; #private; } + /** + * Creates a media query and provides a `matches` property that reflects its current state. + */ + export class MediaQuery { + + constructor(query: string); + get matches(): boolean; + #private; + } export {}; }