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(`
diff --git a/tests/utility/selectOptions/select.ts b/tests/utility/selectOptions/select.ts index da7cf8f3..9611b7c7 100644 --- a/tests/utility/selectOptions/select.ts +++ b/tests/utility/selectOptions/select.ts @@ -216,6 +216,30 @@ test('should call onChange/input bubbling up the event when a new option is sele expect(onInputForm).toHaveBeenCalledTimes(1) }) +test('does not fire input/change when the option is already selected', async () => { + const {select, options, eventWasFired, user} = setupSelect() + expect(options[0].selected).toBe(true) + + await user.selectOptions(select, '1') + + expect(eventWasFired('click')).toBe(true) + expect(eventWasFired('input')).toBe(false) + expect(eventWasFired('change')).toBe(false) + expect(options[0].selected).toBe(true) +}) + +test('only fires input/change for options that were not selected on multi-selects', async () => { + const {select, options, getEvents, user} = setupSelect({multiple: true}) + options[0].selected = true + + await user.selectOptions(select, ['1', '3']) + + expect(getEvents('click')).toHaveLength(2) + expect(getEvents('input')).toHaveLength(1) + expect(getEvents('change')).toHaveLength(1) + expect(options.map(o => o.selected)).toEqual([true, false, true]) +}) + test('fire no pointer events when select has disabled pointer events', async () => { const {select, options, getEventSnapshot, user} = setupSelect({ pointerEvents: 'none',