From 810ec3caef5c1b59043f26202783159e81632e70 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 15 Oct 2025 10:21:26 -0400 Subject: [PATCH] fix: preserve `` state while focused diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js index e39fb865cdf5..46e8f524f8f3 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js @@ -3,6 +3,7 @@ import { listen_to_event_and_reset_event } from './shared.js'; import { is } from '../../../proxy.js'; import { is_array } from '../../../../shared/utils.js'; import * as w from '../../../warnings.js'; +import { Batch, current_batch, previous_batch } from '../../../reactivity/batch.js'; /** * Selects the correct option(s) (depending on whether this is a multiple select) @@ -83,6 +84,7 @@ export function init_select(select) { * @returns {void} */ export function bind_select_value(select, get, set = get) { + var batches = new WeakSet(); var mounting = true; listen_to_event_and_reset_event(select, 'change', (is_reset) => { @@ -102,11 +104,30 @@ export function bind_select_value(select, get, set = get) { } set(value); + + if (current_batch !== null) { + batches.add(current_batch); + } }); // Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated effect(() => { var value = get(); + + if (select === 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); + + // Don't update the ... + //

{await find(selected)}

+ if (batches.has(batch)) { + return; + } + } + select_option(select, value, mounting); // Mounting and value undefined -> take selection from dom diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js index b0772ad3c071..76a2032c7a58 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js @@ -2,8 +2,9 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ - async test({ assert, target, instance }) { - instance.shift(); + async test({ assert, target }) { + const [shift] = target.querySelectorAll('button'); + shift.click(); await tick(); const [input] = target.querySelectorAll('input'); @@ -13,7 +14,7 @@ export default test({ input.dispatchEvent(new InputEvent('input', { bubbles: true })); await tick(); - assert.htmlEqual(target.innerHTML, `

0

`); + assert.htmlEqual(target.innerHTML, `

0

`); assert.equal(input.value, '1'); input.focus(); @@ -21,17 +22,17 @@ export default test({ input.dispatchEvent(new InputEvent('input', { bubbles: true })); await tick(); - assert.htmlEqual(target.innerHTML, `

0

`); + assert.htmlEqual(target.innerHTML, `

0

`); assert.equal(input.value, '2'); - instance.shift(); + shift.click(); await tick(); - assert.htmlEqual(target.innerHTML, `

1

`); + assert.htmlEqual(target.innerHTML, `

1

`); assert.equal(input.value, '2'); - instance.shift(); + shift.click(); await tick(); - assert.htmlEqual(target.innerHTML, `

2

`); + assert.htmlEqual(target.innerHTML, `

2

`); assert.equal(input.value, '2'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte index 2fc898e6540d..e2f01a66c892 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte @@ -1,22 +1,23 @@ + + - +

{await push(count)}

{#snippet pending()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/_config.js new file mode 100644 index 000000000000..7fddca0d586c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/_config.js @@ -0,0 +1,82 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [shift] = target.querySelectorAll('button'); + shift.click(); + await tick(); + + const [select] = target.querySelectorAll('select'); + + select.focus(); + select.value = 'three'; + select.dispatchEvent(new InputEvent('change', { bubbles: true })); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

two

+ ` + ); + assert.equal(select.value, 'three'); + + select.focus(); + select.value = 'one'; + select.dispatchEvent(new InputEvent('change', { bubbles: true })); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

two

+ ` + ); + assert.equal(select.value, 'one'); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

three

+ ` + ); + assert.equal(select.value, 'one'); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

one

+ ` + ); + assert.equal(select.value, 'one'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte new file mode 100644 index 000000000000..566ea60ec5cf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte @@ -0,0 +1,31 @@ + + + + + + + +

{await push(selected)}

+ + {#snippet pending()} +

loading...

+ {/snippet} +