mirror of https://github.com/sveltejs/svelte
feat: provide `MediaQuery` / `prefersReducedMotion` (#14422)
* feat: provide `MediaQuery` / `prefersReducedMotion` closes #5346 * matches -> current, server fallback * createStartStopNotifier * test polyfill * more tests fixes * feedback * rename * tweak, types * hnnnggh * mark as pure * fix type check * notify -> subscribe * add links to inline docs * better API, more docs * add example to prefersReducedMotion * add example for MediaQuery * typo * fix example * tweak docs * changesets * note when APIs were added * add note * regenerate --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>pull/14570/head
parent
73b3cf72d0
commit
0a9890bb1e
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': minor
|
||||
---
|
||||
|
||||
feat: add `createSubscriber` function for creating reactive values that depend on subscriptions
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': minor
|
||||
---
|
||||
|
||||
feat: add reactive `MediaQuery` class, and a `prefersReducedMotion` class instance
|
@ -1,2 +1,32 @@
|
||||
import { MediaQuery } from 'svelte/reactivity';
|
||||
|
||||
export * from './spring.js';
|
||||
export * from './tweened.js';
|
||||
|
||||
/**
|
||||
* A [media query](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery) that matches if the user [prefers reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion).
|
||||
*
|
||||
* ```svelte
|
||||
* <script>
|
||||
* import { prefersReducedMotion } from 'svelte/motion';
|
||||
* import { fly } from 'svelte/transition';
|
||||
*
|
||||
* let visible = $state(false);
|
||||
* </script>
|
||||
*
|
||||
* <button onclick={() => visible = !visible}>
|
||||
* toggle
|
||||
* </button>
|
||||
*
|
||||
* {#if visible}
|
||||
* <p transition:fly={{ y: prefersReducedMotion.current ? 0 : 200 }}>
|
||||
* flies in, unless the user prefers reduced motion
|
||||
* </p>
|
||||
* {/if}
|
||||
* ```
|
||||
* @type {MediaQuery}
|
||||
* @since 5.7.0
|
||||
*/
|
||||
export const prefersReducedMotion = /*@__PURE__*/ new MediaQuery(
|
||||
'(prefers-reduced-motion: reduce)'
|
||||
);
|
||||
|
@ -0,0 +1,81 @@
|
||||
import { get, tick, untrack } from '../internal/client/runtime.js';
|
||||
import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js';
|
||||
import { source } from '../internal/client/reactivity/sources.js';
|
||||
import { increment } from './utils.js';
|
||||
|
||||
/**
|
||||
* Returns a `subscribe` function that, if called in an effect (including expressions in the template),
|
||||
* calls its `start` callback with an `update` function. Whenever `update` is called, the effect re-runs.
|
||||
*
|
||||
* If `start` returns a function, it will be called when the effect is destroyed.
|
||||
*
|
||||
* If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
|
||||
* are active, and the returned teardown function will only be called when all effects are destroyed.
|
||||
*
|
||||
* It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery):
|
||||
*
|
||||
* ```js
|
||||
* import { createSubscriber } from 'svelte/reactivity';
|
||||
* import { on } from 'svelte/events';
|
||||
*
|
||||
* export class MediaQuery {
|
||||
* #query;
|
||||
* #subscribe;
|
||||
*
|
||||
* constructor(query) {
|
||||
* this.#query = window.matchMedia(`(${query})`);
|
||||
*
|
||||
* this.#subscribe = createSubscriber((update) => {
|
||||
* // when the `change` event occurs, re-run any effects that read `this.current`
|
||||
* const off = on(this.#query, 'change', update);
|
||||
*
|
||||
* // stop listening when all the effects are destroyed
|
||||
* return () => off();
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* get current() {
|
||||
* this.#subscribe();
|
||||
*
|
||||
* // Return the current state of the query, whether or not we're in an effect
|
||||
* return this.#query.matches;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
* @param {(update: () => void) => (() => void) | void} start
|
||||
* @since 5.7.0
|
||||
*/
|
||||
export function createSubscriber(start) {
|
||||
let subscribers = 0;
|
||||
let version = source(0);
|
||||
/** @type {(() => void) | void} */
|
||||
let stop;
|
||||
|
||||
return () => {
|
||||
if (effect_tracking()) {
|
||||
get(version);
|
||||
|
||||
render_effect(() => {
|
||||
if (subscribers === 0) {
|
||||
stop = untrack(() => start(() => increment(version)));
|
||||
}
|
||||
|
||||
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.
|
||||
subscribers -= 1;
|
||||
|
||||
if (subscribers === 0) {
|
||||
stop?.();
|
||||
stop = undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { createSubscriber } from './create-subscriber.js';
|
||||
import { on } from '../events/index.js';
|
||||
|
||||
/**
|
||||
* Creates a media query and provides a `current` property that reflects whether or not it matches.
|
||||
*
|
||||
* Use it carefully — during server-side rendering, there is no way to know what the correct value should be, potentially causing content to change upon hydration.
|
||||
* If you can use the media query in CSS to achieve the same effect, do that.
|
||||
*
|
||||
* ```svelte
|
||||
* <script>
|
||||
* import { MediaQuery } from 'svelte/reactivity';
|
||||
*
|
||||
* const large = new MediaQuery('min-width: 800px');
|
||||
* </script>
|
||||
*
|
||||
* <h1>{large.current ? 'large screen' : 'small screen'}</h1>
|
||||
* ```
|
||||
* @since 5.7.0
|
||||
*/
|
||||
export class MediaQuery {
|
||||
#query;
|
||||
#subscribe = createSubscriber((update) => {
|
||||
return on(this.#query, 'change', update);
|
||||
});
|
||||
|
||||
get current() {
|
||||
this.#subscribe();
|
||||
|
||||
return this.#query.matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} query A media query string
|
||||
* @param {boolean} [matches] Fallback value for the server
|
||||
*/
|
||||
constructor(query, matches) {
|
||||
// For convenience (and because people likely forget them) we add the parentheses; double parentheses are not a problem
|
||||
this.#query = window.matchMedia(`(${query})`);
|
||||
}
|
||||
}
|
Loading…
Reference in new issue