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 <rich.harris@vercel.com>
pull/11913/head
wackbyte 3 weeks ago committed by GitHub
parent f3c291d8dd
commit c0c1a56675
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: bind `activeElement` and `pointerLockElement` in `<svelte:document>`

@ -268,7 +268,9 @@ As with `<svelte:window>`, this element may only appear the top level of your co
You can also bind to the following properties:
- `activeElement`
- `fullscreenElement`
- `pointerLockElement`
- `visibilityState`
All are readonly.

@ -1364,7 +1364,9 @@ export interface SvelteMediaTimeRange {
}
export interface SvelteDocumentAttributes extends HTMLAttributes<Document> {
readonly 'bind:activeElement'?: Document['activeElement'] | undefined | null;
readonly 'bind:fullscreenElement'?: Document['fullscreenElement'] | undefined | null;
readonly 'bind:pointerLockElement'?: Document['pointerLockElement'] | undefined | null;
readonly 'bind:visibilityState'?: Document['visibilityState'] | undefined | null;
}

@ -2773,6 +2773,11 @@ export const template_visitors = {
call_expr = b.call('$.bind_window_size', b.literal(node.name), setter);
break;
// document
case 'activeElement':
call_expr = b.call('$.bind_active_element', setter);
break;
// media
case 'muted':
call_expr = b.call(`$.bind_muted`, state.node, getter, setter);

@ -86,11 +86,20 @@ export const binding_properties = {
omit_in_ssr: true
},
// document
activeElement: {
valid_elements: ['svelte:document'],
omit_in_ssr: true
},
fullscreenElement: {
valid_elements: ['svelte:document'],
event: 'fullscreenchange',
omit_in_ssr: true
},
pointerLockElement: {
valid_elements: ['svelte:document'],
event: 'pointerlockchange',
omit_in_ssr: true
},
visibilityState: {
valid_elements: ['svelte:document'],
event: 'visibilitychange',

@ -0,0 +1,17 @@
import { listen } from './shared.js';
/**
* @param {(activeElement: Element | null) => void} update
* @returns {void}
*/
export function bind_active_element(update) {
listen(document, ['focusin', 'focusout'], (event) => {
if (event && event.type === 'focusout' && /** @type {FocusEvent} */ (event).relatedTarget) {
// The tests still pass if we remove this, because of JSDOM limitations, but it is necessary
// to avoid temporarily resetting to `document.body`
return;
}
update(document.activeElement);
});
}

@ -4,9 +4,9 @@ import { add_form_reset_listener } from '../misc.js';
/**
* Fires the handler once immediately (unless corresponding arg is set to `false`),
* then listens to the given events until the render effect context is destroyed
* @param {Element | Window} target
* @param {EventTarget} target
* @param {Array<string>} events
* @param {() => void} handler
* @param {(event?: Event) => void} handler
* @param {any} call_handler_immediately
*/
export function listen(target, events, handler, call_handler_immediately = true) {

@ -36,6 +36,7 @@ export { event, delegate, replay_events } from './dom/elements/events.js';
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
export { set_style } from './dom/elements/style.js';
export { animation, transition } from './dom/elements/transitions.js';
export { bind_active_element } from './dom/elements/bindings/document.js';
export { bind_checked, bind_files, bind_group, bind_value } from './dom/elements/bindings/input.js';
export {
bind_buffered,

@ -0,0 +1,16 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
// This test is slightly inaccurate, because blurring elements (or focusing the `<body>` directly)
// doesn't trigger the relevant `focusin` event in JSDOM.
export default test({
test({ assert, target, logs }) {
const [btn1, btn2] = target.querySelectorAll('button');
flushSync(() => btn1.focus());
assert.deepEqual(logs, ['...', 'BODY', 'one']);
flushSync(() => btn2.focus());
assert.deepEqual(logs, ['...', 'BODY', 'one', 'two']);
}
});

@ -0,0 +1,10 @@
<script>
let active;
$: console.log(active?.id || active?.nodeName || '...');
</script>
<svelte:document bind:activeElement={active} />
<button id="one">one</button>
<button id="two">two</button>

@ -1,7 +1,7 @@
[
{
"code": "bind_invalid_name",
"message": "`bind:clientWidth` is not a valid binding. Possible bindings for <svelte:document> are focused, fullscreenElement, visibilityState, this",
"message": "`bind:clientWidth` is not a valid binding. Possible bindings for <svelte:document> are focused, activeElement, fullscreenElement, pointerLockElement, visibilityState, this",
"start": {
"line": 5,
"column": 17

Loading…
Cancel
Save