-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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>
- Loading branch information
1 parent
73b3cf7
commit 0a9890b
Showing
12 changed files
with
305 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'svelte': minor | ||
--- | ||
|
||
feat: add `createSubscriber` function for creating reactive values that depend on subscriptions |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'svelte': minor | ||
--- | ||
|
||
feat: add reactive `MediaQuery` class, and a `prefersReducedMotion` class instance |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)' | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
}); | ||
}; | ||
}); | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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})`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.