Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions tests/src/tests/checkbox/checkbox-group-test.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
items = [],
disabledItems = [],
readonlyItems = [],
type,
onFormSubmit,
getValue: getValueProp,
setValue: setValueProp,
Expand All @@ -17,6 +18,7 @@
items?: string[];
disabledItems?: string[];
readonlyItems?: string[];
type?: "submit";
onFormSubmit?: (fd: FormData) => void;
setValue?: (value: string[]) => void;
getValue?: () => string[];
Expand All @@ -31,6 +33,7 @@
value={itemValue}
disabled={disabledItems?.includes(itemValue)}
readonly={readonlyItems?.includes(itemValue)}
{type}
>
{#snippet children({ checked, indeterminate })}
<span data-testid="{itemValue}-indicator">
Expand Down
42 changes: 29 additions & 13 deletions tests/src/tests/checkbox/checkbox-test.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
<script lang="ts">
import { Checkbox } from "bits-ui";

let { checked = false, ...restProps }: Checkbox.RootProps = $props();
let {
checked = false,
onFormSubmit,
...restProps
}: Checkbox.RootProps & {
onFormSubmit?: (fd: FormData) => void;
} = $props();
</script>

<main>
<p data-testid="binding">{checked}</p>
<Checkbox.Root name="terms" data-testid="root" bind:checked {...restProps}>
{#snippet children({ checked, indeterminate })}
<span data-testid="indicator">
{#if indeterminate}
indeterminate
{:else}
{checked}
{/if}
</span>
{/snippet}
</Checkbox.Root>
<form
method="POST"
onsubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
onFormSubmit?.(formData);
}}
>
<p data-testid="binding">{checked}</p>
<Checkbox.Root name="terms" data-testid="root" bind:checked {...restProps}>
{#snippet children({ checked, indeterminate })}
<span data-testid="indicator">
{#if indeterminate}
indeterminate
{:else}
{checked}
{/if}
</span>
{/snippet}
</Checkbox.Root>
<button type="submit" data-testid="submit">Submit</button>
</form>
</main>
72 changes: 69 additions & 3 deletions tests/src/tests/checkbox/checkbox.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -12,7 +11,7 @@ const kbd = getTestKbd();

const groupItems = ["a", "b", "c", "d"];

function setup(props?: Checkbox.RootProps) {
function setup(props?: ComponentProps<typeof CheckboxTest>) {
const returned = render(CheckboxTest, props);
const root = page.getByTestId("root");
return { ...returned, root };
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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[];
},
});
Expand All @@ -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;
Expand Down