From 18c58871cc80c3181649baf84ec194ec3bdded2b Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:48:16 -0500 Subject: [PATCH] fix: skip `input` and `change` when option selectedness does not change Selecting an option that is already selected (or deselecting one that is not selected) does not change the select's value, and browsers don't dispatch `input` or `change` in that case. The pointer events and the `click` are still dispatched. Closes #1072 --- src/utility/selectOptions.ts | 5 +++++ tests/utility/selectOptions/deselect.ts | 14 ++++++++++++-- tests/utility/selectOptions/select.ts | 24 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/utility/selectOptions.ts b/src/utility/selectOptions.ts index 23d06eec..5e5b2c33 100644 --- a/src/utility/selectOptions.ts +++ b/src/utility/selectOptions.ts @@ -60,6 +60,11 @@ async function selectOptionsBase( if (isDisabled(select) || !selectedOptions.length) return const selectOption = (option: HTMLOptionElement) => { + // The browser does not fire `input` or `change` + // if the selectedness of the option does not change. + if (option.selected === newValue) { + return + } option.selected = newValue this.dispatchUIEvent(select, 'input', { bubbles: true, diff --git a/tests/utility/selectOptions/deselect.ts b/tests/utility/selectOptions/deselect.ts index ef385758..1cbea828 100644 --- a/tests/utility/selectOptions/deselect.ts +++ b/tests/utility/selectOptions/deselect.ts @@ -56,12 +56,22 @@ test('blurs previously focused element', async () => { select[name="select"][value=[]] - focusin: ← button option[value="1"][selected=false] - pointerup option[value="1"][selected=false] - mouseup: primary - select[name="select"][value=[]] - input - select[name="select"][value=[]] - change option[value="1"][selected=false] - click: primary `) }) +test('does not fire input/change when the option is not selected', async () => { + const {select, options, eventWasFired, user} = setupSelect({multiple: true}) + options[1].selected = true + + await user.deselectOptions(select, '1') + + expect(eventWasFired('click')).toBe(true) + expect(eventWasFired('input')).toBe(false) + expect(eventWasFired('change')).toBe(false) + expect(options.map(o => o.selected)).toEqual([false, true, false]) +}) + test('toggle options as expected', async () => { const {element, user} = setup(`