From 1fcbc035b1c5c7b6aaf8202cbe830e454d56242f Mon Sep 17 00:00:00 2001 From: "yugo.innami" Date: Fri, 24 Apr 2026 01:10:15 +0900 Subject: [PATCH 1/3] fix: delegate checkbox/radio/switch label keydown to native inputs --- .../test/Checkbox.test.js | 16 +++++++++++++++ .../test/RadioGroup.test.js | 20 +++++++++++++++++++ .../react-aria-components/test/Switch.test.js | 16 +++++++++++++++ packages/react-aria/src/radio/useRadio.ts | 4 ++++ packages/react-aria/src/toggle/useToggle.ts | 4 ++++ 5 files changed, 60 insertions(+) diff --git a/packages/react-aria-components/test/Checkbox.test.js b/packages/react-aria-components/test/Checkbox.test.js index 91e467f98a7..d6ef0d521b6 100644 --- a/packages/react-aria-components/test/Checkbox.test.js +++ b/packages/react-aria-components/test/Checkbox.test.js @@ -385,4 +385,20 @@ describe.each(['Checkbox', 'CheckboxField'])('%s', (comp) => { expect(onBlur).not.toHaveBeenCalled(); expect(onFocus).not.toHaveBeenCalled(); }); + + it('should support implicit form submission from a focused checkbox on Enter', async () => { + let onSubmit = jest.fn(e => e.preventDefault()); + let {getByRole} = render( +
+ Test + +
+ ); + + let checkbox = getByRole('checkbox'); + await user.click(checkbox); + await user.keyboard('{Enter}'); + + expect(onSubmit).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/react-aria-components/test/RadioGroup.test.js b/packages/react-aria-components/test/RadioGroup.test.js index f12aece756d..e02f3e17c7a 100644 --- a/packages/react-aria-components/test/RadioGroup.test.js +++ b/packages/react-aria-components/test/RadioGroup.test.js @@ -739,4 +739,24 @@ describe.each(['RadioGroup', 'RadioField'])('%s', (comp) => { expect(onFocusB).toHaveBeenCalledTimes(1); expect(onBlurA).toHaveBeenCalledTimes(1); }); + + it('should support implicit form submission from a focused radio on Enter', async () => { + let onSubmit = jest.fn(e => e.preventDefault()); + let {getAllByRole} = render( +
+ + + A + B + + +
+ ); + + let radio = getAllByRole('radio')[0]; + await user.click(radio); + await user.keyboard('{Enter}'); + + expect(onSubmit).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/react-aria-components/test/Switch.test.js b/packages/react-aria-components/test/Switch.test.js index cc1bbce8031..28dedce94b2 100644 --- a/packages/react-aria-components/test/Switch.test.js +++ b/packages/react-aria-components/test/Switch.test.js @@ -359,4 +359,20 @@ describe.each(['Switch', 'SwitchField'])('%s', (comp) => { expect(checkbox).not.toHaveAttribute('aria-describedby'); }); } + + it('should support implicit form submission from a focused switch on Enter', async () => { + let onSubmit = jest.fn(e => e.preventDefault()); + let {getByRole} = render( +
+ Test + +
+ ); + + let s = getByRole('switch'); + await user.click(s); + await user.keyboard('{Enter}'); + + expect(onSubmit).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/react-aria/src/radio/useRadio.ts b/packages/react-aria/src/radio/useRadio.ts index 3bdb69b32b9..827c43b19b0 100644 --- a/packages/react-aria/src/radio/useRadio.ts +++ b/packages/react-aria/src/radio/useRadio.ts @@ -118,6 +118,10 @@ export function useRadio(props: AriaRadioProps, state: RadioGroupState, ref: Ref } }); + // Let the hidden radio input handle keyboard events natively so Enter can + // submit forms like a native radio control. + delete labelProps.onKeyDown; + let {focusableProps} = useFocusable(mergeProps(props, { onFocus: () => state.setLastFocusedValue(value) }), ref); diff --git a/packages/react-aria/src/toggle/useToggle.ts b/packages/react-aria/src/toggle/useToggle.ts index d35be1ada26..487348b98fd 100644 --- a/packages/react-aria/src/toggle/useToggle.ts +++ b/packages/react-aria/src/toggle/useToggle.ts @@ -161,6 +161,10 @@ export function useToggle(props: AriaToggleProps, state: ToggleState, ref: RefOb isDisabled: isDisabled || isReadOnly }); + // Let the hidden input handle keyboard events natively so Enter can + // submit forms like a native checkbox/switch control. + delete labelProps.onKeyDown; + let {focusableProps} = useFocusable(props, ref); let interactions = mergeProps(pressProps, focusableProps); let domProps = filterDOMProps(props, {labelable: true}); From 3e8efccf3e85050d515af7a2b8f1eed45eb3531a Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 26 Jun 2026 11:52:09 +1000 Subject: [PATCH 2/3] avoid preventDefault for Enter on checkbox and radio --- packages/react-aria/src/interactions/usePress.ts | 4 ++++ packages/react-aria/src/toggle/useToggle.ts | 4 ---- packages/react-aria/test/interactions/usePress.test.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-aria/src/interactions/usePress.ts b/packages/react-aria/src/interactions/usePress.ts index 5e88c3b25af..5cf946f0f8e 100644 --- a/packages/react-aria/src/interactions/usePress.ts +++ b/packages/react-aria/src/interactions/usePress.ts @@ -1163,6 +1163,10 @@ function shouldPreventDefaultUp(target: Element) { function shouldPreventDefaultKeyboard(target: Element, key: string) { if (target instanceof HTMLInputElement) { + if (key === 'Enter' && (target.type === 'checkbox' || target.type === 'radio')) { + // Enter on a checkbox or radio should do an implicit form submission, but not toggle the input. + return false; + } return !isValidInputKey(target, key); } diff --git a/packages/react-aria/src/toggle/useToggle.ts b/packages/react-aria/src/toggle/useToggle.ts index c12894a4398..bd24c22ab4a 100644 --- a/packages/react-aria/src/toggle/useToggle.ts +++ b/packages/react-aria/src/toggle/useToggle.ts @@ -186,10 +186,6 @@ export function useToggle( isDisabled: isDisabled || isReadOnly }); - // Let the hidden input handle keyboard events natively so Enter can - // submit forms like a native checkbox/switch control. - delete labelProps.onKeyDown; - let {focusableProps} = useFocusable(props, ref); let interactions = mergeProps(pressProps, focusableProps); let domProps = filterDOMProps(props, {labelable: true}); diff --git a/packages/react-aria/test/interactions/usePress.test.js b/packages/react-aria/test/interactions/usePress.test.js index 2ac25bcc4d8..ec848c7467f 100644 --- a/packages/react-aria/test/interactions/usePress.test.js +++ b/packages/react-aria/test/interactions/usePress.test.js @@ -3315,7 +3315,7 @@ describe('usePress', function () { fireEvent.keyDown(el, {key: 'Enter'}); fireEvent.keyUp(el, {key: 'Enter'}); - // Enter key handled should do nothing on a checkbox + // Enter key handled should not result in a press event on a checkbox expect(events).toEqual([]); let allow = fireEvent.keyDown(el, {key: ' '}); From bdc3482fa569671b1adf60a6300640a6a71dc025 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 26 Jun 2026 11:52:28 +1000 Subject: [PATCH 3/3] fix lint --- packages/react-aria/src/radio/useRadio.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-aria/src/radio/useRadio.ts b/packages/react-aria/src/radio/useRadio.ts index 2ed784c2dec..a00ba7e4aa3 100644 --- a/packages/react-aria/src/radio/useRadio.ts +++ b/packages/react-aria/src/radio/useRadio.ts @@ -132,7 +132,6 @@ export function useRadio( } }); - let {focusableProps} = useFocusable( mergeProps(props, { onFocus: () => state.setLastFocusedValue(value)