feat: add `svelte/reactivity/window` module (#14660)

* feat: add `svelte/reactivity/window` module

* lint

* fix

* hide private types

* online binding

* tweak docs

* tweak

* add @since tags

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/14662/head
Rich Harris 2 weeks ago committed by GitHub
parent a2539cfe1f
commit d43a10ba7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: add `svelte/reactivity/window` module

@ -0,0 +1,15 @@
---
title: svelte/reactivity/window
---
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>`](svelte-window) bindings or manually creating your own event listeners.
```svelte
<script>
import { innerWidth, innerHeight } from 'svelte/reactivity/window';
</script>
<p>{innerWidth.current}x{innerHeight.current}</p>
```
> MODULE: svelte/reactivity/window

@ -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"

@ -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`,

@ -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;

@ -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';
*
* <h1>{large.current ? 'large screen' : 'small screen'}</h1>
* ```
* @extends {ReactiveValue<boolean>}
* @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)
);
}
}

@ -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();
}
}

@ -0,0 +1,154 @@
import { BROWSER } from 'esm-env';
import { on } from '../../events/index.js';
import { ReactiveValue } from '../reactive-value.js';
import { get } from '../../internal/client/index.js';
import { set, source } from '../../internal/client/reactivity/sources.js';
/**
* `scrollX.current` is a reactive view of `window.scrollX`. On the server it is `undefined`.
* @since 5.11.0
*/
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`.
* @since 5.11.0
*/
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`.
* @since 5.11.0
*/
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`.
* @since 5.11.0
*/
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`.
* @since 5.11.0
*/
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`.
* @since 5.11.0
*/
export const outerHeight = new ReactiveValue(
BROWSER ? () => window.outerHeight : () => undefined,
(update) => on(window, 'resize', update)
);
/**
* `screenLeft.current` is a reactive view of `window.screenLeft`. It is updated inside a `requestAnimationFrame` callback. On the server it is `undefined`.
* @since 5.11.0
*/
export const screenLeft = new ReactiveValue(
BROWSER ? () => window.screenLeft : () => undefined,
(update) => {
let value = window.screenLeft;
let frame = requestAnimationFrame(function check() {
frame = requestAnimationFrame(check);
if (value !== (value = window.screenLeft)) {
update();
}
});
return () => {
cancelAnimationFrame(frame);
};
}
);
/**
* `screenTop.current` is a reactive view of `window.screenTop`. It is updated inside a `requestAnimationFrame` callback. On the server it is `undefined`.
* @since 5.11.0
*/
export const screenTop = new ReactiveValue(
BROWSER ? () => window.screenTop : () => undefined,
(update) => {
let value = window.screenTop;
let frame = requestAnimationFrame(function check() {
frame = requestAnimationFrame(check);
if (value !== (value = window.screenTop)) {
update();
}
});
return () => {
cancelAnimationFrame(frame);
};
}
);
/**
* `online.current` is a reactive view of `navigator.onLine`. On the server it is `undefined`.
* @since 5.11.0
*/
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,
* on Firefox and Safari it won't.
* @type {{ get current(): number }}
* @since 5.11.0
*/
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;
}
})();

@ -1900,16 +1900,15 @@ declare module 'svelte/reactivity' {
*
* <h1>{large.current ? 'large screen' : 'small screen'}</h1>
* ```
* @extends {ReactiveValue<boolean>}
* @since 5.7.0
*/
export class MediaQuery {
export class MediaQuery extends ReactiveValue<boolean> {
/**
* @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,77 @@ declare module 'svelte/reactivity' {
* @since 5.7.0
*/
export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void;
class ReactiveValue<T> {
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`.
* @since 5.11.0
*/
export const scrollX: ReactiveValue<number | undefined>;
/**
* `scrollY.current` is a reactive view of `window.scrollY`. On the server it is `undefined`.
* @since 5.11.0
*/
export const scrollY: ReactiveValue<number | undefined>;
/**
* `innerWidth.current` is a reactive view of `window.innerWidth`. On the server it is `undefined`.
* @since 5.11.0
*/
export const innerWidth: ReactiveValue<number | undefined>;
/**
* `innerHeight.current` is a reactive view of `window.innerHeight`. On the server it is `undefined`.
* @since 5.11.0
*/
export const innerHeight: ReactiveValue<number | undefined>;
/**
* `outerWidth.current` is a reactive view of `window.outerWidth`. On the server it is `undefined`.
* @since 5.11.0
*/
export const outerWidth: ReactiveValue<number | undefined>;
/**
* `outerHeight.current` is a reactive view of `window.outerHeight`. On the server it is `undefined`.
* @since 5.11.0
*/
export const outerHeight: ReactiveValue<number | undefined>;
/**
* `screenLeft.current` is a reactive view of `window.screenLeft`. It is updated inside a `requestAnimationFrame` callback. On the server it is `undefined`.
* @since 5.11.0
*/
export const screenLeft: ReactiveValue<number | undefined>;
/**
* `screenTop.current` is a reactive view of `window.screenTop`. It is updated inside a `requestAnimationFrame` callback. On the server it is `undefined`.
* @since 5.11.0
*/
export const screenTop: ReactiveValue<number | undefined>;
/**
* `online.current` is a reactive view of `navigator.onLine`. On the server it is `undefined`.
* @since 5.11.0
*/
export const online: ReactiveValue<boolean | undefined>;
/**
* `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.
* @since 5.11.0
*/
export const devicePixelRatio: {
get current(): number;
};
class ReactiveValue<T> {
constructor(fn: () => T, onsubscribe: (update: () => void) => void);
get current(): T;
#private;
}
export {};
}

Loading…
Cancel
Save