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/healthy-garlics-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: only skip updating bound `<input>` if the input was the source of the change
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -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?
Expand All @@ -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())) {
Expand All @@ -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(() => {
Expand All @@ -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:
//
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
`
<label>
<input /> arrow up/down
</label>
<p>value = 2</p>
`
);

flushSync(() => {
input.focus();
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
});
assert.equal(input.value, '1');
assert.htmlEqual(
target.innerHTML,
`
<label>
<input /> arrow up/down
</label>
<p>value = 1</p>
`
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script>
let value = $state('1');

function onkeydown (e) {
let _v = parseFloat(value);
if (e.key === 'ArrowUp') _v += 1;
else if (e.key === 'ArrowDown') _v -= 1;
value = _v.toString();
}
</script>

<label>
<input bind:value {onkeydown} /> arrow up/down
</label>

<p>value = {value}</p>
Loading