Checkbox component
This commit is contained in:
parent
5bd158782a
commit
3a84dfaac9
@ -11,6 +11,9 @@
|
||||
"style": {
|
||||
"useConst": "error",
|
||||
"useTemplate": "error"
|
||||
},
|
||||
"a11y": {
|
||||
"useSemanticElements": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
97
packages/core/src/components/checkbox/checkbox.tsx
Normal file
97
packages/core/src/components/checkbox/checkbox.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import type { JSX } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
import { createControllableSignal } from "../../primitives/create-controllable-signal";
|
||||
|
||||
/** Represents all three states a checkbox can be in. */
|
||||
export type CheckedState = boolean | "indeterminate";
|
||||
|
||||
export interface CheckboxProps
|
||||
extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> {
|
||||
/** Controlled checked state. */
|
||||
checked?: CheckedState;
|
||||
/** Default checked state (uncontrolled). @default false */
|
||||
defaultChecked?: CheckedState;
|
||||
/** Called when checked state changes. */
|
||||
onCheckedChange?: (checked: CheckedState) => void;
|
||||
/** Native form field name — renders a hidden checkbox input. */
|
||||
name?: string;
|
||||
/** Value submitted with the form when checked. @default "on" */
|
||||
value?: string;
|
||||
required?: boolean;
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
/**
|
||||
* A tri-state checkbox control supporting checked, unchecked, and indeterminate.
|
||||
* Click cycle: indeterminate → checked → unchecked → checked.
|
||||
*/
|
||||
export function Checkbox(props: CheckboxProps): JSX.Element {
|
||||
const [local, rest] = splitProps(props, [
|
||||
"checked",
|
||||
"defaultChecked",
|
||||
"onCheckedChange",
|
||||
"disabled",
|
||||
"required",
|
||||
"name",
|
||||
"value",
|
||||
"children",
|
||||
]);
|
||||
|
||||
const [isChecked, setChecked] = createControllableSignal<CheckedState>(
|
||||
local.onCheckedChange
|
||||
? {
|
||||
value: () => local.checked,
|
||||
defaultValue: () => local.defaultChecked ?? false,
|
||||
onChange: local.onCheckedChange,
|
||||
}
|
||||
: {
|
||||
value: () => local.checked,
|
||||
defaultValue: () => local.defaultChecked ?? false,
|
||||
},
|
||||
);
|
||||
|
||||
const ariaChecked = (): boolean | "mixed" => {
|
||||
const c = isChecked();
|
||||
return c === "indeterminate" ? "mixed" : c;
|
||||
};
|
||||
|
||||
const dataState = (): string => {
|
||||
const c = isChecked();
|
||||
if (c === "indeterminate") return "indeterminate";
|
||||
return c ? "checked" : "unchecked";
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="checkbox"
|
||||
aria-checked={ariaChecked()}
|
||||
aria-required={local.required || undefined}
|
||||
data-state={dataState()}
|
||||
disabled={local.disabled}
|
||||
{...rest}
|
||||
onClick={() => {
|
||||
if (!local.disabled) {
|
||||
const current = isChecked();
|
||||
setChecked(current === "indeterminate" ? true : !current);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
{local.name && (
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-hidden="true"
|
||||
tabIndex={-1}
|
||||
name={local.name}
|
||||
value={local.value ?? "on"}
|
||||
checked={isChecked() === true}
|
||||
ref={(el) => {
|
||||
if (el) el.indeterminate = isChecked() === "indeterminate";
|
||||
}}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
2
packages/core/src/components/checkbox/index.ts
Normal file
2
packages/core/src/components/checkbox/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { Checkbox } from "./checkbox";
|
||||
export type { CheckboxProps, CheckedState } from "./checkbox";
|
||||
53
packages/core/tests/components/checkbox/checkbox.test.tsx
Normal file
53
packages/core/tests/components/checkbox/checkbox.test.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { fireEvent, render, screen } from "@solidjs/testing-library";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Checkbox } from "../../../src/components/checkbox/index";
|
||||
|
||||
describe("Checkbox", () => {
|
||||
it("renders with role=checkbox", () => {
|
||||
render(() => <Checkbox />);
|
||||
expect(screen.getByRole("checkbox")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is unchecked by default", () => {
|
||||
render(() => <Checkbox />);
|
||||
expect(screen.getByRole("checkbox").getAttribute("aria-checked")).toBe("false");
|
||||
expect(screen.getByRole("checkbox").getAttribute("data-state")).toBe("unchecked");
|
||||
});
|
||||
|
||||
it("checks on click", () => {
|
||||
render(() => <Checkbox />);
|
||||
fireEvent.click(screen.getByRole("checkbox"));
|
||||
expect(screen.getByRole("checkbox").getAttribute("aria-checked")).toBe("true");
|
||||
expect(screen.getByRole("checkbox").getAttribute("data-state")).toBe("checked");
|
||||
});
|
||||
|
||||
it("unchecks when clicked again", () => {
|
||||
render(() => <Checkbox defaultChecked />);
|
||||
fireEvent.click(screen.getByRole("checkbox"));
|
||||
expect(screen.getByRole("checkbox").getAttribute("aria-checked")).toBe("false");
|
||||
});
|
||||
|
||||
it("supports indeterminate default state", () => {
|
||||
render(() => <Checkbox defaultChecked="indeterminate" />);
|
||||
expect(screen.getByRole("checkbox").getAttribute("aria-checked")).toBe("mixed");
|
||||
expect(screen.getByRole("checkbox").getAttribute("data-state")).toBe("indeterminate");
|
||||
});
|
||||
|
||||
it("indeterminate -> checked on click", () => {
|
||||
render(() => <Checkbox defaultChecked="indeterminate" />);
|
||||
fireEvent.click(screen.getByRole("checkbox"));
|
||||
expect(screen.getByRole("checkbox").getAttribute("aria-checked")).toBe("true");
|
||||
});
|
||||
|
||||
it("does not toggle when disabled", () => {
|
||||
render(() => <Checkbox disabled />);
|
||||
fireEvent.click(screen.getByRole("checkbox"));
|
||||
expect(screen.getByRole("checkbox").getAttribute("aria-checked")).toBe("false");
|
||||
});
|
||||
|
||||
it("controlled: stays at given value", () => {
|
||||
render(() => <Checkbox checked={false} onCheckedChange={() => {}} />);
|
||||
fireEvent.click(screen.getByRole("checkbox"));
|
||||
expect(screen.getByRole("checkbox").getAttribute("aria-checked")).toBe("false");
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user