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
pull/16493/head
Rich Harris 1 month ago committed by GitHub
parent 7eb11e0e24
commit dc043fb2d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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 { hydrating } from '../../hydration.js';
import { untrack } from '../../../runtime.js'; import { untrack } from '../../../runtime.js';
import { is_runes } from '../../../context.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 * @param {HTMLInputElement} input
@ -76,13 +76,18 @@ export function bind_value(input, get, set = get) {
var value = get(); var value = get();
if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) { if (input === document.activeElement) {
// we need both, because in non-async mode, render effects run before previous_batch is set
var batch = /** @type {Batch} */ (previous_batch ?? current_batch);
// Never rewrite the contents of a focused input. We can get here if, for example, // 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: // an update is deferred because of async work depending on the input:
// //
// <input bind:value={query}> // <input bind:value={query}>
// <p>{await find(query)}</p> // <p>{await find(query)}</p>
return; if (batches.has(batch)) {
return;
}
} }
if (is_numberlike_input(input) && value === to_number(input.value)) { if (is_numberlike_input(input) && value === to_number(input.value)) {

@ -38,6 +38,13 @@ const batches = new Set();
/** @type {Batch | null} */ /** @type {Batch | null} */
export let current_batch = null; export let current_batch = null;
/**
* This is needed to avoid overwriting inputs in non-async mode
* TODO 6.0 remove this, as non-async mode will go away
* @type {Batch | null}
*/
export let previous_batch = null;
/** /**
* When time travelling, we re-evaluate deriveds based on the temporary * When time travelling, we re-evaluate deriveds based on the temporary
* values of their dependencies rather than their actual values, and cache * values of their dependencies rather than their actual values, and cache
@ -71,7 +78,6 @@ let last_scheduled_effect = null;
let is_flushing = false; let is_flushing = false;
let is_flushing_sync = false; let is_flushing_sync = false;
export class Batch { export class Batch {
/** /**
* The current values of any sources that are updated in this batch * The current values of any sources that are updated in this batch
@ -173,6 +179,8 @@ export class Batch {
process(root_effects) { process(root_effects) {
queued_root_effects = []; queued_root_effects = [];
previous_batch = null;
/** @type {Map<Source, { v: unknown, wv: number }> | null} */ /** @type {Map<Source, { v: unknown, wv: number }> | null} */
var current_values = null; var current_values = null;
@ -218,6 +226,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 // 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. // newly updated sources, which could lead to infinite loops when effects run over and over again.
previous_batch = current_batch;
current_batch = null; current_batch = null;
flush_queued_effects(render_effects); flush_queued_effects(render_effects);
@ -350,6 +359,7 @@ export class Batch {
deactivate() { deactivate() {
current_batch = null; current_batch = null;
previous_batch = null;
for (const update of effect_pending_updates) { for (const update of effect_pending_updates) {
effect_pending_updates.delete(update); 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