Checkbox component
This commit is contained in:
parent
5bd158782a
commit
3a84dfaac9
@ -11,6 +11,9 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"useConst": "error",
|
"useConst": "error",
|
||||||
"useTemplate": "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