Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event Bubbling Issue with Radio Component in Form Navigation #6235

Open
dylandifilippo opened this issue Apr 19, 2024 · 4 comments
Open

Event Bubbling Issue with Radio Component in Form Navigation #6235

dylandifilippo opened this issue Apr 19, 2024 · 4 comments
Labels
bug Something isn't working

Comments

@dylandifilippo
Copy link

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

@LFDanLu
Copy link
Member

LFDanLu commented Apr 25, 2024

Could you use a capturing listener here instead?

@dylandifilippo
Copy link
Author

@LFDanLu Do you have any exemple I could try ?

@LFDanLu
Copy link
Member

LFDanLu commented May 2, 2024

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?

@snowystinger
Copy link
Member

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.

@snowystinger snowystinger added the bug Something isn't working label May 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants