diff --git a/.changeset/healthy-garlics-do.md b/.changeset/healthy-garlics-do.md new file mode 100644 index 0000000000..c27ace34de --- /dev/null +++ b/.changeset/healthy-garlics-do.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: only skip updating bound `` if the input was the source of the change diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 569d1179e6..7c1fccea0f 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -1,3 +1,4 @@ +/** @import { Batch } from '../../../reactivity/batch.js' */ import { DEV } from 'esm-env'; import { render_effect, teardown } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; @@ -7,6 +8,7 @@ import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { untrack } from '../../../runtime.js'; import { is_runes } from '../../../context.js'; +import { current_batch } from '../../../reactivity/batch.js'; /** * @param {HTMLInputElement} input @@ -17,6 +19,8 @@ import { is_runes } from '../../../context.js'; export function bind_value(input, get, set = get) { var runes = is_runes(); + var batches = new WeakSet(); + listen_to_event_and_reset_event(input, 'input', (is_reset) => { if (DEV && input.type === 'checkbox') { // TODO should this happen in prod too? @@ -28,6 +32,10 @@ export function bind_value(input, get, set = get) { value = is_numberlike_input(input) ? to_number(value) : value; set(value); + if (current_batch !== null) { + batches.add(current_batch); + } + // In runes mode, respect any validation in accessors (doesn't apply in legacy mode, // because we use mutable state which ensures the render effect always runs) if (runes && value !== (value = get())) { @@ -54,6 +62,10 @@ export function bind_value(input, get, set = get) { (untrack(get) == null && input.value) ) { set(is_numberlike_input(input) ? to_number(input.value) : input.value); + + if (current_batch !== null) { + batches.add(current_batch); + } } render_effect(() => { @@ -64,7 +76,7 @@ export function bind_value(input, get, set = get) { var value = get(); - if (input === document.activeElement) { + if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) { // Never rewrite the contents of a focused input. We can get here if, for example, // an update is deferred because of async work depending on the input: // diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/_config.js new file mode 100644 index 0000000000..1d2e6dd470 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/_config.js @@ -0,0 +1,40 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + async test({ assert, target }) { + const [input] = target.querySelectorAll('input'); + + flushSync(() => { + input.focus(); + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })); + }); + assert.equal(input.value, '2'); + assert.htmlEqual( + target.innerHTML, + ` + +
value = 2
+ ` + ); + + flushSync(() => { + input.focus(); + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); + }); + assert.equal(input.value, '1'); + assert.htmlEqual( + target.innerHTML, + ` + +value = 1
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/main.svelte new file mode 100644 index 0000000000..4cc174e404 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/main.svelte @@ -0,0 +1,16 @@ + + + + +value = {value}