From 1fcbc035b1c5c7b6aaf8202cbe830e454d56242f Mon Sep 17 00:00:00 2001 From: "yugo.innami" Date: Fri, 24 Apr 2026 01:10:15 +0900 Subject: [PATCH] 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});