diff --git a/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts b/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts index fd4a946e2..114b18f61 100644 --- a/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts +++ b/packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts @@ -194,7 +194,14 @@ export class CheckboxRootState { onkeydown(e: BitsKeyboardEvent) { if (this.trueDisabled || this.trueReadonly) return; - if (e.key === kbd.ENTER) e.preventDefault(); + if (e.key === kbd.ENTER) { + e.preventDefault(); + if (this.opts.type.current === "submit") { + const form = e.currentTarget.closest("form"); + form?.requestSubmit(); + } + return; + } if (e.key === kbd.SPACE) { e.preventDefault(); this.#toggle(); diff --git a/tests/src/tests/checkbox/checkbox-group-test.svelte b/tests/src/tests/checkbox/checkbox-group-test.svelte index 1fba9540b..92410d71a 100644 --- a/tests/src/tests/checkbox/checkbox-group-test.svelte +++ b/tests/src/tests/checkbox/checkbox-group-test.svelte @@ -6,6 +6,7 @@ items = [], disabledItems = [], readonlyItems = [], + type, onFormSubmit, getValue: getValueProp, setValue: setValueProp, @@ -17,6 +18,7 @@ items?: string[]; disabledItems?: string[]; readonlyItems?: string[]; + type?: "submit"; onFormSubmit?: (fd: FormData) => void; setValue?: (value: string[]) => void; getValue?: () => string[]; @@ -31,6 +33,7 @@ value={itemValue} disabled={disabledItems?.includes(itemValue)} readonly={readonlyItems?.includes(itemValue)} + {type} > {#snippet children({ checked, indeterminate })} diff --git a/tests/src/tests/checkbox/checkbox-test.svelte b/tests/src/tests/checkbox/checkbox-test.svelte index b63dc804c..94d37fdc1 100644 --- a/tests/src/tests/checkbox/checkbox-test.svelte +++ b/tests/src/tests/checkbox/checkbox-test.svelte @@ -1,20 +1,36 @@
-

{checked}

- - {#snippet children({ checked, indeterminate })} - - {#if indeterminate} - indeterminate - {:else} - {checked} - {/if} - - {/snippet} - +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + onFormSubmit?.(formData); + }} + > +

{checked}

+ + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + indeterminate + {:else} + {checked} + {/if} + + {/snippet} + + +
diff --git a/tests/src/tests/checkbox/checkbox.browser.test.ts b/tests/src/tests/checkbox/checkbox.browser.test.ts index e47acd015..9e884b90e 100644 --- a/tests/src/tests/checkbox/checkbox.browser.test.ts +++ b/tests/src/tests/checkbox/checkbox.browser.test.ts @@ -2,7 +2,6 @@ import { page, userEvent, type Locator } from "@vitest/browser/context"; import { describe, expect, it, vi } from "vitest"; import { render } from "vitest-browser-svelte"; import { type ComponentProps, tick } from "svelte"; -import type { Checkbox } from "bits-ui"; import { getTestKbd } from "../utils.js"; import CheckboxTest from "./checkbox-test.svelte"; import CheckboxGroupTest from "./checkbox-group-test.svelte"; @@ -12,7 +11,7 @@ const kbd = getTestKbd(); const groupItems = ["a", "b", "c", "d"]; -function setup(props?: Checkbox.RootProps) { +function setup(props?: ComponentProps) { const returned = render(CheckboxTest, props); const root = page.getByTestId("root"); return { ...returned, root }; @@ -148,6 +147,38 @@ describe("Single Checkbox", () => { await expect.element(indicator).not.toHaveTextContent("indeterminate"); await expect.element(page.getByRole("checkbox")).not.toBeChecked(); }); + + it("should submit the form when the `Enter` key is pressed with type='submit'", async () => { + let submittedValues: FormDataEntryValue[] | undefined; + const t = setup({ + type: "submit", + onFormSubmit: (fd: FormData) => { + submittedValues = fd.getAll("terms"); + }, + }); + await t.root.click(); + await expectChecked(t.root); + (t.root.element() as HTMLElement).focus(); + await userEvent.keyboard(kbd.ENTER); + expect(submittedValues).toEqual(["on"]); + await expectChecked(t.root); + }); + + it("should not toggle or submit the form when `Enter` is pressed on a checkbox with type='button'", async () => { + let submittedValues: FormDataEntryValue[] | undefined; + const t = setup({ + type: "button", + onFormSubmit: (fd: FormData) => { + submittedValues = fd.getAll("terms"); + }, + }); + await t.root.click(); + await expectChecked(t.root); + (t.root.element() as HTMLElement).focus(); + await userEvent.keyboard(kbd.ENTER); + expect(submittedValues).toBeUndefined(); + await expectChecked(t.root); + }); }); describe("Props and Events", () => { @@ -266,7 +297,7 @@ describe("Checkbox Group", () => { let submittedValues: string[] | undefined; const t = setupGroup({ name: "myGroup", - onFormSubmit: (fd) => { + onFormSubmit: (fd: FormData) => { submittedValues = fd.getAll("myGroup") as string[]; }, }); @@ -280,6 +311,41 @@ describe("Checkbox Group", () => { expect(submittedValues).toEqual(["a", "b"]); }); + it("should submit the form when `Enter` is pressed on a checkbox with type='submit'", async () => { + let submittedValues: string[] | undefined; + const t = setupGroup({ + name: "myGroup", + type: "submit", + onFormSubmit: (fd: FormData) => { + submittedValues = fd.getAll("myGroup") as string[]; + }, + }); + const [a] = t.checkboxes; + await a.click(); + await expectChecked(a); + (a.element() as HTMLElement).focus(); + await userEvent.keyboard(kbd.ENTER); + expect(submittedValues).toEqual(["a"]); + await expectChecked(a); + }); + + it("should not toggle or submit the form when `Enter` is pressed on a checkbox with default type", async () => { + let submittedValues: string[] | undefined; + const t = setupGroup({ + name: "myGroup", + onFormSubmit: (fd: FormData) => { + submittedValues = fd.getAll("myGroup") as string[]; + }, + }); + const [a] = t.checkboxes; + await a.click(); + await expectChecked(a); + (a.element() as HTMLElement).focus(); + await userEvent.keyboard(kbd.ENTER); + expect(submittedValues).toBeUndefined(); + await expectChecked(a); + }); + it("should handle binding value", async () => { const t = setupGroup(); const [a, b, _, d] = t.checkboxes;