Skip to content

Commit dc043fb

Browse files
authored
fix: don't update a focused input with values from its own past (#16491)
* fix: don't update a focused input with values from its own past * remove * fix
1 parent 7eb11e0 commit dc043fb

File tree

5 files changed

+86
-4
lines changed

5 files changed

+86
-4
lines changed

.changeset/fast-mails-fail.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: don't update a focused input with values from its own past

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { queue_micro_task } from '../../task.js';
88
import { hydrating } from '../../hydration.js';
99
import { untrack } from '../../../runtime.js';
1010
import { is_runes } from '../../../context.js';
11-
import { current_batch } from '../../../reactivity/batch.js';
11+
import { current_batch, previous_batch } from '../../../reactivity/batch.js';
1212

1313
/**
1414
* @param {HTMLInputElement} input
@@ -76,13 +76,18 @@ export function bind_value(input, get, set = get) {
7676

7777
var value = get();
7878

79-
if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) {
79+
if (input === document.activeElement) {
80+
// we need both, because in non-async mode, render effects run before previous_batch is set
81+
var batch = /** @type {Batch} */ (previous_batch ?? current_batch);
82+
8083
// Never rewrite the contents of a focused input. We can get here if, for example,
8184
// an update is deferred because of async work depending on the input:
8285
//
8386
// <input bind:value={query}>
8487
// <p>{await find(query)}</p>
85-
return;
88+
if (batches.has(batch)) {
89+
return;
90+
}
8691
}
8792

8893
if (is_numberlike_input(input) && value === to_number(input.value)) {

packages/svelte/src/internal/client/reactivity/batch.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ const batches = new Set();
3838
/** @type {Batch | null} */
3939
export let current_batch = null;
4040

41+
/**
42+
* This is needed to avoid overwriting inputs in non-async mode
43+
* TODO 6.0 remove this, as non-async mode will go away
44+
* @type {Batch | null}
45+
*/
46+
export let previous_batch = null;
47+
4148
/**
4249
* When time travelling, we re-evaluate deriveds based on the temporary
4350
* values of their dependencies rather than their actual values, and cache
@@ -71,7 +78,6 @@ let last_scheduled_effect = null;
7178
let is_flushing = false;
7279

7380
let is_flushing_sync = false;
74-
7581
export class Batch {
7682
/**
7783
* The current values of any sources that are updated in this batch
@@ -173,6 +179,8 @@ export class Batch {
173179
process(root_effects) {
174180
queued_root_effects = [];
175181

182+
previous_batch = null;
183+
176184
/** @type {Map<Source, { v: unknown, wv: number }> | null} */
177185
var current_values = null;
178186

@@ -218,6 +226,7 @@ export class Batch {
218226

219227
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
220228
// newly updated sources, which could lead to infinite loops when effects run over and over again.
229+
previous_batch = current_batch;
221230
current_batch = null;
222231

223232
flush_queued_effects(render_effects);
@@ -350,6 +359,7 @@ export class Batch {
350359

351360
deactivate() {
352361
current_batch = null;
362+
previous_batch = null;
353363

354364
for (const update of effect_pending_updates) {
355365
effect_pending_updates.delete(update);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target, instance }) {
6+
instance.shift();
7+
await tick();
8+
9+
const [input] = target.querySelectorAll('input');
10+
11+
input.focus();
12+
input.value = '1';
13+
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
14+
await tick();
15+
16+
assert.htmlEqual(target.innerHTML, `<input type="number" /> <p>0</p>`);
17+
assert.equal(input.value, '1');
18+
19+
input.focus();
20+
input.value = '2';
21+
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
22+
await tick();
23+
24+
assert.htmlEqual(target.innerHTML, `<input type="number" /> <p>0</p>`);
25+
assert.equal(input.value, '2');
26+
27+
instance.shift();
28+
await tick();
29+
assert.htmlEqual(target.innerHTML, `<input type="number" /> <p>1</p>`);
30+
assert.equal(input.value, '2');
31+
32+
instance.shift();
33+
await tick();
34+
assert.htmlEqual(target.innerHTML, `<input type="number" /> <p>2</p>`);
35+
assert.equal(input.value, '2');
36+
}
37+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script lang="ts">
2+
let count = $state(0);
3+
4+
let deferreds = [];
5+
6+
export function shift() {
7+
const d = deferreds.shift();
8+
d.d.resolve(d.v);
9+
}
10+
11+
function push(v) {
12+
const d = Promise.withResolvers();
13+
deferreds.push({ d, v });
14+
return d.promise;
15+
}
16+
</script>
17+
18+
<svelte:boundary>
19+
<input type="number" bind:value={count} />
20+
<p>{await push(count)}</p>
21+
22+
{#snippet pending()}
23+
<p>loading...</p>
24+
{/snippet}
25+
</svelte:boundary>

0 commit comments

Comments
 (0)