From 23cefee66ceb95e6f6bfac5e67ec234f341817ca Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Thu, 19 Jun 2025 12:00:04 -0700 Subject: [PATCH 1/4] Allow onChange to bubble in Checkbox, Radio, and Switch --- packages/@react-aria/radio/src/useRadio.ts | 3 +-- packages/@react-aria/toggle/src/useToggle.ts | 3 --- .../react-aria-components/test/Checkbox.test.js | 16 ++++++++++++++++ .../test/RadioGroup.test.js | 17 +++++++++++++++++ .../react-aria-components/test/Switch.test.js | 16 ++++++++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/radio/src/useRadio.ts b/packages/@react-aria/radio/src/useRadio.ts index 3c4f0220a22..ea5f74d586e 100644 --- a/packages/@react-aria/radio/src/useRadio.ts +++ b/packages/@react-aria/radio/src/useRadio.ts @@ -57,8 +57,7 @@ export function useRadio(props: AriaRadioProps, state: RadioGroupState, ref: Ref let checked = state.selectedValue === value; - let onChange = (e) => { - e.stopPropagation(); + let onChange = () => { state.setSelectedValue(value); }; diff --git a/packages/@react-aria/toggle/src/useToggle.ts b/packages/@react-aria/toggle/src/useToggle.ts index fee8b760286..9ca0ec5e9ef 100644 --- a/packages/@react-aria/toggle/src/useToggle.ts +++ b/packages/@react-aria/toggle/src/useToggle.ts @@ -51,9 +51,6 @@ export function useToggle(props: AriaToggleProps, state: ToggleState, ref: RefOb } = props; let onChange = (e) => { - // since we spread props on label, onChange will end up there as well as in here. - // so we have to stop propagation at the lowest level that we care about - e.stopPropagation(); state.setSelected(e.target.checked); }; diff --git a/packages/react-aria-components/test/Checkbox.test.js b/packages/react-aria-components/test/Checkbox.test.js index 6754c5dd5ef..52509bf95f3 100644 --- a/packages/react-aria-components/test/Checkbox.test.js +++ b/packages/react-aria-components/test/Checkbox.test.js @@ -244,4 +244,20 @@ describe('Checkbox', () => { expect(inputRef.current).toBe(getByRole('checkbox')); expect(contextInputRef.current).toBe(getByRole('checkbox')); }); + + it('should allow onChange event to bubble to the form', async () => { + let onChange = jest.fn(); + + let {getByRole} = render( +
+ Test +
+ ); + + let checkbox = getByRole('checkbox'); + await user.click(checkbox); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.lastCall[0].target.name).toBe('test'); + }); }); diff --git a/packages/react-aria-components/test/RadioGroup.test.js b/packages/react-aria-components/test/RadioGroup.test.js index bab80b346b3..fdb3a1b029f 100644 --- a/packages/react-aria-components/test/RadioGroup.test.js +++ b/packages/react-aria-components/test/RadioGroup.test.js @@ -557,4 +557,21 @@ describe('RadioGroup', () => { expect(inputRef.current).toBe(radio); expect(contextInputRef.current).toBe(radio); }); + + it('should allow onChange event to bubble to the form', async () => { + let onChange = jest.fn(); + + let {getAllByRole} = render( +
+ + + ); + + let radios = getAllByRole('radio'); + await user.click(radios[1]); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.lastCall[0].target.name).toBe('test'); + expect(onChange.mock.lastCall[0].target.value).toBe('b'); + }); }); diff --git a/packages/react-aria-components/test/Switch.test.js b/packages/react-aria-components/test/Switch.test.js index 987088625a8..7fdfd22187c 100644 --- a/packages/react-aria-components/test/Switch.test.js +++ b/packages/react-aria-components/test/Switch.test.js @@ -229,4 +229,20 @@ describe('Switch', () => { expect(inputRef.current).toBe(getByRole('switch')); expect(contextInputRef.current).toBe(getByRole('switch')); }); + + it('should allow onChange event to bubble to the form', async () => { + let onChange = jest.fn(); + + let {getByRole} = render( +
+ Test +
+ ); + + let input = getByRole('switch'); + await user.click(input); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.lastCall[0].target.name).toBe('test'); + }); }); From 0489595ff2c3fc8c8b6cbb3bd3fce8241f7829eb Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Thu, 19 Jun 2025 12:03:17 -0700 Subject: [PATCH 2/4] Emit native change and input events when Select and Slider change --- .../@react-aria/select/src/HiddenSelect.tsx | 12 +++++-- .../@react-aria/slider/src/useSliderThumb.ts | 22 ++++++++++-- packages/@react-aria/utils/src/index.ts | 1 + .../@react-aria/utils/src/useInputEvent.ts | 34 +++++++++++++++++++ .../picker/test/Picker.test.js | 30 +++++++++++++++- .../combobox/src/useComboBoxState.ts | 2 +- packages/@react-stately/select/package.json | 1 + .../select/src/useSelectState.ts | 13 +++++-- .../slider/src/useSliderState.ts | 19 +++++++++-- packages/@react-stately/utils/src/index.ts | 1 + .../@react-stately/utils/src/useStateEvent.ts | 33 ++++++++++++++++++ .../test/ColorSlider.test.js | 29 ++++++++++++++++ .../react-aria-components/test/Slider.test.js | 29 ++++++++++++++++ 13 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 packages/@react-aria/utils/src/useInputEvent.ts create mode 100644 packages/@react-stately/utils/src/useStateEvent.ts diff --git a/packages/@react-aria/select/src/HiddenSelect.tsx b/packages/@react-aria/select/src/HiddenSelect.tsx index b0d61ddc2aa..3a561b464ed 100644 --- a/packages/@react-aria/select/src/HiddenSelect.tsx +++ b/packages/@react-aria/select/src/HiddenSelect.tsx @@ -14,7 +14,7 @@ import {FocusableElement, RefObject} from '@react-types/shared'; import React, {JSX, ReactNode, useRef} from 'react'; import {selectData} from './useSelect'; import {SelectState} from '@react-stately/select'; -import {useFormReset} from '@react-aria/utils'; +import {useFormReset, useInputEvent} from '@react-aria/utils'; import {useFormValidation} from '@react-aria/form'; import {useVisuallyHidden} from '@react-aria/visually-hidden'; @@ -44,7 +44,7 @@ export interface HiddenSelectProps extends AriaHiddenSelectProps { export interface AriaHiddenSelectOptions extends AriaHiddenSelectProps { /** A ref to the hidden ` cannot have `display: none` or `hidden` for autofill to work. // In Firefox, there must be a