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

Checkbox and other components do not render <input> element with form attribute #2530

Open
vincerubinetti opened this issue Nov 16, 2023 · 0 comments · May be fixed by #2668
Open

Checkbox and other components do not render <input> element with form attribute #2530

vincerubinetti opened this issue Nov 16, 2023 · 0 comments · May be fixed by #2668

Comments

@vincerubinetti
Copy link

vincerubinetti commented Nov 16, 2023

Bug report

Current Behavior

In #874, @benoitgrelard says...

That is correct, if it's outside of a form, there isn't any need for any native component anyway because bubbling only makes sense within forms.

That is not correct. See a simplified example of my case:

<div>
  <AbstractedCheckbox label="Accept terms and conditions" name="consented" form="terms-form" />
  <form id="terms-form" onSubmit={(event) => console.log(Object.fromEntries(new FormData(event.target)))}/>
</div>

// result -> {}
// result when form wraps checkbox -> { consented: "on" }

Forms are not always the parent of form fields. In the above case, Radix doesn't realize it's part of a form, and so doesn't render an <input> element, and the checkbox state doesn't end up in the FormData.

Expected behavior

An <input>/native element is rendered when the form attribute is present.

Suggested solution

This line in Checkbox.tsx (and in Switch and elsewhere)...

const isFormControl = button ? Boolean(button.closest('form')) : true;

should be changed to something like...

const isFormControl = button ? Boolean(button.closest('form')) || !!checkboxProps.form : true;

Or... just always render an <input>? Not sure why it was decided to avoid it, every other library, headless or not, seems to always render an <input> and just visually and accessibly hide it as appropriate.

Additional context

This is a perfectly valid feature of the HTML spec...

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form
https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fae-form

... so I don't like having to justify why I would need it, but here is some context.

I tend to do it this because that way the form does not affect layout or styling. Yes I'm aware of display: contents but that does not prevent it from completely changing the CSS selectors required. As such, I tend to have an abstract form component that gets portaled to the body.

I'm sure there are many other cases why someone might need the input element there.

Workaround

As a very unpleasant workaround, to my <AbstractCheckbox> component I've had to track and sync a local copy of the checked state and add an invisible (visually and accessibly) <input> so the form can pick it up, like this (irrelevant stuff removed):

type Props = {
  label: ReactNode;
  value?: boolean;
  onChange?: (value: boolean) => void;
} & CheckboxProps;

const Checkbox = ({ label, tooltip, value, onChange, ...props }: Props) => {
  const id = useId();

  const [checked, setChecked] = useState(false);
  useEffect(() => {
    if (value !== undefined) setChecked(value);
  }, [value]);

  return (
    <div>
      <Root
        {...props}
        id={id}
        checked={checked}
        onCheckedChange={(checked) => {
          if (onChange) onChange(!!checked);
          setChecked(!!checked);
        }}
      >
        <Indicator>
          <FaCheck />
        </Indicator>
      </Root>

      <label htmlFor={id}>
        {label}
      </label>

      {/* https://github.com/radix-ui/primitives/issues/2530 */}
      <input
        type="checkbox"
        form={props.form}
        name={props.name}
        style={{ display: "none" }}
        checked={checked}
        readOnly
      />
    </div>
  );
};

export default Checkbox;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants