fix: preserve `<select>` state while focused (#16958)

pull/15673/merge
Rich Harris 2 weeks ago committed by GitHub
parent 9b5fb3f430
commit ee093e4c86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve `<select>` state while focused

@ -3,6 +3,7 @@ import { listen_to_event_and_reset_event } from './shared.js';
import { is } from '../../../proxy.js'; import { is } from '../../../proxy.js';
import { is_array } from '../../../../shared/utils.js'; import { is_array } from '../../../../shared/utils.js';
import * as w from '../../../warnings.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) * Selects the correct option(s) (depending on whether this is a multiple select)
@ -83,6 +84,7 @@ export function init_select(select) {
* @returns {void} * @returns {void}
*/ */
export function bind_select_value(select, get, set = get) { export function bind_select_value(select, get, set = get) {
var batches = new WeakSet();
var mounting = true; var mounting = true;
listen_to_event_and_reset_event(select, 'change', (is_reset) => { 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); 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 // 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(() => { effect(() => {
var value = get(); 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 <select> if it is focused. We can get here if, for example,
// an update is deferred because of async work depending on the select:
//
// <select bind:value={selected}>...</select>
// <p>{await find(selected)}</p>
if (batches.has(batch)) {
return;
}
}
select_option(select, value, mounting); select_option(select, value, mounting);
// Mounting and value undefined -> take selection from dom // Mounting and value undefined -> take selection from dom

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

@ -1,22 +1,23 @@
<script lang="ts"> <script lang="ts">
let count = $state(0); let count = $state(0);
let deferreds = []; let resolvers = [];
let input;
export function shift() { function push(value) {
const d = deferreds.shift(); const { promise, resolve } = Promise.withResolvers();
d.d.resolve(d.v); resolvers.push(() => resolve(value));
} return promise;
function push(v) {
const d = Promise.withResolvers();
deferreds.push({ d, v });
return d.promise;
} }
</script> </script>
<button onclick={() => {
input.focus();
resolvers.shift()?.();
}}>shift</button>
<svelte:boundary> <svelte:boundary>
<input type="number" bind:value={count} /> <input bind:this={input} type="number" bind:value={count} />
<p>{await push(count)}</p> <p>{await push(count)}</p>
{#snippet pending()} {#snippet pending()}

@ -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,
`
<button>shift</button>
<select>
<option>one</option>
<option>two</option>
<option>three</option>
</select>
<p>two</p>
`
);
assert.equal(select.value, 'three');
select.focus();
select.value = 'one';
select.dispatchEvent(new InputEvent('change', { bubbles: true }));
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>shift</button>
<select>
<option>one</option>
<option>two</option>
<option>three</option>
</select>
<p>two</p>
`
);
assert.equal(select.value, 'one');
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>shift</button>
<select>
<option>one</option>
<option>two</option>
<option>three</option>
</select>
<p>three</p>
`
);
assert.equal(select.value, 'one');
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>shift</button>
<select>
<option>one</option>
<option>two</option>
<option>three</option>
</select>
<p>one</p>
`
);
assert.equal(select.value, 'one');
}
});

@ -0,0 +1,31 @@
<script lang="ts">
let selected = $state('two');
let resolvers = [];
let select;
function push(value) {
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => resolve(value));
return promise;
}
</script>
<button onclick={() => {
select.focus();
resolvers.shift()?.();
}}>shift</button>
<svelte:boundary>
<select bind:this={select} bind:value={selected}>
<option>one</option>
<option>two</option>
<option>three</option>
</select>
<p>{await push(selected)}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save