Skip to content

Commit 9134cac

Browse files
authored
fix: only skip updating bound <input> if the input was the source of the change (#16373)
* fix: only skip updating bound `<input>` if the input was the source of the change * import Batch as type, not value
1 parent 1e4547b commit 9134cac

File tree

4 files changed

+74
-1
lines changed

4 files changed

+74
-1
lines changed

.changeset/healthy-garlics-do.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+
fix: only skip updating bound `<input>` if the input was the source of the change

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** @import { Batch } from '../../../reactivity/batch.js' */
12
import { DEV } from 'esm-env';
23
import { render_effect, teardown } from '../../../reactivity/effects.js';
34
import { listen_to_event_and_reset_event } from './shared.js';
@@ -7,6 +8,7 @@ import { queue_micro_task } from '../../task.js';
78
import { hydrating } from '../../hydration.js';
89
import { untrack } from '../../../runtime.js';
910
import { is_runes } from '../../../context.js';
11+
import { current_batch } from '../../../reactivity/batch.js';
1012

1113
/**
1214
* @param {HTMLInputElement} input
@@ -17,6 +19,8 @@ import { is_runes } from '../../../context.js';
1719
export function bind_value(input, get, set = get) {
1820
var runes = is_runes();
1921

22+
var batches = new WeakSet();
23+
2024
listen_to_event_and_reset_event(input, 'input', (is_reset) => {
2125
if (DEV && input.type === 'checkbox') {
2226
// TODO should this happen in prod too?
@@ -28,6 +32,10 @@ export function bind_value(input, get, set = get) {
2832
value = is_numberlike_input(input) ? to_number(value) : value;
2933
set(value);
3034

35+
if (current_batch !== null) {
36+
batches.add(current_batch);
37+
}
38+
3139
// In runes mode, respect any validation in accessors (doesn't apply in legacy mode,
3240
// because we use mutable state which ensures the render effect always runs)
3341
if (runes && value !== (value = get())) {
@@ -54,6 +62,10 @@ export function bind_value(input, get, set = get) {
5462
(untrack(get) == null && input.value)
5563
) {
5664
set(is_numberlike_input(input) ? to_number(input.value) : input.value);
65+
66+
if (current_batch !== null) {
67+
batches.add(current_batch);
68+
}
5769
}
5870

5971
render_effect(() => {
@@ -64,7 +76,7 @@ export function bind_value(input, get, set = get) {
6476

6577
var value = get();
6678

67-
if (input === document.activeElement) {
79+
if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) {
6880
// Never rewrite the contents of a focused input. We can get here if, for example,
6981
// an update is deferred because of async work depending on the input:
7082
//
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
mode: ['client', 'hydrate'],
6+
7+
async test({ assert, target }) {
8+
const [input] = target.querySelectorAll('input');
9+
10+
flushSync(() => {
11+
input.focus();
12+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
13+
});
14+
assert.equal(input.value, '2');
15+
assert.htmlEqual(
16+
target.innerHTML,
17+
`
18+
<label>
19+
<input /> arrow up/down
20+
</label>
21+
<p>value = 2</p>
22+
`
23+
);
24+
25+
flushSync(() => {
26+
input.focus();
27+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
28+
});
29+
assert.equal(input.value, '1');
30+
assert.htmlEqual(
31+
target.innerHTML,
32+
`
33+
<label>
34+
<input /> arrow up/down
35+
</label>
36+
<p>value = 1</p>
37+
`
38+
);
39+
}
40+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script>
2+
let value = $state('1');
3+
4+
function onkeydown (e) {
5+
let _v = parseFloat(value);
6+
if (e.key === 'ArrowUp') _v += 1;
7+
else if (e.key === 'ArrowDown') _v -= 1;
8+
value = _v.toString();
9+
}
10+
</script>
11+
12+
<label>
13+
<input bind:value {onkeydown} /> arrow up/down
14+
</label>
15+
16+
<p>value = {value}</p>

0 commit comments

Comments
 (0)