Skip to content

Commit c0c1a56

Browse files
feat: bind activeElement and pointerLockElement in <svelte:document> (#11879)
* feat: bind `activeElement` and `pointerLockElement` in `<svelte:document>` * add test, use focusin/focusout rather than focus/blur --------- Co-authored-by: Rich Harris <[email protected]>
1 parent f3c291d commit c0c1a56

File tree

11 files changed

+70
-3
lines changed

11 files changed

+70
-3
lines changed

.changeset/lovely-bugs-sneeze.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: bind `activeElement` and `pointerLockElement` in `<svelte:document>`

documentation/docs/02-template-syntax/07-special-elements.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,9 @@ As with `<svelte:window>`, this element may only appear the top level of your co
268268

269269
You can also bind to the following properties:
270270

271+
- `activeElement`
271272
- `fullscreenElement`
273+
- `pointerLockElement`
272274
- `visibilityState`
273275

274276
All are readonly.

packages/svelte/elements.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,7 +1364,9 @@ export interface SvelteMediaTimeRange {
13641364
}
13651365

13661366
export interface SvelteDocumentAttributes extends HTMLAttributes<Document> {
1367+
readonly 'bind:activeElement'?: Document['activeElement'] | undefined | null;
13671368
readonly 'bind:fullscreenElement'?: Document['fullscreenElement'] | undefined | null;
1369+
readonly 'bind:pointerLockElement'?: Document['pointerLockElement'] | undefined | null;
13681370
readonly 'bind:visibilityState'?: Document['visibilityState'] | undefined | null;
13691371
}
13701372

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2773,6 +2773,11 @@ export const template_visitors = {
27732773
call_expr = b.call('$.bind_window_size', b.literal(node.name), setter);
27742774
break;
27752775

2776+
// document
2777+
case 'activeElement':
2778+
call_expr = b.call('$.bind_active_element', setter);
2779+
break;
2780+
27762781
// media
27772782
case 'muted':
27782783
call_expr = b.call(`$.bind_muted`, state.node, getter, setter);

packages/svelte/src/compiler/phases/bindings.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,20 @@ export const binding_properties = {
8686
omit_in_ssr: true
8787
},
8888
// document
89+
activeElement: {
90+
valid_elements: ['svelte:document'],
91+
omit_in_ssr: true
92+
},
8993
fullscreenElement: {
9094
valid_elements: ['svelte:document'],
9195
event: 'fullscreenchange',
9296
omit_in_ssr: true
9397
},
98+
pointerLockElement: {
99+
valid_elements: ['svelte:document'],
100+
event: 'pointerlockchange',
101+
omit_in_ssr: true
102+
},
94103
visibilityState: {
95104
valid_elements: ['svelte:document'],
96105
event: 'visibilitychange',
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { listen } from './shared.js';
2+
3+
/**
4+
* @param {(activeElement: Element | null) => void} update
5+
* @returns {void}
6+
*/
7+
export function bind_active_element(update) {
8+
listen(document, ['focusin', 'focusout'], (event) => {
9+
if (event && event.type === 'focusout' && /** @type {FocusEvent} */ (event).relatedTarget) {
10+
// The tests still pass if we remove this, because of JSDOM limitations, but it is necessary
11+
// to avoid temporarily resetting to `document.body`
12+
return;
13+
}
14+
15+
update(document.activeElement);
16+
});
17+
}

packages/svelte/src/internal/client/dom/elements/bindings/shared.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { add_form_reset_listener } from '../misc.js';
44
/**
55
* Fires the handler once immediately (unless corresponding arg is set to `false`),
66
* then listens to the given events until the render effect context is destroyed
7-
* @param {Element | Window} target
7+
* @param {EventTarget} target
88
* @param {Array<string>} events
9-
* @param {() => void} handler
9+
* @param {(event?: Event) => void} handler
1010
* @param {any} call_handler_immediately
1111
*/
1212
export function listen(target, events, handler, call_handler_immediately = true) {

packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export { event, delegate, replay_events } from './dom/elements/events.js';
3636
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
3737
export { set_style } from './dom/elements/style.js';
3838
export { animation, transition } from './dom/elements/transitions.js';
39+
export { bind_active_element } from './dom/elements/bindings/document.js';
3940
export { bind_checked, bind_files, bind_group, bind_value } from './dom/elements/bindings/input.js';
4041
export {
4142
bind_buffered,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
// This test is slightly inaccurate, because blurring elements (or focusing the `<body>` directly)
5+
// doesn't trigger the relevant `focusin` event in JSDOM.
6+
export default test({
7+
test({ assert, target, logs }) {
8+
const [btn1, btn2] = target.querySelectorAll('button');
9+
10+
flushSync(() => btn1.focus());
11+
assert.deepEqual(logs, ['...', 'BODY', 'one']);
12+
13+
flushSync(() => btn2.focus());
14+
assert.deepEqual(logs, ['...', 'BODY', 'one', 'two']);
15+
}
16+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
let active;
3+
4+
$: console.log(active?.id || active?.nodeName || '...');
5+
</script>
6+
7+
<svelte:document bind:activeElement={active} />
8+
9+
<button id="one">one</button>
10+
<button id="two">two</button>

packages/svelte/tests/validator/samples/document-binding-invalid-dimensions/errors.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[
22
{
33
"code": "bind_invalid_name",
4-
"message": "`bind:clientWidth` is not a valid binding. Possible bindings for <svelte:document> are focused, fullscreenElement, visibilityState, this",
4+
"message": "`bind:clientWidth` is not a valid binding. Possible bindings for <svelte:document> are focused, activeElement, fullscreenElement, pointerLockElement, visibilityState, this",
55
"start": {
66
"line": 5,
77
"column": 17

0 commit comments

Comments
 (0)