mirror of https://github.com/sveltejs/svelte
fix: keep the current selection of a `<select>` when its `defaultValue` is applied (#18719)
Also ensure init_select isn't called multiple times Follow-up to #18591.pull/18722/head
parent
4d5139552d
commit
864de81bb6
@ -0,0 +1,52 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { ok, test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
const [nothing, unmatched, spread, late, touched] = target.querySelectorAll('select');
|
||||||
|
const [change_default, change_spread, load, add] = target.querySelectorAll('button');
|
||||||
|
const p = target.querySelector('p');
|
||||||
|
ok(p);
|
||||||
|
|
||||||
|
assert.equal(nothing.selectedIndex, -1);
|
||||||
|
assert.equal(unmatched.selectedIndex, -1);
|
||||||
|
assert.equal(spread.selectedIndex, -1);
|
||||||
|
assert.equal(late.selectedIndex, -1);
|
||||||
|
assert.equal(touched.value, 'b');
|
||||||
|
|
||||||
|
change_default.click();
|
||||||
|
change_spread.click();
|
||||||
|
flushSync();
|
||||||
|
|
||||||
|
assert.equal(nothing.selectedIndex, -1);
|
||||||
|
assert.equal(unmatched.selectedIndex, -1);
|
||||||
|
assert.equal(spread.selectedIndex, -1);
|
||||||
|
assert.equal(spread.className, 'two');
|
||||||
|
assert.htmlEqual(p.innerHTML, 'zzz null');
|
||||||
|
assert.deepEqual(
|
||||||
|
[...nothing.options].map((option) => option.defaultSelected),
|
||||||
|
[true, false]
|
||||||
|
);
|
||||||
|
|
||||||
|
// a default change never moves the current selection
|
||||||
|
assert.equal(touched.value, 'b');
|
||||||
|
|
||||||
|
// a default whose option arrives later selects it
|
||||||
|
load.click();
|
||||||
|
flushSync();
|
||||||
|
await Promise.resolve();
|
||||||
|
assert.equal(late.value, 'b');
|
||||||
|
|
||||||
|
// a user selection survives option mutations and default changes
|
||||||
|
touched.options[2].selected = true;
|
||||||
|
touched.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
add.click();
|
||||||
|
flushSync();
|
||||||
|
await Promise.resolve();
|
||||||
|
assert.equal(touched.value, 'c');
|
||||||
|
assert.deepEqual(
|
||||||
|
[...touched.options].map((option) => option.defaultSelected),
|
||||||
|
[true, false, false, false]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<script>
|
||||||
|
let unmatched = $state('zzz');
|
||||||
|
let nothing = $state(null);
|
||||||
|
let defaultValue = $state('b');
|
||||||
|
let props = $state({ defaultValue: 'b', class: 'one' });
|
||||||
|
let late = $state([]);
|
||||||
|
let options = $state(['a', 'b', 'c']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select value={null} {defaultValue}>
|
||||||
|
<option value="a">A</option>
|
||||||
|
<option value="b">B</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select bind:value={unmatched} {defaultValue}>
|
||||||
|
<option value="a">A</option>
|
||||||
|
<option value="b">B</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select {...props} bind:value={nothing}>
|
||||||
|
<option value="a">A</option>
|
||||||
|
<option value="b">B</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select defaultValue="b">
|
||||||
|
{#each late as option}<option value={option}>{option}</option>{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select {defaultValue}>
|
||||||
|
{#each options as option}<option value={option}>{option}</option>{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button onclick={() => (defaultValue = 'a')}>change default</button>
|
||||||
|
<button onclick={() => (props.class = 'two')}>change class</button>
|
||||||
|
<button onclick={() => (late = ['a', 'b', 'c'])}>load options</button>
|
||||||
|
<button onclick={() => options.push('d')}>add option</button>
|
||||||
|
<p>{unmatched} {String(nothing)}</p>
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
let observers = 0;
|
||||||
|
const MutationObserver = globalThis.MutationObserver;
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
before_test() {
|
||||||
|
observers = 0;
|
||||||
|
globalThis.MutationObserver = class extends MutationObserver {
|
||||||
|
/** @param {MutationCallback} callback */
|
||||||
|
constructor(callback) {
|
||||||
|
super(callback);
|
||||||
|
observers++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
after_test() {
|
||||||
|
globalThis.MutationObserver = MutationObserver;
|
||||||
|
},
|
||||||
|
|
||||||
|
test({ assert, target }) {
|
||||||
|
const selects = target.querySelectorAll('select');
|
||||||
|
for (const select of selects) {
|
||||||
|
assert.equal(select.selectedIndex, 0);
|
||||||
|
assert.equal(select.options[1].defaultSelected, true);
|
||||||
|
}
|
||||||
|
assert.equal(observers, selects.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
let value = $state('a');
|
||||||
|
let bound = $state('a');
|
||||||
|
let spread = $state('a');
|
||||||
|
let props = { defaultValue: 'b' };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select {value} defaultValue="b">
|
||||||
|
<option value="a">A</option>
|
||||||
|
<option value="b">B</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select bind:value={bound} defaultValue="b">
|
||||||
|
<option value="a">A</option>
|
||||||
|
<option value="b">B</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select {...props} bind:value={spread}>
|
||||||
|
<option value="a">A</option>
|
||||||
|
<option value="b">B</option>
|
||||||
|
</select>
|
||||||
Loading…
Reference in new issue