Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading