Repository: tailwindlabs/headlessui (TypeScript)

Issue:
[a11y]: Checkbox does not submit form on enter
**What package within Headless UI are you using?**

@headlessui/react

**What version of that package are you using?**

For example: v2.0.0-alpha.4 (but also occurs in latest stable version)

**What browser are you using?**

Chrome

**Describe your issue**

The current <Checkbox /> implementation does not support to submit the form on enter. (The <Switch> components works)
Example from the docs:
```tsx
import { Checkbox } from '@headlessui/react'
import { useState } from 'react'

function Example() {
  const [enabled, setEnabled] = useState(true)

  return (
    <form action="/notification-settings" method="post">
      <Checkbox
        checked={enabled}
        onChange={setEnabled}
        name="notifications"
        className="group block size-4 rounded border bg-white data-[checked]:bg-blue-500"
      >
        <svg className="stroke-white opacity-0 group-data-[checked]:opacity-100" viewBox="0 0 14 14" fill="none">
          <path d="M3 8L6 11L11 3.5" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </Checkbox>
      <button>Submit</button>
    </form>
  )
}
```



Discussion:
Hey!

This should be fixed by #2962, and will be available in the next release.

You can already try it using:
- `npm install @headlessui/react@insiders`.
@RobinMalfait Wow, thank you very much for fixing this so fast!
Any plans when the next release will be released? 
Hey just a heads up that we noticed today that our insiders builds were broken. If you now try and install the latest insiders you should get this updated checkbox behavior 👍

```sh
npm install @headlessui/react@insiders
```



Patch:
diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index 3f73116555..61c089882c 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Use `isFocused` instead of `isFocusVisible` for `Input` and `Textarea` components ([#2940](https://github.com/tailwindlabs/headlessui/pull/2940))
 - Ensure `children` prop of `Field` component can be a render prop ([#2941](https://github.com/tailwindlabs/headlessui/pull/2941))
 - Add `hidden` attribute to internal `<Hidden />` component when the `Features.Hidden` feature is used ([#2955](https://github.com/tailwindlabs/headlessui/pull/2955))
+- Attempt form submission when pressing `Enter` on `Checkbox` component ([#2962](https://github.com/tailwindlabs/headlessui/pull/2962))
 
 ## [2.0.0-alpha.4] - 2024-01-03
 
diff --git a/packages/@headlessui-react/src/components/checkbox/checkbox.tsx b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx
index ec1cbf04b1..aaa7ad25e6 100644
--- a/packages/@headlessui-react/src/components/checkbox/checkbox.tsx
+++ b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx
@@ -21,6 +21,7 @@ import { FormFields } from '../../internal/form-fields'
 import { useProvidedId } from '../../internal/id'
 import type { Props } from '../../types'
 import { isDisabledReactIssue7711 } from '../../utils/bugs'
+import { attemptSubmit } from '../../utils/form'
 import {
   forwardRefWithAs,
   mergeProps,
@@ -110,20 +111,22 @@ function CheckboxFn<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TTyp
 
   let handleClick = useEvent((event: ReactMouseEvent) => {
     if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
+    event.preventDefault()
     toggle()
   })
 
-  let handleKeyDown = useEvent((event: ReactKeyboardEvent) => {
-    if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
-
-    switch (event.key) {
-      case Keys.Space:
-        event.preventDefault()
-        toggle()
-        break
+  let handleKeyUp = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
+    if (event.key === Keys.Space) {
+      event.preventDefault()
+      toggle()
+    } else if (event.key === Keys.Enter) {
+      attemptSubmit(event.currentTarget)
     }
   })
 
+  // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button.
+  let handleKeyPress = useEvent((event: ReactKeyboardEvent<HTMLElement>) => event.preventDefault())
+
   let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false })
   let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled ?? false })
   let { pressed: active, pressProps } = useActivePress({ disabled: disabled ?? false })
@@ -139,7 +142,8 @@ function CheckboxFn<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TTyp
       'aria-disabled': disabled ? true : undefined,
       indeterminate: indeterminate ? 'true' : undefined,
       tabIndex: 0,
-      onKeyDown: disabled ? undefined : handleKeyDown,
+      onKeyUp: disabled ? undefined : handleKeyUp,
+      onKeyPress: disabled ? undefined : handleKeyPress,
       onClick: disabled ? undefined : handleClick,
     },
     focusProps,


Test patch:
diff --git a/packages/@headlessui-react/src/components/checkbox/checkbox.test.tsx b/packages/@headlessui-react/src/components/checkbox/checkbox.test.tsx
index b8b2c46496..7c1c60ec8d 100644
--- a/packages/@headlessui-react/src/components/checkbox/checkbox.test.tsx
+++ b/packages/@headlessui-react/src/components/checkbox/checkbox.test.tsx
@@ -119,3 +119,47 @@ describe.each([
     })
   )
 })
+
+describe('Form submissions', () => {
+  it('should be possible to use in an uncontrolled way', async () => {
+    let handleSubmission = jest.fn()
+
+    render(
+      <form
+        onSubmit={(e) => {
+          e.preventDefault()
+          handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
+        }}
+      >
+        <Checkbox name="notifications" />
+      </form>
+    )
+
+    // Focus the checkbox
+    await focus(getCheckbox())
+
+    // Submit
+    await press(Keys.Enter)
+
+    // No values
+    expect(handleSubmission).toHaveBeenLastCalledWith({})
+
+    // Toggle
+    await click(getCheckbox())
+
+    // Submit
+    await press(Keys.Enter)
+
+    // Notifications should be on
+    expect(handleSubmission).toHaveBeenLastCalledWith({ notifications: 'on' })
+
+    // Toggle
+    await click(getCheckbox())
+
+    // Submit
+    await press(Keys.Enter)
+
+    // Notifications should be off (or in this case, non-existant)
+    expect(handleSubmission).toHaveBeenLastCalledWith({})
+  })
+})