react-spectrum
react-spectrum copied to clipboard
Event Bubbling Issue with Radio Component in Form Navigation
Provide a general summary of the issue here
We are currently developing an internal SSO form using React Aria for building our form components. We have encountered a specific issue with the RadioGroup and Radio components where the event bubbling seems to be stopped by the radio button logic. This is problematic for us as we intend to allow users to navigate to the next step in our form by pressing the 'Enter' key while a radio button is focused. However, the 'Enter' key event does not bubble up due to the internal handling in the Radio component, preventing our form from behaving as intended.
๐ค Expected Behavior?
Pressing the 'Enter' key while any Radio component is focused should allow the event to bubble up to parent elements where a handler could be triggered to navigate through the form.
๐ฏ Current Behavior
The 'Enter' key event does not bubble up from the Radio components, preventing the form from navigating to the next step.
๐ Possible Solution
We are looking for a way to modify or override the default behavior to allow the event to bubble up, or perhaps a prop that could be passed to the Radio or RadioGroup components to control this behavior.
๐ฆ Context
Here what I found when investigated the Radio code:
The usePress hook from react-aria is being used in the Radio component. This hook handles the press events (like mouse down, mouse up, key down, key up) and provides the pressProps that are spread onto the label element in the Radio component.
The usePress hook calls preventDefault on the event object for 'Enter' key press to prevent the default browser behavior. This is why the 'Enter' key press event is not bubbling up.
If you want the 'Enter' key press event to bubble up, you would need to modify the usePress hook to not call preventDefault on the 'Enter' key press event. However, this would require modifying the react-aria library, which is not recommended.
A better approach would be to handle the 'Enter' key press event at the individual radio button level. You can do this by adding an onKeyPress event handler to the Radio component and checking if the key pressed was 'Enter'.
function Radio(props: RadioProps, ref: ForwardedRef<HTMLInputElement>) {
[props, ref] = useContextProps(props, ref, RadioContext);
let state = React.useContext(RadioGroupStateContext)!;
let domRef = useObjectRef(ref);
let {inputProps, isSelected, isDisabled, isPressed: isPressedKeyboard} = useRadio({
...removeDataAttributes<RadioProps>(props),
// ReactNode type doesn't allow function children.
children: typeof props.children === 'function' ? true : props.children
}, state, domRef);
let {isFocused, isFocusVisible, focusProps} = useFocusRing();
let interactionDisabled = isDisabled || state.isReadOnly;
// Handle press state for full label. Keyboard press state is returned by useRadio
// since it is handled on the <input> element itself.
let [isPressed, setPressed] = useState(false);
let {pressProps} = usePress({
isDisabled: interactionDisabled,
onPressStart(e) {
if (e.pointerType !== 'keyboard') {
setPressed(true);
}
},
onPressEnd(e) {
if (e.pointerType !== 'keyboard') {
setPressed(false);
}
}
});
let {hoverProps, isHovered} = useHover({
isDisabled: interactionDisabled
});
let pressed = interactionDisabled ? false : (isPressed || isPressedKeyboard);
let renderProps = useRenderProps({
...props,
defaultClassName: 'react-aria-Radio',
values: {
isSelected,
isPressed: pressed,
isHovered,
isFocused,
isFocusVisible,
isDisabled,
isReadOnly: state.isReadOnly,
isInvalid: state.isInvalid,
isRequired: state.isRequired
}
});
let DOMProps = filterDOMProps(props);
delete DOMProps.id;
return (
<label
{...mergeProps(DOMProps, pressProps, hoverProps, renderProps)}
data-selected={isSelected || undefined}
data-pressed={pressed || undefined}
data-hovered={isHovered || undefined}
data-focused={isFocused || undefined}
data-focus-visible={isFocusVisible || undefined}
data-disabled={isDisabled || undefined}
data-readonly={state.isReadOnly || undefined}
data-invalid={state.isInvalid || undefined}
data-required={state.isRequired || undefined}>
<VisuallyHidden elementType="span">
<input {...mergeProps(inputProps, focusProps)} ref={domRef} />
</VisuallyHidden>
{renderProps.children}
</label>
);
}
๐ฅ๏ธ Steps to Reproduce
Upon pressing the enter key, the console.log will output de "radio" but not the "div". Also, you have to click on the radio button and press enter log something.
"use client"
import {
Label,
Radio,
RadioGroup
} from 'react-aria-components';
import CheckCircleIcon from '@spectrum-icons/workflow/CheckmarkCircle';
export default function RadioGroupExample() {
return (
<div className="bg-gradient-to-r from-blue-300 to-indigo-300 p-2 sm:p-8 rounded-lg flex justify-center">
<RadioGroup
className="flex flex-col gap-2 w-full max-w-[300px]"
defaultValue="Standard"
>
<Label className="text-xl text-slate-900 font-semibold font-serif">
Shipping
</Label>
<ShippingOption
name="Standard"
time="4-10 business days"
price="$4.99"
/>
<ShippingOption
name="Express"
time="2-5 business days"
price="$15.99"
/>
<ShippingOption
name="Lightning"
time="1 business day"
price="$24.99"
/>
</RadioGroup>
</div>
);
}
function ShippingOption({ name, time, price }: { name: string, time: string, price: string }) {
return (
<div onKeyDown={
// when the enter key is pressed, log the value of the radio button
(e) => {
if ((e as unknown as KeyboardEvent)?.key === "Enter") {
console.log("div", e);
}
}
}>
<Radio
value={name}
onKeyDown={
// when the enter key is pressed, log the value of the radio button
(e) => {
if ((e as unknown as KeyboardEvent)?.key === "Enter") {
console.log("Radio", e);
}
}
}
className={(
{ isFocusVisible, isSelected, isPressed }
) => `
group relative flex cursor-default rounded-lg px-4 py-3 shadow-lg outline-none bg-clip-padding border border-solid
${
isFocusVisible
? 'ring-2 ring-blue-600 ring-offset-1 ring-offset-white/80'
: ''
}
${
isSelected
? 'bg-blue-600 border-white/30 text-white'
: 'border-transparent'
}
${isPressed && !isSelected ? 'bg-blue-50' : ''}
${!isSelected && !isPressed ? 'bg-white' : ''}
`}
>
<div className="flex w-full items-center justify-between gap-3">
<div className="flex items-center shrink-0 text-blue-100 group-selected:text-white">
<CheckCircleIcon size="M" />
</div>
<div className="flex flex-1 flex-col">
<div className="text-lg font-serif font-semibold text-gray-900 group-selected:text-white">
{name}
</div>
<div className="inline text-gray-500 group-selected:text-sky-100">
{time}
</div>
</div>
<div className="font-medium font-mono text-gray-900 group-selected:text-white">
{price}
</div>
</div>
</Radio>
</div>
);
}
Version
1.1.1
What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
Mac OS
๐งข Your Company/Team
Adobe/react-aria
๐ท Tracking Issue
No response
Could you use a capturing listener here instead?
@LFDanLu Do you have any exemple I could try ?
https://codesandbox.io/p/sandbox/unruffled-mestorf-dd8h7d?file=%2Fsrc%2FApp.js%3A12%2C10, could you use a capturing listener to detect Enter on a radio input element?
I don't think you should do this. Enter is typically used to submit forms implicitly. You may confuse users by having Enter behave as a Tab depending on the context, in a form or out of one.
That said, I think we have a bug, I cannot implicitly submit from a RadioGroup. See native works: https://jsfiddle.net/xc1j9tg0/ I altered the codesandbox to something similar to this fiddle, the submit is not called. This is probably related to stopping the key from bubbling. I thought we had an issue already open for this, but I couldn't find it. So I'll leave this one for now.