Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-pandas-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: prevent `selectedcontent` mutation from changing the selected option
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ export function select_option(select, value, mounting = false) {
* @param {HTMLSelectElement} select
*/
export function init_select(select) {
var observer = new MutationObserver(() => {
var observer = new MutationObserver((entries) => {
// Mutations related to `<selectedcontent>` can never affect the option list.
// Reacting to them could revert a user-initiated selection change, because the
// records are delivered as soon as any listener returns (e.g. a delegated `input`
// handler), which can happen before the `change` handler has updated `__value`
if (entries.every(is_selectedcontent_mutation)) return;

// @ts-ignore
select_option(select, select.__value);
// Deliberately don't update the potential binding value,
Expand Down Expand Up @@ -162,3 +168,22 @@ function get_option_value(option) {
return option.value;
}
}

/**
* Returns `true` if the mutation stems from the browser mirroring the selected
* option's content into `<selectedcontent>`, or from us replacing the
* `<selectedcontent>` element with a clone of itself
* @param {MutationRecord} entry
*/
function is_selectedcontent_mutation(entry) {
if (/** @type {Element} */ (entry.target).closest('selectedcontent') !== null) {
return true;
}

if (entry.type === 'childList') {
var nodes = [...entry.addedNodes, ...entry.removedNodes];
return nodes.length > 0 && nodes.every((node) => node.nodeName === 'SELECTEDCONTENT');
}

return false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../assert';

export default test({
async test({ target, assert }) {
const select = target.querySelector('select');
ok(select);

select.value = 'B';
select.dispatchEvent(new Event('input', { bubbles: true }));

// because another element has a delegated `oninput` handler, a global `input`
// listener runs and, once it returns, a microtask checkpoint delivers the
// mutation records *before* the `change` event — we emulate that checkpoint here
await Promise.resolve();

select.dispatchEvent(new Event('change', { bubbles: true }));
flushSync();

assert.equal(select.value, 'B');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
let value = $state('A');
</script>

<input oninput={() => {}} />
<select bind:value>
<button><selectedcontent></selectedcontent></button>
<option>A</option>
<option>B</option>
<option>C</option>
</select>

<style>
select,::picker(select){
appearance: base-select;
}
</style>
Loading