radix-ui / primitives

Radix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by @workos.
https://radix-ui.com/primitives
MIT License
15.79k stars 820 forks source link

Form submit event doesn't fire on enter when Checkboxis in focus #1595

Closed ziplb closed 2 years ago

ziplb commented 2 years ago

Bug report

Current Behavior

If Checkbox element is in focus, then form submit eventdoes not fire on Enter.

Expected behavior

Form submit event fires on Enter when focus is on Checkbox component.

Reproducible example

You can use the code snippet from the main documentation of Checkbox component. You need to add a submit event handler to the form:

<form onSubmit={() => console.log('submit')}>
...rest

Then you should click on the Checkbox so that it gets focus. After that, pressing enter will not cause the handler to be called.

Additional context

Most likely, this is due to the fact that the input itself doesn't receive focus, but the button responsible for styling the checkbox gets it(focus). The same issue is with RadioGroup component.

Your environment

Software Name(s) Version
Radix Package(s) @radix-ui/react-checkbox 1.0.0
React n/a 17.0.47
Browser Google Chrome 103.0.5060.134
Assistive tech
Node n/a 16.13.1
npm/yarn pnpm 7.1.6
Operating System macOS Monterey 12.4
benoitgrelard commented 2 years ago

Hi @ziplb,

I don't think that's a bug. As far as I know, implicit submission of forms only happens with inputs and buttons. Native checkbox/radio do not submit parent forms using the enter key. I tweaked your sandbox to use native form components instead and it definitely doesn't submit.

I will close the issue as I don't believe this is a thing, but please keep discussing if I am wrong.

zgottlieb commented 2 years ago

As far as I know, implicit submission of forms only happens with inputs and buttons. Native checkbox/radio do not submit parent forms using the enter key.

@benoitgrelard, I've seen that mentioned elsewhere about HTML forms, though it's unclear if that's intended behavior or an actual bug in a prior implementation of the HTML Form spec.

I did a little more research and came across this example from MDN, which I adapted into this CodeSandbox. Judging by that, it does look like submitting a form on Enter from a radio button does in fact work.

I've been trying to get this working with the RadioGroup in Radix UI, and while I've managed to work around it by triggering a form submission onKeyDown on the RadioGroup, I'd prefer an option where it works more similarly to the MDN example.

benoitgrelard commented 2 years ago

Hey @zgottlieb,

Thanks for doing more research into this. It definitely looks like there are some inconsistencies between browsers.

Taking your example further, I can see that indeed the form is submitted implicitly when pressing enter on radios or checkbox in Chrome, Safari, Firefox.

However, only Firefox seems to continue to do that if I remove the submit button from the form. Chrome and Safari seem to rely on the presence of a submit button to implicitly submit on those other types of inputs.

I'm not sure where we should stand on this to be honest. Additionally I also think that in user land it's pretty rare/unknown that this is even possible (as a developer I didn't know myself).

mstade commented 1 year ago

Running into this issue as well, is there a workaround available beyond catching the keydown event and submitting the form programmatically?

Saadchr commented 1 year ago

I'm facing the same issue. It is very useful to be able to quickly submit a form by clicking on Enter. Here is my workaround with an useEffect hook.

"use client";
import React from "react";
import { produce } from "immer";
import {
  Button,
  Checkbox,
  Text,
  Flex,
  Heading,
  RadioGroup,
  RadioGroupRoot,
  RadioGroupItem,
  Grid,
} from "@radix-ui/themes";

import * as Form from "@radix-ui/react-form";

const INITIAL_STATE = {
  size: "",
  toppings: {},
};

const SIZES = [
  { slug: "sm", label: 'Small (10")' },
  { slug: "md", label: 'Medium (12")' },
  { slug: "lg", label: 'Large (14")' },
  { slug: "xl", label: 'Pizza For Days (16")' },
];
const TOPPINGS = [
  { slug: "anchovies", label: "Anchovies" },
  { slug: "mushrooms", label: "Mushrooms" },
  { slug: "green-pepper", label: "Green Pepper" },
  { slug: "onions", label: "Onions" },
  { slug: "pineapple", label: "Pineapple" },
  { slug: "pepperoni", label: "Pepperoni" },
  { slug: "sausage", label: "Sausage" },
  { slug: "chicken", label: "Chicken" },
  { slug: "bacon", label: "Bacon" },
  { slug: "feta", label: "Feta" },
  { slug: "provolone", label: "Provolone" },
  { slug: "gummy-bears", label: "Gummy Bears" },
  { slug: "popcorn", label: "Popcorn" },
  { slug: "lucky-charms", label: "Lucky Charms" },
  { slug: "ice-cream", label: "Vanilla Ice Cream" },
  { slug: "cotton-candy", label: "Cotton Candy" },
];

type State = {
  size: string;
  toppings: Record<string, boolean>;
};

type Action =
  | { type: "select-size"; slug: string }
  | { type: "add-topping"; slug: string }
  | { type: "remove-topping"; slug: string }
  | { type: "add-all-toppings"; toppingSlugs: string[] }
  | { type: "remove-all-toppings" };

function reducer(state: State, action: Action): State {
  return produce(state, (draftState: any) => {
    switch (action.type) {
      case "select-size": {
        draftState.size = action.slug;
        break;
      }

      case "add-topping": {
        draftState.toppings[action.slug] = true;
        break;
      }

      case "remove-topping": {
        delete draftState.toppings[action.slug];
        break;
      }

      case "add-all-toppings": {
        action.toppingSlugs.forEach((slug: any) => {
          draftState.toppings[slug] = true;
        });
        break;
      }

      case "remove-all-toppings": {
        draftState.toppings = {};
        break;
      }
    }
  });
}

export default function Home() {
  console.log("render");
  const id = React.useId();

  const [state, dispatch] = React.useReducer(reducer, INITIAL_STATE);
  function handleToggleAllToppings() {
    if (hasSelectedAllToppings) {
      dispatch({
        type: "remove-all-toppings",
      });
    } else {
      dispatch({
        type: "add-all-toppings",
        toppingSlugs: TOPPINGS.map((topping) => topping.slug),
      });
    }
  }

  console.log(state);

  const hasSelectedAllToppings = TOPPINGS.every(
    ({ slug }) => state.toppings[slug]
  );

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event?.preventDefault();
    console.info(event);
    window.alert(JSON.stringify(state, null, 2));
  }

  React.useEffect(() => {
    const handleKeyDown = (event: any) => {
      if (event.key === "Enter") {
        event.preventDefault(); // To prevent any default behavior like submitting the form
        window.alert(JSON.stringify(state, null, 2));
      }
    };

    document.addEventListener("keydown", handleKeyDown);

    return () => {
      document.removeEventListener("keydown", handleKeyDown);
    };
  }, [state]); // Correct dependency array

  return (
    <main className=" border-red-900 border min-h-full p-10 flex  justify-center items-center">
      <form
        className=" flex flex-col gap-3 min-w-[80%] max-w-[600px]"
        onSubmit={handleSubmit}
      >
        <Heading>Your order</Heading>
        <fieldset className="border border-solid border-gray-900 p-3">
          <legend>
            <Text size={"2"}>Choose your size</Text>
          </legend>

          <RadioGroupRoot
            name="size"
            required={true}
            onValueChange={(value) => {
              dispatch({ type: "select-size", slug: value });
            }}
          >
            <Flex gap="2" direction="column">
              {SIZES.map(({ slug, label }) => (
                <Text key={slug} as="label" size="1">
                  <label>
                    <Flex className="border border-500-red" gap={"2"}>
                      <RadioGroupItem
                        required={true}
                        value={slug}
                        id={`size-${slug}-${id}`}
                      />
                      {label}
                    </Flex>
                  </label>
                </Text>
              ))}
            </Flex>
          </RadioGroupRoot>
        </fieldset>

        <fieldset className="border border-solid border-gray-900 p-3">
          <legend>
            <Text size={"2"}>Select your pizza toppings</Text>
          </legend>
          <Grid columns="" gap="3" width="auto">
            {TOPPINGS.map(({ slug, label }) => {
              const hasTopping = !!state.toppings[slug];

              return (
                <Text as="label" size={"1"} key={slug}>
                  <Flex className="border border-500-red" gap="2">
                    <Checkbox
                      size={"1"}
                      id={`topping-${slug}-${id}`}
                      checked={hasTopping}
                      onCheckedChange={() => {
                        dispatch({
                          type: hasTopping ? "remove-topping" : "add-topping",
                          slug,
                        });
                      }}
                    />
                    {label}
                  </Flex>
                </Text>
              );
            })}
            <Flex justify={"end"}>
              <Button
                variant="soft"
                type="button"
                onClick={handleToggleAllToppings}
              >
                {" "}
                {hasSelectedAllToppings ? "Remove all" : "Select all"}
              </Button>
            </Flex>
          </Grid>
        </fieldset>
        <Button type="submit">Take your order</Button>
        <input type="submit" style={{ display: "none" }} />
      </form>
    </main>
  );
}
ronaldruzicka commented 5 months ago

Hi, is there any chance to reopen this issue?

We have the same problem with radio, checkbox and select. I created a sandbox where is a FormRadix and a FormNative. If you select inputs with keyboard and hit enter, in FormNative even if you are focused on radio, checkbox or select the form submits.

On the other hand in FormRadix it doesn't. With radio and checkbox it does nothing, but when you are focused on select it opens. Native select element can be opened only with Spacebar not Enter ...is there any particular reason why it should be opened with Enter as well?

david-arteaga commented 2 months ago

For what it's worth, I do think it's worth tackling this discrepancy between native html elements and Radix elements.

When you do include a <button type"submit"> in a form, which I'd say is by far the most common case when using <form>s, the behavior across browsers is consistent (Enter on checkbox, radio, select does submit the form). And the radix "equivalent" components don't match this behavior.

I think many developers expect Radix primitives to behave the same (or at least as close as possible) to native HTML elements, and this discrepancy does introduce friction and was certainly a surprise for me when I started using Radix.