Skip to content

Commit ea6f89d

Browse files
committed
feat: add svelte/reactivity/window module
1 parent ab1f7f4 commit ea6f89d

File tree

9 files changed

+385
-23
lines changed

9 files changed

+385
-23
lines changed

.changeset/orange-ducks-obey.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: add `svelte/reactivity/window` module
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
title: svelte/reactivity/window
3+
---
4+
5+
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>`](svelte-window) bindings or manually creating your own event listeners.
6+
7+
```svelte
8+
<script>
9+
import { innerWidth, innerHeight } from 'svelte/reactivity/window';
10+
</script>
11+
12+
<p>{innerWidth.current}x{innerHeight.current}</p>
13+
```
14+
15+
> MODULE: svelte/reactivity/window

packages/svelte/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@
7373
"browser": "./src/reactivity/index-client.js",
7474
"default": "./src/reactivity/index-server.js"
7575
},
76+
"./reactivity/window": {
77+
"types": "./types/index.d.ts",
78+
"default": "./src/reactivity/window/index.js"
79+
},
7680
"./server": {
7781
"types": "./types/index.d.ts",
7882
"default": "./src/server/index.js"

packages/svelte/scripts/generate-types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ await createBundle({
3535
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
3636
[`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`,
3737
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`,
38+
[`${pkg.name}/reactivity/window`]: `${dir}/src/reactivity/window/index.js`,
3839
[`${pkg.name}/server`]: `${dir}/src/server/index.d.ts`,
3940
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
4041
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,

packages/svelte/src/internal/client/dom/task.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { run_all } from '../../shared/utils.js';
22

33
// Fallback for when requestIdleCallback is not available
4-
const request_idle_callback =
4+
export const request_idle_callback =
55
typeof requestIdleCallback === 'undefined'
66
? (/** @type {() => void} */ cb) => setTimeout(cb, 1)
77
: requestIdleCallback;
Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { createSubscriber } from './create-subscriber.js';
21
import { on } from '../events/index.js';
2+
import { ReactiveValue } from './reactive-value.js';
33

44
/**
55
* 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';
1616
*
1717
* <h1>{large.current ? 'large screen' : 'small screen'}</h1>
1818
* ```
19+
* @extends {ReactiveValue<boolean>}
1920
* @since 5.7.0
2021
*/
21-
export class MediaQuery {
22-
#query;
23-
#subscribe = createSubscriber((update) => {
24-
return on(this.#query, 'change', update);
25-
});
26-
27-
get current() {
28-
this.#subscribe();
29-
30-
return this.#query.matches;
31-
}
32-
22+
export class MediaQuery extends ReactiveValue {
3323
/**
3424
* @param {string} query A media query string
35-
* @param {boolean} [matches] Fallback value for the server
25+
* @param {boolean} [fallback] Fallback value for the server
3626
*/
37-
constructor(query, matches) {
38-
// For convenience (and because people likely forget them) we add the parentheses; double parentheses are not a problem
39-
this.#query = window.matchMedia(`(${query})`);
27+
constructor(query, fallback) {
28+
const q = window.matchMedia(`(${query})`);
29+
super(
30+
() => q.matches,
31+
(update) => on(q, 'change', update)
32+
);
4033
}
4134
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createSubscriber } from './create-subscriber.js';
2+
3+
/**
4+
* @template T
5+
*/
6+
export class ReactiveValue {
7+
#fn;
8+
#subscribe;
9+
10+
/**
11+
*
12+
* @param {() => T} fn
13+
* @param {(update: () => void) => void} onsubscribe
14+
*/
15+
constructor(fn, onsubscribe) {
16+
this.#fn = fn;
17+
this.#subscribe = createSubscriber(onsubscribe);
18+
}
19+
20+
get current() {
21+
this.#subscribe();
22+
return this.#fn();
23+
}
24+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { BROWSER } from 'esm-env';
2+
import { on } from '../../events/index.js';
3+
import { ReactiveValue } from '../reactive-value.js';
4+
import { get } from '../../internal/client';
5+
import { set, source } from '../../internal/client/reactivity/sources.js';
6+
7+
/**
8+
* `scrollX.current` is a reactive view of `window.scrollX`. On the server it is `undefined`
9+
*/
10+
export const scrollX = new ReactiveValue(
11+
BROWSER ? () => window.scrollX : () => undefined,
12+
(update) => on(window, 'scroll', update)
13+
);
14+
15+
/**
16+
* `scrollY.current` is a reactive view of `window.scrollY`. On the server it is `undefined`
17+
*/
18+
export const scrollY = new ReactiveValue(
19+
BROWSER ? () => window.scrollY : () => undefined,
20+
(update) => on(window, 'scroll', update)
21+
);
22+
23+
/**
24+
* `innerWidth.current` is a reactive view of `window.innerWidth`. On the server it is `undefined`
25+
*/
26+
export const innerWidth = new ReactiveValue(
27+
BROWSER ? () => window.innerWidth : () => undefined,
28+
(update) => on(window, 'resize', update)
29+
);
30+
31+
/**
32+
* `innerHeight.current` is a reactive view of `window.innerHeight`. On the server it is `undefined`
33+
*/
34+
export const innerHeight = new ReactiveValue(
35+
BROWSER ? () => window.innerHeight : () => undefined,
36+
(update) => on(window, 'resize', update)
37+
);
38+
39+
/**
40+
* `outerWidth.current` is a reactive view of `window.outerWidth`. On the server it is `undefined`
41+
*/
42+
export const outerWidth = new ReactiveValue(
43+
BROWSER ? () => window.outerWidth : () => undefined,
44+
(update) => on(window, 'resize', update)
45+
);
46+
47+
/**
48+
* `outerHeight.current` is a reactive view of `window.outerHeight`. On the server it is `undefined`
49+
*/
50+
export const outerHeight = new ReactiveValue(
51+
BROWSER ? () => window.outerHeight : () => undefined,
52+
(update) => on(window, 'resize', update)
53+
);
54+
55+
/**
56+
* `screenLeft.current` is a reactive view of `window.screenLeft`. On the server it is `undefined`
57+
*/
58+
export const screenLeft = new ReactiveValue(
59+
BROWSER ? () => window.screenLeft : () => undefined,
60+
(update) => {
61+
let screenLeft = window.screenLeft;
62+
63+
let frame = requestAnimationFrame(function check() {
64+
frame = requestAnimationFrame(check);
65+
66+
if (screenLeft !== (screenLeft = window.screenLeft)) {
67+
update();
68+
}
69+
});
70+
71+
return () => {
72+
cancelAnimationFrame(frame);
73+
};
74+
}
75+
);
76+
77+
/**
78+
* `screenTop.current` is a reactive view of `window.screenTop`. On the server it is `undefined`
79+
*/
80+
export const screenTop = new ReactiveValue(
81+
BROWSER ? () => window.screenTop : () => undefined,
82+
(update) => {
83+
let screenTop = window.screenTop;
84+
85+
let frame = requestAnimationFrame(function check() {
86+
frame = requestAnimationFrame(check);
87+
88+
if (screenTop !== (screenTop = window.screenTop)) {
89+
update();
90+
}
91+
});
92+
93+
return () => {
94+
cancelAnimationFrame(frame);
95+
};
96+
}
97+
);
98+
99+
/**
100+
* `devicePixelRatio.current` is a reactive view of `window.devicePixelRatio`. On the server it is `undefined`.
101+
* Note that behaviour differs between browsers — on Chrome it will respond to the current zoom level,
102+
* on Firefox and Safari it won't
103+
*/
104+
export const devicePixelRatio = /* @__PURE__ */ new (class DevicePixelRatio {
105+
#dpr = source(BROWSER ? window.devicePixelRatio : undefined);
106+
107+
#update() {
108+
const off = on(
109+
window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`),
110+
'change',
111+
() => {
112+
set(this.#dpr, window.devicePixelRatio);
113+
114+
off();
115+
this.#update();
116+
}
117+
);
118+
}
119+
120+
constructor() {
121+
this.#update();
122+
}
123+
124+
get current() {
125+
get(this.#dpr);
126+
return window.devicePixelRatio;
127+
}
128+
})();

0 commit comments

Comments
 (0)