fix: don't update a focused input with values from its own past

pull/16491/head
Rich Harris 1 month ago
parent 4e74cd35fe
commit 92c9c963ea

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't update a focused input with values from its own past

@ -8,7 +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';
import { current_batch, previous_batch } from '../../../reactivity/batch.js';
/**
* @param {HTMLInputElement} input
@ -76,7 +76,7 @@ export function bind_value(input, get, set = get) {
var value = get();
if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) {
if (input === document.activeElement && batches.has(/** @type {Batch} */ (previous_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:
//

@ -38,6 +38,9 @@ const batches = new Set();
/** @type {Batch | null} */
export let current_batch = null;
/** @type {Batch | null} */
export let previous_batch = current_batch;
/**
* When time travelling, we re-evaluate deriveds based on the temporary
* values of their dependencies rather than their actual values, and cache
@ -72,7 +75,10 @@ let is_flushing = false;
let is_flushing_sync = false;
let uid = 1;
export class Batch {
id = uid++;
/**
* The current values of any sources that are updated in this batch
* They keys of this map are identical to `this.#previous`
@ -218,6 +224,7 @@ export class Batch {
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
// newly updated sources, which could lead to infinite loops when effects run over and over again.
previous_batch = current_batch;
current_batch = null;
flush_queued_effects(render_effects);
@ -350,6 +357,7 @@ export class Batch {
deactivate() {
current_batch = null;
previous_batch = null;
for (const update of effect_pending_updates) {
effect_pending_updates.delete(update);

@ -0,0 +1,37 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, instance }) {
instance.shift();
await tick();
const [input] = target.querySelectorAll('input');
input.focus();
input.value = '1';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();
assert.htmlEqual(target.innerHTML, `<input type="number" /> <p>0</p>`);
assert.equal(input.value, '1');
input.focus();
input.value = '2';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();
assert.htmlEqual(target.innerHTML, `<input type="number" /> <p>0</p>`);
assert.equal(input.value, '2');
instance.shift();
await tick();
assert.htmlEqual(target.innerHTML, `<input type="number" /> <p>1</p>`);
assert.equal(input.value, '2');
instance.shift();
await tick();
assert.htmlEqual(target.innerHTML, `<input type="number" /> <p>2</p>`);
assert.equal(input.value, '2');
}
});

@ -0,0 +1,25 @@
<script lang="ts">
let count = $state(0);
let deferreds = [];
export function shift() {
const d = deferreds.shift();
d.d.resolve(d.v);
}
function push(v) {
const d = Promise.withResolvers();
deferreds.push({ d, v });
return d.promise;
}
</script>
<svelte:boundary>
<input type="number" bind:value={count} />
<p>{await push(count)}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save