From c5c4bea9dae1234e038400f539ae5fee7e28b056 Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Mon, 25 Nov 2024 12:10:34 +0100
Subject: [PATCH 01/23] 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 {};
}
From 62d370a753cbc889eb8ec701f013c24113a996fc Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Mon, 25 Nov 2024 17:03:20 +0100
Subject: [PATCH 02/23] matches -> current, server fallback
---
.../svelte/src/reactivity/index-server.js | 9 +++++++-
packages/svelte/src/reactivity/media-query.js | 21 +++++++++++--------
2 files changed, 20 insertions(+), 10 deletions(-)
diff --git a/packages/svelte/src/reactivity/index-server.js b/packages/svelte/src/reactivity/index-server.js
index 71202f41bc12..653a7bae4a82 100644
--- a/packages/svelte/src/reactivity/index-server.js
+++ b/packages/svelte/src/reactivity/index-server.js
@@ -5,5 +5,12 @@ export const SvelteURL = globalThis.URL;
export const SvelteURLSearchParams = globalThis.URLSearchParams;
export class MediaQuery {
- matches = false;
+ current;
+ /**
+ * @param {string} query
+ * @param {boolean} [matches]
+ */
+ constructor(query, matches = false) {
+ this.current = matches;
+ }
}
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index acb965d182da..ecb44cd50ed6 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -3,20 +3,22 @@ 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.
+ * Creates a media query and provides a `current` property that reflects whether or not it matches.
*/
export class MediaQuery {
- #matches = source(false);
+ #version = source(0);
#subscribers = 0;
#query;
/** @type {any} */
#listener;
- get matches() {
+ get current() {
if (effect_tracking()) {
+ get(this.#version);
+
render_effect(() => {
if (this.#subscribers === 0) {
- this.#listener = () => set(this.#matches, this.#query.matches);
+ this.#listener = () => set(this.#version, this.#version.v + 1);
this.#query.addEventListener('change', this.#listener);
}
@@ -37,13 +39,14 @@ export class MediaQuery {
});
}
- return get(this.#matches);
+ return this.#query.matches;
}
- /** @param {string} query */
- constructor(query) {
+ /**
+ * @param {string} query A media query string (don't forget the braces)
+ * @param {boolean} [matches] Fallback value for the server
+ */
+ constructor(query, matches) {
this.#query = window.matchMedia(query);
- console.log('MediaQuery.constructor', query, this.#query);
- this.#matches.v = this.#query.matches;
}
}
From 9da3a10adb30942b86e278b565bb84de0aa37e40 Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Mon, 25 Nov 2024 17:44:43 +0100
Subject: [PATCH 03/23] createStartStopNotifier
---
.../svelte/src/reactivity/index-server.js | 7 +++
packages/svelte/src/reactivity/media-query.js | 37 ++++----------
.../src/reactivity/start-stop-notifier.js | 40 ++++++++++++++++
packages/svelte/src/store/index-client.js | 48 ++++++-------------
packages/svelte/types/index.d.ts | 11 +++--
5 files changed, 79 insertions(+), 64 deletions(-)
create mode 100644 packages/svelte/src/reactivity/start-stop-notifier.js
diff --git a/packages/svelte/src/reactivity/index-server.js b/packages/svelte/src/reactivity/index-server.js
index 653a7bae4a82..436f2aab54c6 100644
--- a/packages/svelte/src/reactivity/index-server.js
+++ b/packages/svelte/src/reactivity/index-server.js
@@ -14,3 +14,10 @@ export class MediaQuery {
this.current = matches;
}
}
+
+/**
+ * @param {any} _
+ */
+export function createStartStopNotifier(_) {
+ return () => {};
+}
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index ecb44cd50ed6..333628b659b9 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -1,42 +1,20 @@
-import { get, tick } from '../internal/client/runtime.js';
+import { get } 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';
+import { effect_tracking } from '../internal/client/reactivity/effects.js';
+import { createStartStopNotifier } from './start-stop-notifier.js';
/**
* Creates a media query and provides a `current` property that reflects whether or not it matches.
*/
export class MediaQuery {
#version = source(0);
- #subscribers = 0;
#query;
- /** @type {any} */
- #listener;
+ #notify;
get current() {
if (effect_tracking()) {
get(this.#version);
-
- render_effect(() => {
- if (this.#subscribers === 0) {
- this.#listener = () => set(this.#version, this.#version.v + 1);
- 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);
- }
- });
- };
- });
+ this.#notify();
}
return this.#query.matches;
@@ -48,5 +26,10 @@ export class MediaQuery {
*/
constructor(query, matches) {
this.#query = window.matchMedia(query);
+ this.#notify = createStartStopNotifier(() => {
+ const listener = () => set(this.#version, this.#version.v + 1);
+ this.#query.addEventListener('change', listener);
+ return () => this.#query.removeEventListener('change', listener);
+ });
}
}
diff --git a/packages/svelte/src/reactivity/start-stop-notifier.js b/packages/svelte/src/reactivity/start-stop-notifier.js
new file mode 100644
index 000000000000..8c561b6f8868
--- /dev/null
+++ b/packages/svelte/src/reactivity/start-stop-notifier.js
@@ -0,0 +1,40 @@
+import { tick, untrack } from '../internal/client/runtime.js';
+import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js';
+
+/**
+ * Returns a function that, when invoked in a reactive context, calls the `start` function once,
+ * and calls the `stop` function returned from `start` when all reactive contexts it's called in
+ * are destroyed. This is useful for creating a notifier that starts and stops when the
+ * "subscriber" count goes from 0 to 1 and back to 0.
+ * @param {() => () => void} start
+ */
+export function createStartStopNotifier(start) {
+ let subscribers = 0;
+ /** @type {() => void} */
+ let stop;
+
+ return () => {
+ if (effect_tracking()) {
+ render_effect(() => {
+ if (subscribers === 0) {
+ stop = untrack(start);
+ }
+
+ 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();
+ }
+ });
+ };
+ });
+ }
+ };
+}
diff --git a/packages/svelte/src/store/index-client.js b/packages/svelte/src/store/index-client.js
index f2f1dfc4eba1..ec3f2c43efde 100644
--- a/packages/svelte/src/store/index-client.js
+++ b/packages/svelte/src/store/index-client.js
@@ -1,14 +1,14 @@
/** @import { Readable, Writable } from './public.js' */
-import { noop } from '../internal/shared/utils.js';
import {
effect_root,
effect_tracking,
render_effect
} from '../internal/client/reactivity/effects.js';
import { source } from '../internal/client/reactivity/sources.js';
-import { get as get_source, tick } from '../internal/client/runtime.js';
+import { get as get_source } from '../internal/client/runtime.js';
import { increment } from '../reactivity/utils.js';
import { get, writable } from './shared/index.js';
+import { createStartStopNotifier } from '../reactivity/start-stop-notifier.js';
export { derived, get, readable, readonly, writable } from './shared/index.js';
@@ -110,42 +110,24 @@ export function toStore(get, set) {
export function fromStore(store) {
let value = /** @type {V} */ (undefined);
let version = source(0);
- let subscribers = 0;
- let unsubscribe = noop;
+ const notify = createStartStopNotifier(() => {
+ let ran = false;
+
+ const unsubscribe = store.subscribe((v) => {
+ value = v;
+ if (ran) increment(version);
+ });
+
+ ran = true;
+
+ return unsubscribe;
+ });
function current() {
if (effect_tracking()) {
get_source(version);
-
- render_effect(() => {
- if (subscribers === 0) {
- let ran = false;
-
- unsubscribe = store.subscribe((v) => {
- value = v;
- if (ran) increment(version);
- });
-
- ran = true;
- }
-
- 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) {
- unsubscribe();
- }
- });
- };
- });
-
+ notify();
return value;
}
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index fe432fc3edf2..5c64465a3216 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1726,12 +1726,15 @@ declare module 'svelte/reactivity' {
#private;
}
/**
- * Creates a media query and provides a `matches` property that reflects its current state.
+ * Creates a media query and provides a `current` property that reflects whether or not it matches.
*/
export class MediaQuery {
-
- constructor(query: string);
- get matches(): boolean;
+ /**
+ * @param query A media query string (don't forget the braces)
+ * @param matches Fallback value for the server
+ */
+ constructor(query: string, matches?: boolean | undefined);
+ get current(): boolean;
#private;
}
From 7c3fb02e96544a32bec617bf37cbe3d33d521e51 Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Mon, 25 Nov 2024 17:44:52 +0100
Subject: [PATCH 04/23] test polyfill
---
packages/svelte/tests/runtime-legacy/shared.ts | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index b14c0bdf4bd3..0fac5927b23e 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -478,3 +478,13 @@ export function ok(value: any): asserts value {
throw new Error(`Expected truthy value, got ${value}`);
}
}
+
+// @ts-expect-error JS DOM doesn't support it
+Window.prototype.matchMedia = (media) => {
+ return {
+ matches: false,
+ media,
+ addEventListener: () => {},
+ removeEventListener: () => {}
+ };
+};
From defa6465ad26d9a83b0abbf6ff4f3e4c0dad8b03 Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Mon, 25 Nov 2024 17:55:15 +0100
Subject: [PATCH 05/23] more tests fixes
---
packages/svelte/tests/helpers.js | 10 ++++++++++
packages/svelte/tests/motion/test.ts | 2 ++
packages/svelte/tests/runtime-legacy/shared.ts | 10 ----------
3 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index 002ebf2e38ff..0d4a220f78a9 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -172,3 +172,13 @@ export function write(file, contents) {
fs.writeFileSync(file, contents);
}
+
+// @ts-expect-error JS DOM doesn't support it
+Window.prototype.matchMedia = (media) => {
+ return {
+ matches: false,
+ media,
+ addEventListener: () => {},
+ removeEventListener: () => {}
+ };
+};
diff --git a/packages/svelte/tests/motion/test.ts b/packages/svelte/tests/motion/test.ts
index 05971b5cab65..b6554e5e56ed 100644
--- a/packages/svelte/tests/motion/test.ts
+++ b/packages/svelte/tests/motion/test.ts
@@ -1,3 +1,5 @@
+// @vitest-environment jsdom
+import '../helpers.js'; // for the matchMedia polyfill
import { describe, it, assert } from 'vitest';
import { get } from 'svelte/store';
import { spring, tweened } from 'svelte/motion';
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index 0fac5927b23e..b14c0bdf4bd3 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -478,13 +478,3 @@ export function ok(value: any): asserts value {
throw new Error(`Expected truthy value, got ${value}`);
}
}
-
-// @ts-expect-error JS DOM doesn't support it
-Window.prototype.matchMedia = (media) => {
- return {
- matches: false,
- media,
- addEventListener: () => {},
- removeEventListener: () => {}
- };
-};
From 905556b5179ac74335f5ca9e536622468dbc5985 Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Mon, 25 Nov 2024 19:57:23 +0100
Subject: [PATCH 06/23] feedback
---
packages/svelte/src/reactivity/media-query.js | 20 +++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index 333628b659b9..0b414e706911 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -1,15 +1,19 @@
import { get } from '../internal/client/runtime.js';
-import { set, source } from '../internal/client/reactivity/sources.js';
+import { source } from '../internal/client/reactivity/sources.js';
import { effect_tracking } from '../internal/client/reactivity/effects.js';
import { createStartStopNotifier } from './start-stop-notifier.js';
+import { on } from '../events/index.js';
+import { increment } from './utils.js';
/**
* Creates a media query and provides a `current` property that reflects whether or not it matches.
*/
export class MediaQuery {
- #version = source(0);
#query;
- #notify;
+ #version = source(0);
+ #notify = createStartStopNotifier(() => {
+ return on(this.#query, 'change', () => increment(this.#version));
+ });
get current() {
if (effect_tracking()) {
@@ -21,15 +25,11 @@ export class MediaQuery {
}
/**
- * @param {string} query A media query string (don't forget the braces)
+ * @param {string} query A media query string
* @param {boolean} [matches] Fallback value for the server
*/
constructor(query, matches) {
- this.#query = window.matchMedia(query);
- this.#notify = createStartStopNotifier(() => {
- const listener = () => set(this.#version, this.#version.v + 1);
- this.#query.addEventListener('change', listener);
- return () => this.#query.removeEventListener('change', listener);
- });
+ // For convenience (and because people likely forget them) we add the parentheses; double parantheses are not a problem
+ this.#query = window.matchMedia(`(${query})`);
}
}
From 6e2bc3e12cad3bf46743f536d822df7e80757ad2 Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Mon, 25 Nov 2024 19:58:33 +0100
Subject: [PATCH 07/23] rename
---
.../{start-stop-notifier.js => create-subscriber.js} | 2 +-
packages/svelte/src/reactivity/media-query.js | 4 ++--
packages/svelte/src/store/index-client.js | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
rename packages/svelte/src/reactivity/{start-stop-notifier.js => create-subscriber.js} (96%)
diff --git a/packages/svelte/src/reactivity/start-stop-notifier.js b/packages/svelte/src/reactivity/create-subscriber.js
similarity index 96%
rename from packages/svelte/src/reactivity/start-stop-notifier.js
rename to packages/svelte/src/reactivity/create-subscriber.js
index 8c561b6f8868..d64a32aa4345 100644
--- a/packages/svelte/src/reactivity/start-stop-notifier.js
+++ b/packages/svelte/src/reactivity/create-subscriber.js
@@ -8,7 +8,7 @@ import { effect_tracking, render_effect } from '../internal/client/reactivity/ef
* "subscriber" count goes from 0 to 1 and back to 0.
* @param {() => () => void} start
*/
-export function createStartStopNotifier(start) {
+export function createSubscriber(start) {
let subscribers = 0;
/** @type {() => void} */
let stop;
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index 0b414e706911..b93eddf4e320 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -1,7 +1,7 @@
import { get } from '../internal/client/runtime.js';
import { source } from '../internal/client/reactivity/sources.js';
import { effect_tracking } from '../internal/client/reactivity/effects.js';
-import { createStartStopNotifier } from './start-stop-notifier.js';
+import { createSubscriber } from './create-subscriber.js';
import { on } from '../events/index.js';
import { increment } from './utils.js';
@@ -11,7 +11,7 @@ import { increment } from './utils.js';
export class MediaQuery {
#query;
#version = source(0);
- #notify = createStartStopNotifier(() => {
+ #notify = createSubscriber(() => {
return on(this.#query, 'change', () => increment(this.#version));
});
diff --git a/packages/svelte/src/store/index-client.js b/packages/svelte/src/store/index-client.js
index ec3f2c43efde..1140f4b18c5c 100644
--- a/packages/svelte/src/store/index-client.js
+++ b/packages/svelte/src/store/index-client.js
@@ -8,7 +8,7 @@ import { source } from '../internal/client/reactivity/sources.js';
import { get as get_source } from '../internal/client/runtime.js';
import { increment } from '../reactivity/utils.js';
import { get, writable } from './shared/index.js';
-import { createStartStopNotifier } from '../reactivity/start-stop-notifier.js';
+import { createSubscriber } from '../reactivity/create-subscriber.js';
export { derived, get, readable, readonly, writable } from './shared/index.js';
@@ -111,7 +111,7 @@ export function fromStore(store) {
let value = /** @type {V} */ (undefined);
let version = source(0);
- const notify = createStartStopNotifier(() => {
+ const notify = createSubscriber(() => {
let ran = false;
const unsubscribe = store.subscribe((v) => {
From 80887196f8df6057a10fa6434a4772ef9cba9ccd Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Mon, 25 Nov 2024 20:00:52 +0100
Subject: [PATCH 08/23] tweak, types
---
packages/svelte/src/reactivity/create-subscriber.js | 7 ++++---
packages/svelte/src/reactivity/index-client.js | 1 +
packages/svelte/src/reactivity/index-server.js | 2 +-
packages/svelte/types/index.d.ts | 9 ++++++++-
4 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js
index d64a32aa4345..5911b4936c68 100644
--- a/packages/svelte/src/reactivity/create-subscriber.js
+++ b/packages/svelte/src/reactivity/create-subscriber.js
@@ -6,11 +6,11 @@ import { effect_tracking, render_effect } from '../internal/client/reactivity/ef
* and calls the `stop` function returned from `start` when all reactive contexts it's called in
* are destroyed. This is useful for creating a notifier that starts and stops when the
* "subscriber" count goes from 0 to 1 and back to 0.
- * @param {() => () => void} start
+ * @param {() => (() => void) | void} start
*/
export function createSubscriber(start) {
let subscribers = 0;
- /** @type {() => void} */
+ /** @type {(() => void) | void} */
let stop;
return () => {
@@ -30,7 +30,8 @@ export function createSubscriber(start) {
subscribers -= 1;
if (subscribers === 0) {
- stop();
+ stop?.();
+ stop = undefined;
}
});
};
diff --git a/packages/svelte/src/reactivity/index-client.js b/packages/svelte/src/reactivity/index-client.js
index d11d5195e741..3eb9b95333ab 100644
--- a/packages/svelte/src/reactivity/index-client.js
+++ b/packages/svelte/src/reactivity/index-client.js
@@ -4,3 +4,4 @@ export { SvelteMap } from './map.js';
export { SvelteURL } from './url.js';
export { SvelteURLSearchParams } from './url-search-params.js';
export { MediaQuery } from './media-query.js';
+export { createSubscriber } from './create-subscriber.js';
diff --git a/packages/svelte/src/reactivity/index-server.js b/packages/svelte/src/reactivity/index-server.js
index 436f2aab54c6..6a6c9dcf1360 100644
--- a/packages/svelte/src/reactivity/index-server.js
+++ b/packages/svelte/src/reactivity/index-server.js
@@ -18,6 +18,6 @@ export class MediaQuery {
/**
* @param {any} _
*/
-export function createStartStopNotifier(_) {
+export function createSubscriber(_) {
return () => {};
}
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 5c64465a3216..2e9d3d188bda 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1730,13 +1730,20 @@ declare module 'svelte/reactivity' {
*/
export class MediaQuery {
/**
- * @param query A media query string (don't forget the braces)
+ * @param query A media query string
* @param matches Fallback value for the server
*/
constructor(query: string, matches?: boolean | undefined);
get current(): boolean;
#private;
}
+ /**
+ * Returns a function that, when invoked in a reactive context, calls the `start` function once,
+ * and calls the `stop` function returned from `start` when all reactive contexts it's called in
+ * are destroyed. This is useful for creating a notifier that starts and stops when the
+ * "subscriber" count goes from 0 to 1 and back to 0.
+ * */
+ export function createSubscriber(start: () => (() => void) | void): () => void;
export {};
}
From c996cc4a89ace88a1fd4c331dfbd3e15e4f8a6be Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Mon, 25 Nov 2024 20:29:58 +0100
Subject: [PATCH 09/23] hnnnggh
---
packages/svelte/tests/helpers.js | 19 +++++++++++--------
1 file changed, 11 insertions(+), 8 deletions(-)
diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index 0d4a220f78a9..45b90240c99a 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -173,12 +173,15 @@ export function write(file, contents) {
fs.writeFileSync(file, contents);
}
-// @ts-expect-error JS DOM doesn't support it
-Window.prototype.matchMedia = (media) => {
- return {
- matches: false,
- media,
- addEventListener: () => {},
- removeEventListener: () => {}
+// Guard because not all test contexts load this with JSDOM
+if (typeof window !== 'undefined') {
+ // @ts-expect-error JS DOM doesn't support it
+ Window.prototype.matchMedia = (media) => {
+ return {
+ matches: false,
+ media,
+ addEventListener: () => {},
+ removeEventListener: () => {}
+ };
};
-};
+}
From 42cd56302f9f7a88cd4fe100265f85ba73a8cd19 Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Mon, 25 Nov 2024 20:45:32 +0100
Subject: [PATCH 10/23] mark as pure
---
packages/svelte/src/motion/index.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/packages/svelte/src/motion/index.js b/packages/svelte/src/motion/index.js
index 295e32eb0d65..c5ff5b02f22b 100644
--- a/packages/svelte/src/motion/index.js
+++ b/packages/svelte/src/motion/index.js
@@ -7,4 +7,6 @@ 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)');
+export const prefersReducedMotion = /*@__PURE__*/ new MediaQuery(
+ '(prefers-reduced-motion: reduce)'
+);
From 075b6dbba832d764882d2b44b465bdaa92b0eaad Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Mon, 25 Nov 2024 21:03:09 +0100
Subject: [PATCH 11/23] fix type check
---
packages/svelte/tsconfig.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json
index 017b92629866..380307901edd 100644
--- a/packages/svelte/tsconfig.json
+++ b/packages/svelte/tsconfig.json
@@ -25,6 +25,7 @@
"svelte/motion": ["./src/motion/public.d.ts"],
"svelte/server": ["./src/server/index.d.ts"],
"svelte/store": ["./src/store/public.d.ts"],
+ "svelte/reactivity": ["./src/reactivity/index-client.js"],
"#compiler": ["./src/compiler/types/index.d.ts"],
"#client": ["./src/internal/client/types.d.ts"],
"#server": ["./src/internal/server/types.d.ts"],
From 1a6b8f37d3efe85337f1599443f9617cb8dcc166 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Tue, 26 Nov 2024 15:27:46 -0500
Subject: [PATCH 12/23] notify -> subscribe
---
packages/svelte/src/reactivity/media-query.js | 4 ++--
packages/svelte/src/store/index-client.js | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index b93eddf4e320..6a5e3da478a8 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -11,14 +11,14 @@ import { increment } from './utils.js';
export class MediaQuery {
#query;
#version = source(0);
- #notify = createSubscriber(() => {
+ #subscribe = createSubscriber(() => {
return on(this.#query, 'change', () => increment(this.#version));
});
get current() {
if (effect_tracking()) {
get(this.#version);
- this.#notify();
+ this.#subscribe();
}
return this.#query.matches;
diff --git a/packages/svelte/src/store/index-client.js b/packages/svelte/src/store/index-client.js
index 1140f4b18c5c..28be7982911b 100644
--- a/packages/svelte/src/store/index-client.js
+++ b/packages/svelte/src/store/index-client.js
@@ -111,7 +111,7 @@ export function fromStore(store) {
let value = /** @type {V} */ (undefined);
let version = source(0);
- const notify = createSubscriber(() => {
+ const subscribe = createSubscriber(() => {
let ran = false;
const unsubscribe = store.subscribe((v) => {
@@ -127,7 +127,7 @@ export function fromStore(store) {
function current() {
if (effect_tracking()) {
get_source(version);
- notify();
+ subscribe();
return value;
}
From d7b331f7070f3a15f10dd2d7e5976a34627f2f6b Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Tue, 26 Nov 2024 15:30:40 -0500
Subject: [PATCH 13/23] add links to inline docs
---
packages/svelte/src/motion/index.js | 2 +-
packages/svelte/types/index.d.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/svelte/src/motion/index.js b/packages/svelte/src/motion/index.js
index c5ff5b02f22b..3db1a55baa45 100644
--- a/packages/svelte/src/motion/index.js
+++ b/packages/svelte/src/motion/index.js
@@ -4,7 +4,7 @@ export * from './spring.js';
export * from './tweened.js';
/**
- * A media query that matches if the user has requested reduced motion.
+ * 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).
* @type {MediaQuery}
*/
export const prefersReducedMotion = /*@__PURE__*/ new MediaQuery(
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 2e9d3d188bda..07280899fc85 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1678,7 +1678,7 @@ declare module 'svelte/motion' {
interpolate?: (a: T, b: T) => (t: number) => T;
}
/**
- * A media query that matches if the user has requested reduced motion.
+ * 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).
* */
export const prefersReducedMotion: MediaQuery;
/**
From 5afa6f3cc3b0c583530d5757ca235d37e30570e5 Mon Sep 17 00:00:00 2001
From: Simon Holthausen
Date: Wed, 27 Nov 2024 11:11:45 +0100
Subject: [PATCH 14/23] better API, more docs
---
.../src/reactivity/create-subscriber.js | 42 +++++++++++++++++--
packages/svelte/src/reactivity/media-query.js | 14 ++-----
packages/svelte/src/store/index-client.js | 9 +---
packages/svelte/types/index.d.ts | 33 ++++++++++++++-
4 files changed, 74 insertions(+), 24 deletions(-)
diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js
index 5911b4936c68..dd7af37b1594 100644
--- a/packages/svelte/src/reactivity/create-subscriber.js
+++ b/packages/svelte/src/reactivity/create-subscriber.js
@@ -1,23 +1,57 @@
-import { tick, untrack } from '../internal/client/runtime.js';
+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 function that, when invoked in a reactive context, calls the `start` function once,
* and calls the `stop` function returned from `start` when all reactive contexts it's called in
* are destroyed. This is useful for creating a notifier that starts and stops when the
- * "subscriber" count goes from 0 to 1 and back to 0.
- * @param {() => (() => void) | void} start
+ * "subscriber" count goes from 0 to 1 and back to 0. Call the `update` function passed to the
+ * `start` function to notify subscribers of an update.
+ *
+ * Usage example (reimplementing `MediaQuery`):
+ *
+ * ```js
+ * import { createSubscriber, on } from 'svelte/reactivity';
+ *
+ * export class MediaQuery {
+ * #query;
+ * #subscribe = createSubscriber((update) => {
+ * // add an event listener to update all subscribers when the match changes
+ * return on(this.#query, 'change', update);
+ * });
+ *
+ * get current() {
+ * // If the `current` property is accessed in a reactive context, start a new
+ * // subscription if there isn't one already. The subscription will under the
+ * // hood ensure that whatever reactive context reads `current` will rerun when
+ * // the match changes
+ * this.#subscribe();
+ * // Return the current state of the query
+ * return this.#query.matches;
+ * }
+ *
+ * constructor(query) {
+ * this.#query = window.matchMedia(`(${query})`);
+ * }
+ * }
+ * ```
+ * @param {(update: () => void) => (() => void) | void} start
*/
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);
+ stop = untrack(() => start(() => increment(version)));
}
subscribers += 1;
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index 6a5e3da478a8..463454bbb2c8 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -1,25 +1,17 @@
-import { get } from '../internal/client/runtime.js';
-import { source } from '../internal/client/reactivity/sources.js';
-import { effect_tracking } from '../internal/client/reactivity/effects.js';
import { createSubscriber } from './create-subscriber.js';
import { on } from '../events/index.js';
-import { increment } from './utils.js';
/**
* Creates a media query and provides a `current` property that reflects whether or not it matches.
*/
export class MediaQuery {
#query;
- #version = source(0);
- #subscribe = createSubscriber(() => {
- return on(this.#query, 'change', () => increment(this.#version));
+ #subscribe = createSubscriber((update) => {
+ return on(this.#query, 'change', update);
});
get current() {
- if (effect_tracking()) {
- get(this.#version);
- this.#subscribe();
- }
+ this.#subscribe();
return this.#query.matches;
}
diff --git a/packages/svelte/src/store/index-client.js b/packages/svelte/src/store/index-client.js
index 28be7982911b..ae6806ec763f 100644
--- a/packages/svelte/src/store/index-client.js
+++ b/packages/svelte/src/store/index-client.js
@@ -4,9 +4,6 @@ import {
effect_tracking,
render_effect
} from '../internal/client/reactivity/effects.js';
-import { source } from '../internal/client/reactivity/sources.js';
-import { get as get_source } from '../internal/client/runtime.js';
-import { increment } from '../reactivity/utils.js';
import { get, writable } from './shared/index.js';
import { createSubscriber } from '../reactivity/create-subscriber.js';
@@ -109,14 +106,13 @@ export function toStore(get, set) {
*/
export function fromStore(store) {
let value = /** @type {V} */ (undefined);
- let version = source(0);
- const subscribe = createSubscriber(() => {
+ const subscribe = createSubscriber((update) => {
let ran = false;
const unsubscribe = store.subscribe((v) => {
value = v;
- if (ran) increment(version);
+ if (ran) update();
});
ran = true;
@@ -126,7 +122,6 @@ export function fromStore(store) {
function current() {
if (effect_tracking()) {
- get_source(version);
subscribe();
return value;
}
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 07280899fc85..4cd31359e2ed 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1741,9 +1741,38 @@ declare module 'svelte/reactivity' {
* Returns a function that, when invoked in a reactive context, calls the `start` function once,
* and calls the `stop` function returned from `start` when all reactive contexts it's called in
* are destroyed. This is useful for creating a notifier that starts and stops when the
- * "subscriber" count goes from 0 to 1 and back to 0.
+ * "subscriber" count goes from 0 to 1 and back to 0. Call the `update` function passed to the
+ * `start` function to notify subscribers of an update.
+ *
+ * Usage example (reimplementing `MediaQuery`):
+ *
+ * ```js
+ * import { createSubscriber, on } from 'svelte/reactivity';
+ *
+ * export class MediaQuery {
+ * #query;
+ * #subscribe = createSubscriber((update) => {
+ * // add an event listener to update all subscribers when the match changes
+ * return on(this.#query, 'change', update);
+ * });
+ *
+ * get current() {
+ * // If the `current` property is accessed in a reactive context, start a new
+ * // subscription if there isn't one already. The subscription will under the
+ * // hood ensure that whatever reactive context reads `current` will rerun when
+ * // the match changes
+ * this.#subscribe();
+ * // Return the current state of the query
+ * return this.#query.matches;
+ * }
+ *
+ * constructor(query) {
+ * this.#query = window.matchMedia(`(${query})`);
+ * }
+ * }
+ * ```
* */
- export function createSubscriber(start: () => (() => void) | void): () => void;
+ export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void;
export {};
}
From 7900d9b3d4cd41c611323460224d63d0f0cd3275 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 2 Dec 2024 17:24:02 -0500
Subject: [PATCH 15/23] add example to prefersReducedMotion
---
packages/svelte/src/motion/index.js | 19 +++++++++++++++++++
packages/svelte/types/index.d.ts | 17 +++++++++++++++++
2 files changed, 36 insertions(+)
diff --git a/packages/svelte/src/motion/index.js b/packages/svelte/src/motion/index.js
index 3db1a55baa45..022be7bc1a1b 100644
--- a/packages/svelte/src/motion/index.js
+++ b/packages/svelte/src/motion/index.js
@@ -5,6 +5,25 @@ 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
+ *
+ *
+ *
+ *
+ * {#if visible}
+ *
+ * flies in, unless the user prefers reduced motion
+ *
+ * {/if}
+ * ```
* @type {MediaQuery}
*/
export const prefersReducedMotion = /*@__PURE__*/ new MediaQuery(
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 66396ec12eda..7f456ffad450 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1686,6 +1686,23 @@ declare module 'svelte/motion' {
}
/**
* 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
+ *
+ *
+ *
+ *
+ * {#if visible}
+ *
+ * flies in, unless the user prefers reduced motion
+ *
+ * {/if}
+ * ```
* */
export const prefersReducedMotion: MediaQuery;
/**
From 4e59abc444e50f457ca667a4a17f35023b34d045 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 2 Dec 2024 17:30:32 -0500
Subject: [PATCH 16/23] add example for MediaQuery
---
packages/svelte/src/reactivity/media-query.js | 12 ++++++++++++
packages/svelte/types/index.d.ts | 18 ++++++++++++++++--
2 files changed, 28 insertions(+), 2 deletions(-)
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index 463454bbb2c8..28e940589615 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -3,6 +3,18 @@ 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.
+ *
+ * ```svelte
+ *
+ *
+ *
- * flies in, unless the user prefers reduced motion
+ * flies in, unless the user prefers reduced motion
*
* {/if}
* ```
@@ -1751,6 +1753,18 @@ declare module 'svelte/reactivity' {
}
/**
* 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.
+ *
+ * ```svelte
+ *
+ *
+ *
{large.current ? 'large screen' : 'small screen'}
+ * ```
*/
export class MediaQuery {
/**
From 0e689c84786bd2ed7bb079ef683f96bd179ff242 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 2 Dec 2024 17:37:40 -0500
Subject: [PATCH 17/23] typo
---
packages/svelte/src/reactivity/media-query.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index 28e940589615..a5cda5e0f8a2 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -33,7 +33,7 @@ export class MediaQuery {
* @param {boolean} [matches] Fallback value for the server
*/
constructor(query, matches) {
- // For convenience (and because people likely forget them) we add the parentheses; double parantheses are not a problem
+ // For convenience (and because people likely forget them) we add the parentheses; double parentheses are not a problem
this.#query = window.matchMedia(`(${query})`);
}
}
From c3b3e1480762095af85f08114bce8ae27f28a777 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 2 Dec 2024 17:39:35 -0500
Subject: [PATCH 18/23] fix example
---
packages/svelte/src/reactivity/create-subscriber.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js
index dd7af37b1594..44552a995542 100644
--- a/packages/svelte/src/reactivity/create-subscriber.js
+++ b/packages/svelte/src/reactivity/create-subscriber.js
@@ -13,7 +13,8 @@ import { increment } from './utils.js';
* Usage example (reimplementing `MediaQuery`):
*
* ```js
- * import { createSubscriber, on } from 'svelte/reactivity';
+ * import { createSubscriber } from 'svelte/reactivity';
+ * import { on } from 'svelte/events';
*
* export class MediaQuery {
* #query;
From 1f0ad0a7460e6ba24ed684e684150d9e1d3a4968 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 2 Dec 2024 18:32:00 -0500
Subject: [PATCH 19/23] tweak docs
---
.../src/reactivity/create-subscriber.js | 42 +++++++++--------
packages/svelte/types/index.d.ts | 45 ++++++++++---------
2 files changed, 48 insertions(+), 39 deletions(-)
diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js
index 44552a995542..085660f86ee3 100644
--- a/packages/svelte/src/reactivity/create-subscriber.js
+++ b/packages/svelte/src/reactivity/create-subscriber.js
@@ -4,13 +4,15 @@ import { source } from '../internal/client/reactivity/sources.js';
import { increment } from './utils.js';
/**
- * Returns a function that, when invoked in a reactive context, calls the `start` function once,
- * and calls the `stop` function returned from `start` when all reactive contexts it's called in
- * are destroyed. This is useful for creating a notifier that starts and stops when the
- * "subscriber" count goes from 0 to 1 and back to 0. Call the `update` function passed to the
- * `start` function to notify subscribers of an update.
+ * 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.
*
- * Usage example (reimplementing `MediaQuery`):
+ * 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';
@@ -18,23 +20,25 @@ import { increment } from './utils.js';
*
* export class MediaQuery {
* #query;
- * #subscribe = createSubscriber((update) => {
- * // add an event listener to update all subscribers when the match changes
- * return on(this.#query, 'change', update);
- * });
+ * #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() {
- * // If the `current` property is accessed in a reactive context, start a new
- * // subscription if there isn't one already. The subscription will under the
- * // hood ensure that whatever reactive context reads `current` will rerun when
- * // the match changes
* this.#subscribe();
- * // Return the current state of the query
- * return this.#query.matches;
- * }
*
- * constructor(query) {
- * this.#query = window.matchMedia(`(${query})`);
+ * // Return the current state of the query, whether or not we're in an effect
+ * return this.#query.matches;
* }
* }
* ```
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index a98e53eb04c0..7e4e58f97f29 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1776,36 +1776,41 @@ declare module 'svelte/reactivity' {
#private;
}
/**
- * Returns a function that, when invoked in a reactive context, calls the `start` function once,
- * and calls the `stop` function returned from `start` when all reactive contexts it's called in
- * are destroyed. This is useful for creating a notifier that starts and stops when the
- * "subscriber" count goes from 0 to 1 and back to 0. Call the `update` function passed to the
- * `start` function to notify subscribers of an update.
+ * 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.
*
- * Usage example (reimplementing `MediaQuery`):
+ * 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, on } from 'svelte/reactivity';
+ * import { createSubscriber } from 'svelte/reactivity';
+ * import { on } from 'svelte/events';
*
* export class MediaQuery {
* #query;
- * #subscribe = createSubscriber((update) => {
- * // add an event listener to update all subscribers when the match changes
- * return on(this.#query, 'change', update);
- * });
+ * #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() {
- * // If the `current` property is accessed in a reactive context, start a new
- * // subscription if there isn't one already. The subscription will under the
- * // hood ensure that whatever reactive context reads `current` will rerun when
- * // the match changes
* this.#subscribe();
- * // Return the current state of the query
- * return this.#query.matches;
- * }
*
- * constructor(query) {
- * this.#query = window.matchMedia(`(${query})`);
+ * // Return the current state of the query, whether or not we're in an effect
+ * return this.#query.matches;
* }
* }
* ```
From 60aaaaca24537255d99f5c3c1313599c829dfd36 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 2 Dec 2024 18:33:46 -0500
Subject: [PATCH 20/23] changesets
---
.changeset/popular-worms-repeat.md | 5 +++++
.changeset/quiet-tables-cheat.md | 5 +++++
2 files changed, 10 insertions(+)
create mode 100644 .changeset/popular-worms-repeat.md
create mode 100644 .changeset/quiet-tables-cheat.md
diff --git a/.changeset/popular-worms-repeat.md b/.changeset/popular-worms-repeat.md
new file mode 100644
index 000000000000..68d9f9a3e80e
--- /dev/null
+++ b/.changeset/popular-worms-repeat.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: add `createSubscriber` function for creating reactive values that depend on subscriptions
diff --git a/.changeset/quiet-tables-cheat.md b/.changeset/quiet-tables-cheat.md
new file mode 100644
index 000000000000..92e9c266cc90
--- /dev/null
+++ b/.changeset/quiet-tables-cheat.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: add reactive `MediaQuery` class, and a `prefersReducedMotion` class instance
From 8288220a6a906f416bcbeaa0801abfcd00c23c2a Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Thu, 5 Dec 2024 10:10:22 -0500
Subject: [PATCH 21/23] note when APIs were added
---
packages/svelte/src/motion/index.js | 1 +
packages/svelte/src/reactivity/create-subscriber.js | 1 +
packages/svelte/src/reactivity/media-query.js | 1 +
packages/svelte/types/index.d.ts | 7 +++++--
4 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/packages/svelte/src/motion/index.js b/packages/svelte/src/motion/index.js
index 022be7bc1a1b..f4262a565024 100644
--- a/packages/svelte/src/motion/index.js
+++ b/packages/svelte/src/motion/index.js
@@ -25,6 +25,7 @@ export * from './tweened.js';
* {/if}
* ```
* @type {MediaQuery}
+ * @since 5.7.0
*/
export const prefersReducedMotion = /*@__PURE__*/ new MediaQuery(
'(prefers-reduced-motion: reduce)'
diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js
index 085660f86ee3..63deca62ea8b 100644
--- a/packages/svelte/src/reactivity/create-subscriber.js
+++ b/packages/svelte/src/reactivity/create-subscriber.js
@@ -43,6 +43,7 @@ import { increment } from './utils.js';
* }
* ```
* @param {(update: () => void) => (() => void) | void} start
+ * @since 5.7.0
*/
export function createSubscriber(start) {
let subscribers = 0;
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index a5cda5e0f8a2..ce5e6d4e1fb1 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -15,6 +15,7 @@ import { on } from '../events/index.js';
*
*
* {/if}
* ```
- * */
+ * @since 5.7.0
+ */
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.
@@ -1765,6 +1766,7 @@ declare module 'svelte/reactivity' {
*
*
{large.current ? 'large screen' : 'small screen'}
* ```
+ * @since 5.7.0
*/
export class MediaQuery {
/**
@@ -1814,7 +1816,8 @@ declare module 'svelte/reactivity' {
* }
* }
* ```
- * */
+ * @since 5.7.0
+ */
export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void;
export {};
From df97184069584ef21170d50da222a7d442150053 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Thu, 5 Dec 2024 10:18:02 -0500
Subject: [PATCH 22/23] add note
---
packages/svelte/src/reactivity/media-query.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index ce5e6d4e1fb1..a2be0adc91e2 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -5,6 +5,7 @@ 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
*