From ea6f89d604b7c7f56fbf85a5d633a4f7c70707f3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Dec 2024 13:47:37 -0500 Subject: [PATCH 1/8] feat: add `svelte/reactivity/window` module --- .changeset/orange-ducks-obey.md | 5 + .../21-svelte-reactivity-window.md | 15 ++ packages/svelte/package.json | 4 + packages/svelte/scripts/generate-types.js | 1 + .../svelte/src/internal/client/dom/task.js | 2 +- packages/svelte/src/reactivity/media-query.js | 27 +-- .../svelte/src/reactivity/reactive-value.js | 24 +++ .../svelte/src/reactivity/window/index.js | 128 +++++++++++ packages/svelte/types/index.d.ts | 202 +++++++++++++++++- 9 files changed, 385 insertions(+), 23 deletions(-) create mode 100644 .changeset/orange-ducks-obey.md create mode 100644 documentation/docs/98-reference/21-svelte-reactivity-window.md create mode 100644 packages/svelte/src/reactivity/reactive-value.js create mode 100644 packages/svelte/src/reactivity/window/index.js diff --git a/.changeset/orange-ducks-obey.md b/.changeset/orange-ducks-obey.md new file mode 100644 index 000000000000..e17bf7ad4289 --- /dev/null +++ b/.changeset/orange-ducks-obey.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `svelte/reactivity/window` module diff --git a/documentation/docs/98-reference/21-svelte-reactivity-window.md b/documentation/docs/98-reference/21-svelte-reactivity-window.md new file mode 100644 index 000000000000..52473e0f7540 --- /dev/null +++ b/documentation/docs/98-reference/21-svelte-reactivity-window.md @@ -0,0 +1,15 @@ +--- +title: svelte/reactivity/window +--- + +This module exports reactive versions of various `window` properties, which you can use in components and [deriveds]($derived) and [effects]($effect) without using [``](svelte-window) bindings or manually creating your own event listeners. + +```svelte + + +

{innerWidth.current}x{innerHeight.current}

+``` + +> MODULE: svelte/reactivity/window diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 62160514bb5d..dedc408a100a 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -73,6 +73,10 @@ "browser": "./src/reactivity/index-client.js", "default": "./src/reactivity/index-server.js" }, + "./reactivity/window": { + "types": "./types/index.d.ts", + "default": "./src/reactivity/window/index.js" + }, "./server": { "types": "./types/index.d.ts", "default": "./src/server/index.js" diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js index 8083851c35db..16bbf52a2e07 100644 --- a/packages/svelte/scripts/generate-types.js +++ b/packages/svelte/scripts/generate-types.js @@ -35,6 +35,7 @@ await createBundle({ [`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`, [`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`, [`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`, + [`${pkg.name}/reactivity/window`]: `${dir}/src/reactivity/window/index.js`, [`${pkg.name}/server`]: `${dir}/src/server/index.d.ts`, [`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`, [`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`, diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 95526b27a769..acb5a5b117f0 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -1,7 +1,7 @@ import { run_all } from '../../shared/utils.js'; // Fallback for when requestIdleCallback is not available -const request_idle_callback = +export const request_idle_callback = typeof requestIdleCallback === 'undefined' ? (/** @type {() => void} */ cb) => setTimeout(cb, 1) : requestIdleCallback; diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js index a2be0adc91e2..7da9ada667f9 100644 --- a/packages/svelte/src/reactivity/media-query.js +++ b/packages/svelte/src/reactivity/media-query.js @@ -1,5 +1,5 @@ -import { createSubscriber } from './create-subscriber.js'; import { on } from '../events/index.js'; +import { ReactiveValue } from './reactive-value.js'; /** * Creates a media query and provides a `current` property that reflects whether or not it matches. @@ -16,26 +16,19 @@ import { on } from '../events/index.js'; * *

{large.current ? 'large screen' : 'small screen'}

* ``` + * @extends {ReactiveValue} * @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; - } - +export class MediaQuery extends ReactiveValue { /** * @param {string} query A media query string - * @param {boolean} [matches] Fallback value for the server + * @param {boolean} [fallback] 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})`); + constructor(query, fallback) { + const q = window.matchMedia(`(${query})`); + super( + () => q.matches, + (update) => on(q, 'change', update) + ); } } diff --git a/packages/svelte/src/reactivity/reactive-value.js b/packages/svelte/src/reactivity/reactive-value.js new file mode 100644 index 000000000000..c7c14ad7cd7a --- /dev/null +++ b/packages/svelte/src/reactivity/reactive-value.js @@ -0,0 +1,24 @@ +import { createSubscriber } from './create-subscriber.js'; + +/** + * @template T + */ +export class ReactiveValue { + #fn; + #subscribe; + + /** + * + * @param {() => T} fn + * @param {(update: () => void) => void} onsubscribe + */ + constructor(fn, onsubscribe) { + this.#fn = fn; + this.#subscribe = createSubscriber(onsubscribe); + } + + get current() { + this.#subscribe(); + return this.#fn(); + } +} diff --git a/packages/svelte/src/reactivity/window/index.js b/packages/svelte/src/reactivity/window/index.js new file mode 100644 index 000000000000..9aa47fe7a459 --- /dev/null +++ b/packages/svelte/src/reactivity/window/index.js @@ -0,0 +1,128 @@ +import { BROWSER } from 'esm-env'; +import { on } from '../../events/index.js'; +import { ReactiveValue } from '../reactive-value.js'; +import { get } from '../../internal/client'; +import { set, source } from '../../internal/client/reactivity/sources.js'; + +/** + * `scrollX.current` is a reactive view of `window.scrollX`. On the server it is `undefined` + */ +export const scrollX = new ReactiveValue( + BROWSER ? () => window.scrollX : () => undefined, + (update) => on(window, 'scroll', update) +); + +/** + * `scrollY.current` is a reactive view of `window.scrollY`. On the server it is `undefined` + */ +export const scrollY = new ReactiveValue( + BROWSER ? () => window.scrollY : () => undefined, + (update) => on(window, 'scroll', update) +); + +/** + * `innerWidth.current` is a reactive view of `window.innerWidth`. On the server it is `undefined` + */ +export const innerWidth = new ReactiveValue( + BROWSER ? () => window.innerWidth : () => undefined, + (update) => on(window, 'resize', update) +); + +/** + * `innerHeight.current` is a reactive view of `window.innerHeight`. On the server it is `undefined` + */ +export const innerHeight = new ReactiveValue( + BROWSER ? () => window.innerHeight : () => undefined, + (update) => on(window, 'resize', update) +); + +/** + * `outerWidth.current` is a reactive view of `window.outerWidth`. On the server it is `undefined` + */ +export const outerWidth = new ReactiveValue( + BROWSER ? () => window.outerWidth : () => undefined, + (update) => on(window, 'resize', update) +); + +/** + * `outerHeight.current` is a reactive view of `window.outerHeight`. On the server it is `undefined` + */ +export const outerHeight = new ReactiveValue( + BROWSER ? () => window.outerHeight : () => undefined, + (update) => on(window, 'resize', update) +); + +/** + * `screenLeft.current` is a reactive view of `window.screenLeft`. On the server it is `undefined` + */ +export const screenLeft = new ReactiveValue( + BROWSER ? () => window.screenLeft : () => undefined, + (update) => { + let screenLeft = window.screenLeft; + + let frame = requestAnimationFrame(function check() { + frame = requestAnimationFrame(check); + + if (screenLeft !== (screenLeft = window.screenLeft)) { + update(); + } + }); + + return () => { + cancelAnimationFrame(frame); + }; + } +); + +/** + * `screenTop.current` is a reactive view of `window.screenTop`. On the server it is `undefined` + */ +export const screenTop = new ReactiveValue( + BROWSER ? () => window.screenTop : () => undefined, + (update) => { + let screenTop = window.screenTop; + + let frame = requestAnimationFrame(function check() { + frame = requestAnimationFrame(check); + + if (screenTop !== (screenTop = window.screenTop)) { + update(); + } + }); + + return () => { + cancelAnimationFrame(frame); + }; + } +); + +/** + * `devicePixelRatio.current` is a reactive view of `window.devicePixelRatio`. On the server it is `undefined`. + * Note that behaviour differs between browsers — on Chrome it will respond to the current zoom level, + * on Firefox and Safari it won't + */ +export const devicePixelRatio = /* @__PURE__ */ new (class DevicePixelRatio { + #dpr = source(BROWSER ? window.devicePixelRatio : undefined); + + #update() { + const off = on( + window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`), + 'change', + () => { + set(this.#dpr, window.devicePixelRatio); + + off(); + this.#update(); + } + ); + } + + constructor() { + this.#update(); + } + + get current() { + get(this.#dpr); + return window.devicePixelRatio; + } +})(); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 61a34dcb8e93..d1d654024b4a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1900,16 +1900,15 @@ declare module 'svelte/reactivity' { * *

{large.current ? 'large screen' : 'small screen'}

* ``` + * @extends {ReactiveValue} * @since 5.7.0 */ - export class MediaQuery { + export class MediaQuery extends ReactiveValue { /** * @param query A media query string - * @param matches Fallback value for the server + * @param fallback Fallback value for the server */ - constructor(query: string, matches?: boolean | undefined); - get current(): boolean; - #private; + constructor(query: string, fallback?: boolean | undefined); } /** * Returns a `subscribe` function that, if called in an effect (including expressions in the template), @@ -1953,6 +1952,199 @@ declare module 'svelte/reactivity' { * @since 5.7.0 */ export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void; + class ReactiveValue { + + constructor(fn: () => T, onsubscribe: (update: () => void) => void); + get current(): T; + #private; + } + + export {}; +} + +declare module 'svelte/reactivity/window' { + /** + * `scrollX.current` is a reactive view of `window.scrollX`. On the server it is `undefined` + */ + export const scrollX: ReactiveValue; + /** + * `scrollY.current` is a reactive view of `window.scrollY`. On the server it is `undefined` + */ + export const scrollY: ReactiveValue; + /** + * `innerWidth.current` is a reactive view of `window.innerWidth`. On the server it is `undefined` + */ + export const innerWidth: ReactiveValue; + /** + * `innerHeight.current` is a reactive view of `window.innerHeight`. On the server it is `undefined` + */ + export const innerHeight: ReactiveValue; + /** + * `outerWidth.current` is a reactive view of `window.outerWidth`. On the server it is `undefined` + */ + export const outerWidth: ReactiveValue; + /** + * `outerHeight.current` is a reactive view of `window.outerHeight`. On the server it is `undefined` + */ + export const outerHeight: ReactiveValue; + /** + * `screenLeft.current` is a reactive view of `window.screenLeft`. On the server it is `undefined` + */ + export const screenLeft: ReactiveValue; + /** + * `screenTop.current` is a reactive view of `window.screenTop`. On the server it is `undefined` + */ + export const screenTop: ReactiveValue; + /** + * `devicePixelRatio.current` is a reactive view of `window.devicePixelRatio`. On the server it is `undefined`. + * Note that behaviour differs between browsers — on Chrome it will respond to the current zoom level, + * on Firefox and Safari it won't + */ + export const devicePixelRatio: { + "__#19@#dpr": Source; + "__#19@#update"(): void; + readonly current: number; + }; + // For all the core internal objects, we use single-character property strings. + // This not only reduces code-size and parsing, but it also improves the performance + // when the JS VM JITs the code. + + type ComponentContext = { + /** parent */ + p: null | ComponentContext; + /** context */ + c: null | Map; + /** deferred effects */ + e: null | Array<{ + fn: () => void | (() => void); + effect: null | Effect; + reaction: null | Reaction; + }>; + /** mounted */ + m: boolean; + /** + * props — needed for legacy mode lifecycle functions, and for `createEventDispatcher` + * @deprecated remove in 6.0 + */ + s: Record; + /** + * exports (and props, if `accessors: true`) — needed for `createEventDispatcher` + * @deprecated remove in 6.0 + */ + x: Record | null; + /** + * legacy stuff + * @deprecated remove in 6.0 + */ + l: null | { + /** local signals (needed for beforeUpdate/afterUpdate) */ + s: null | Source[]; + /** update_callbacks */ + u: null | { + /** afterUpdate callbacks */ + a: Array<() => void>; + /** beforeUpdate callbacks */ + b: Array<() => void>; + /** onMount callbacks */ + m: Array<() => any>; + }; + /** `$:` statements */ + r1: any[]; + /** This tracks whether `$:` statements have run in the current cycle, to ensure they only run once */ + r2: Source; + }; + /** + * dev mode only: the component function + */ + function?: any; + }; + + type Equals = (this: Value, value: unknown) => boolean; + + type TemplateNode = Text | Element | Comment; + + interface TransitionManager { + /** Whether the `global` modifier was used (i.e. `transition:fade|global`) */ + is_global: boolean; + /** Called inside `resume_effect` */ + in: () => void; + /** Called inside `pause_effect` */ + out: (callback?: () => void) => void; + /** Called inside `destroy_effect` */ + stop: () => void; + } + class ReactiveValue { + + constructor(fn: () => T, onsubscribe: (update: () => void) => void); + get current(): T; + #private; + } + interface Signal { + /** Flags bitmask */ + f: number; + /** Write version */ + version: number; + } + + interface Value extends Signal { + /** Signals that read from this signal */ + reactions: null | Reaction[]; + /** Equality function */ + equals: Equals; + /** The latest value for this signal */ + v: V; + } + + interface Reaction extends Signal { + /** The associated component context */ + ctx: null | ComponentContext; + /** The reaction function */ + fn: null | Function; + /** Signals that this signal reads from */ + deps: null | Value[]; + } + + interface Derived extends Value, Reaction { + /** The derived function */ + fn: () => V; + /** Reactions created inside this signal */ + children: null | Reaction[]; + /** Parent effect or derived */ + parent: Effect | Derived | null; + } + + interface Effect extends Reaction { + /** + * Branch effects store their start/end nodes so that they can be + * removed when the effect is destroyed, or moved when an `each` + * block is reconciled. In the case of a single text/element node, + * `start` and `end` will be the same. + */ + nodes_start: null | TemplateNode; + nodes_end: null | TemplateNode; + /** Reactions created inside this signal */ + deriveds: null | Derived[]; + /** The effect function */ + fn: null | (() => void | (() => void)); + /** The teardown function returned from the effect function */ + teardown: null | (() => void); + /** Transition managers created with `$.transition` */ + transitions: null | TransitionManager[]; + /** Next sibling child effect created inside the parent signal */ + prev: null | Effect; + /** Next sibling child effect created inside the parent signal */ + next: null | Effect; + /** First child effect created inside this signal */ + first: null | Effect; + /** Last child effect created inside this signal */ + last: null | Effect; + /** Parent effect */ + parent: Effect | null; + /** Dev only */ + component_function?: any; + } + + type Source = Value; export {}; } From a0ce60441355fbe1671da2ab01afc10234d20e23 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Dec 2024 14:01:36 -0500 Subject: [PATCH 2/8] lint --- packages/svelte/src/reactivity/window/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/reactivity/window/index.js b/packages/svelte/src/reactivity/window/index.js index 9aa47fe7a459..e30d0b83c645 100644 --- a/packages/svelte/src/reactivity/window/index.js +++ b/packages/svelte/src/reactivity/window/index.js @@ -58,12 +58,12 @@ export const outerHeight = new ReactiveValue( export const screenLeft = new ReactiveValue( BROWSER ? () => window.screenLeft : () => undefined, (update) => { - let screenLeft = window.screenLeft; + let value = window.screenLeft; let frame = requestAnimationFrame(function check() { frame = requestAnimationFrame(check); - if (screenLeft !== (screenLeft = window.screenLeft)) { + if (value !== (value = window.screenLeft)) { update(); } }); @@ -80,12 +80,12 @@ export const screenLeft = new ReactiveValue( export const screenTop = new ReactiveValue( BROWSER ? () => window.screenTop : () => undefined, (update) => { - let screenTop = window.screenTop; + let value = window.screenTop; let frame = requestAnimationFrame(function check() { frame = requestAnimationFrame(check); - if (screenTop !== (screenTop = window.screenTop)) { + if (value !== (value = window.screenTop)) { update(); } }); From a8d094017add89e8720214ff7ee1329fc743601d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 10 Dec 2024 14:04:11 -0500 Subject: [PATCH 3/8] fix --- packages/svelte/src/reactivity/window/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/reactivity/window/index.js b/packages/svelte/src/reactivity/window/index.js index e30d0b83c645..14c466f117f2 100644 --- a/packages/svelte/src/reactivity/window/index.js +++ b/packages/svelte/src/reactivity/window/index.js @@ -1,7 +1,7 @@ import { BROWSER } from 'esm-env'; import { on } from '../../events/index.js'; import { ReactiveValue } from '../reactive-value.js'; -import { get } from '../../internal/client'; +import { get } from '../../internal/client/index.js'; import { set, source } from '../../internal/client/reactivity/sources.js'; /** From c87d27a990d87ef8321e4bd73a8f0e2410eae37c Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 10 Dec 2024 22:47:24 +0100 Subject: [PATCH 4/8] hide private types --- .../svelte/src/reactivity/window/index.js | 1 + packages/svelte/types/index.d.ts | 140 +----------------- 2 files changed, 3 insertions(+), 138 deletions(-) diff --git a/packages/svelte/src/reactivity/window/index.js b/packages/svelte/src/reactivity/window/index.js index 14c466f117f2..bea636e0e203 100644 --- a/packages/svelte/src/reactivity/window/index.js +++ b/packages/svelte/src/reactivity/window/index.js @@ -100,6 +100,7 @@ export const screenTop = new ReactiveValue( * `devicePixelRatio.current` is a reactive view of `window.devicePixelRatio`. On the server it is `undefined`. * Note that behaviour differs between browsers — on Chrome it will respond to the current zoom level, * on Firefox and Safari it won't + * @type {{ get current(): number }} */ export const devicePixelRatio = /* @__PURE__ */ new (class DevicePixelRatio { #dpr = source(BROWSER ? window.devicePixelRatio : undefined); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d1d654024b4a..8c921fd37e57 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1999,152 +1999,16 @@ declare module 'svelte/reactivity/window' { * `devicePixelRatio.current` is a reactive view of `window.devicePixelRatio`. On the server it is `undefined`. * Note that behaviour differs between browsers — on Chrome it will respond to the current zoom level, * on Firefox and Safari it won't - */ + * */ export const devicePixelRatio: { - "__#19@#dpr": Source; - "__#19@#update"(): void; - readonly current: number; + get current(): number; }; - // For all the core internal objects, we use single-character property strings. - // This not only reduces code-size and parsing, but it also improves the performance - // when the JS VM JITs the code. - - type ComponentContext = { - /** parent */ - p: null | ComponentContext; - /** context */ - c: null | Map; - /** deferred effects */ - e: null | Array<{ - fn: () => void | (() => void); - effect: null | Effect; - reaction: null | Reaction; - }>; - /** mounted */ - m: boolean; - /** - * props — needed for legacy mode lifecycle functions, and for `createEventDispatcher` - * @deprecated remove in 6.0 - */ - s: Record; - /** - * exports (and props, if `accessors: true`) — needed for `createEventDispatcher` - * @deprecated remove in 6.0 - */ - x: Record | null; - /** - * legacy stuff - * @deprecated remove in 6.0 - */ - l: null | { - /** local signals (needed for beforeUpdate/afterUpdate) */ - s: null | Source[]; - /** update_callbacks */ - u: null | { - /** afterUpdate callbacks */ - a: Array<() => void>; - /** beforeUpdate callbacks */ - b: Array<() => void>; - /** onMount callbacks */ - m: Array<() => any>; - }; - /** `$:` statements */ - r1: any[]; - /** This tracks whether `$:` statements have run in the current cycle, to ensure they only run once */ - r2: Source; - }; - /** - * dev mode only: the component function - */ - function?: any; - }; - - type Equals = (this: Value, value: unknown) => boolean; - - type TemplateNode = Text | Element | Comment; - - interface TransitionManager { - /** Whether the `global` modifier was used (i.e. `transition:fade|global`) */ - is_global: boolean; - /** Called inside `resume_effect` */ - in: () => void; - /** Called inside `pause_effect` */ - out: (callback?: () => void) => void; - /** Called inside `destroy_effect` */ - stop: () => void; - } class ReactiveValue { constructor(fn: () => T, onsubscribe: (update: () => void) => void); get current(): T; #private; } - interface Signal { - /** Flags bitmask */ - f: number; - /** Write version */ - version: number; - } - - interface Value extends Signal { - /** Signals that read from this signal */ - reactions: null | Reaction[]; - /** Equality function */ - equals: Equals; - /** The latest value for this signal */ - v: V; - } - - interface Reaction extends Signal { - /** The associated component context */ - ctx: null | ComponentContext; - /** The reaction function */ - fn: null | Function; - /** Signals that this signal reads from */ - deps: null | Value[]; - } - - interface Derived extends Value, Reaction { - /** The derived function */ - fn: () => V; - /** Reactions created inside this signal */ - children: null | Reaction[]; - /** Parent effect or derived */ - parent: Effect | Derived | null; - } - - interface Effect extends Reaction { - /** - * Branch effects store their start/end nodes so that they can be - * removed when the effect is destroyed, or moved when an `each` - * block is reconciled. In the case of a single text/element node, - * `start` and `end` will be the same. - */ - nodes_start: null | TemplateNode; - nodes_end: null | TemplateNode; - /** Reactions created inside this signal */ - deriveds: null | Derived[]; - /** The effect function */ - fn: null | (() => void | (() => void)); - /** The teardown function returned from the effect function */ - teardown: null | (() => void); - /** Transition managers created with `$.transition` */ - transitions: null | TransitionManager[]; - /** Next sibling child effect created inside the parent signal */ - prev: null | Effect; - /** Next sibling child effect created inside the parent signal */ - next: null | Effect; - /** First child effect created inside this signal */ - first: null | Effect; - /** Last child effect created inside this signal */ - last: null | Effect; - /** Parent effect */ - parent: Effect | null; - /** Dev only */ - component_function?: any; - } - - type Source = Value; export {}; } From cb2efe6c65a930d5cb311a44edcd47550cedabf7 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 10 Dec 2024 22:54:52 +0100 Subject: [PATCH 5/8] online binding --- packages/svelte/src/reactivity/window/index.js | 15 +++++++++++++++ packages/svelte/types/index.d.ts | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/packages/svelte/src/reactivity/window/index.js b/packages/svelte/src/reactivity/window/index.js index bea636e0e203..d986d8456dba 100644 --- a/packages/svelte/src/reactivity/window/index.js +++ b/packages/svelte/src/reactivity/window/index.js @@ -96,6 +96,21 @@ export const screenTop = new ReactiveValue( } ); +/** + * `online.current` is a reactive view of `navigator.onLine`. On the server it is `undefined` + */ +export const online = new ReactiveValue( + BROWSER ? () => navigator.onLine : () => undefined, + (update) => { + const unsub_online = on(window, 'online', update); + const unsub_offline = on(window, 'offline', update); + return () => { + unsub_online(); + unsub_offline(); + }; + } +); + /** * `devicePixelRatio.current` is a reactive view of `window.devicePixelRatio`. On the server it is `undefined`. * Note that behaviour differs between browsers — on Chrome it will respond to the current zoom level, diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 8c921fd37e57..5d9874c8a3b2 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1995,6 +1995,10 @@ declare module 'svelte/reactivity/window' { * `screenTop.current` is a reactive view of `window.screenTop`. On the server it is `undefined` */ export const screenTop: ReactiveValue; + /** + * `online.current` is a reactive view of `navigator.onLine`. On the server it is `undefined` + */ + export const online: ReactiveValue; /** * `devicePixelRatio.current` is a reactive view of `window.devicePixelRatio`. On the server it is `undefined`. * Note that behaviour differs between browsers — on Chrome it will respond to the current zoom level, From 42b8d773ad8376a494276bc73d4289b9a6f05662 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 11 Dec 2024 09:54:49 -0500 Subject: [PATCH 6/8] tweak docs --- .../21-svelte-reactivity-window.md | 2 +- .../svelte/src/reactivity/window/index.js | 20 +++++++++---------- packages/svelte/types/index.d.ts | 20 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/documentation/docs/98-reference/21-svelte-reactivity-window.md b/documentation/docs/98-reference/21-svelte-reactivity-window.md index 52473e0f7540..cc544dc5ba3f 100644 --- a/documentation/docs/98-reference/21-svelte-reactivity-window.md +++ b/documentation/docs/98-reference/21-svelte-reactivity-window.md @@ -2,7 +2,7 @@ title: svelte/reactivity/window --- -This module exports reactive versions of various `window` properties, which you can use in components and [deriveds]($derived) and [effects]($effect) without using [``](svelte-window) bindings or manually creating your own event listeners. +This module exports reactive versions of various `window` values, each of which has a reactive `current` property that you can reference in reactive contexts (templates, [deriveds]($derived) and [effects]($effect)) without using [``](svelte-window) bindings or manually creating your own event listeners. ```svelte