Skip to content

Commit

Permalink
feat: add svelte/reactivity/window module (#14660)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Rich-Harris and dummdidumm authored Dec 11, 2024
1 parent a2539cf commit d43a10b
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-ducks-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: add `svelte/reactivity/window` module
15 changes: 15 additions & 0 deletions documentation/docs/98-reference/21-svelte-reactivity-window.md
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/scripts/generate-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/dom/task.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
27 changes: 10 additions & 17 deletions packages/svelte/src/reactivity/media-query.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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)
);
}
}
24 changes: 24 additions & 0 deletions packages/svelte/src/reactivity/reactive-value.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
154 changes: 154 additions & 0 deletions packages/svelte/src/reactivity/window/index.js
Original file line number Diff line number Diff line change
@@ -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;
}
})();
80 changes: 75 additions & 5 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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 {};
}
Expand Down

0 comments on commit d43a10b

Please sign in to comment.