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_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 <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);
// Mounting and value undefined -> take selection from dom

@ -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, `<input type="number" /> <p>0</p>`);
assert.htmlEqual(target.innerHTML, `<button>shift</button><input type="number" /> <p>0</p>`);
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, `<input type="number" /> <p>0</p>`);
assert.htmlEqual(target.innerHTML, `<button>shift</button><input type="number" /> <p>0</p>`);
assert.equal(input.value, '2');
instance.shift();
shift.click();
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');
instance.shift();
shift.click();
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');
}
});

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