Switch component
Also fix Toggle component to properly handle onChange callback and onClick handler binding.
This commit is contained in:
parent
3def20bf0d
commit
5af7dc6cfa
2
packages/core/src/components/switch/index.ts
Normal file
2
packages/core/src/components/switch/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { Switch } from "./switch";
|
||||
export type { SwitchProps } from "./switch";
|
||||
70
packages/core/src/components/switch/switch.tsx
Normal file
70
packages/core/src/components/switch/switch.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import type { JSX } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
import { createControllableSignal } from "../../primitives/create-controllable-signal";
|
||||
|
||||
export interface SwitchProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
/** Controlled checked state. */
|
||||
checked?: boolean;
|
||||
/** Default checked state (uncontrolled). @default false */
|
||||
defaultChecked?: boolean;
|
||||
/** Called when checked state changes. */
|
||||
onCheckedChange?: (checked: boolean) => 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* An on/off control. Semantically equivalent to a checkbox but visually
|
||||
* represents a binary switch. Uses role="switch" and aria-checked.
|
||||
*/
|
||||
export function Switch(props: SwitchProps): JSX.Element {
|
||||
const [local, rest] = splitProps(props, [
|
||||
"checked",
|
||||
"defaultChecked",
|
||||
"onCheckedChange",
|
||||
"disabled",
|
||||
"required",
|
||||
"name",
|
||||
"value",
|
||||
"children",
|
||||
"onClick",
|
||||
]);
|
||||
|
||||
const [isChecked, setChecked] = createControllableSignal<boolean>({
|
||||
value: () => local.checked,
|
||||
defaultValue: () => local.defaultChecked ?? false,
|
||||
onChange: local.onCheckedChange ?? (() => {}),
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={isChecked()}
|
||||
aria-required={local.required || undefined}
|
||||
data-state={isChecked() ? "checked" : "unchecked"}
|
||||
disabled={local.disabled}
|
||||
{...rest}
|
||||
onClick={() => {
|
||||
if (!local.disabled) setChecked(!isChecked());
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
{local.name && (
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-hidden="true"
|
||||
tabIndex={-1}
|
||||
name={local.name}
|
||||
value={local.value ?? "on"}
|
||||
checked={isChecked()}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -29,22 +29,19 @@ export function Toggle(props: ToggleProps): JSX.Element {
|
||||
const [isPressed, setPressed] = createControllableSignal<boolean>({
|
||||
value: () => local.pressed,
|
||||
defaultValue: () => local.defaultPressed ?? false,
|
||||
onChange: local.onPressedChange,
|
||||
onChange: local.onPressedChange ?? (() => {}),
|
||||
});
|
||||
|
||||
const handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (e) => {
|
||||
local.onClick?.(e);
|
||||
if (!local.disabled) setPressed(!isPressed());
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={isPressed()}
|
||||
data-state={isPressed() ? "on" : "off"}
|
||||
disabled={local.disabled}
|
||||
onClick={handleClick}
|
||||
{...rest}
|
||||
onClick={() => {
|
||||
if (!local.disabled) setPressed(!isPressed());
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</button>
|
||||
|
||||
45
packages/core/tests/components/switch/switch.test.tsx
Normal file
45
packages/core/tests/components/switch/switch.test.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { fireEvent, render, screen } from "@solidjs/testing-library";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Switch } from "../../../src/components/switch/index";
|
||||
|
||||
describe("Switch", () => {
|
||||
it("renders with role=switch", () => {
|
||||
render(() => <Switch />);
|
||||
expect(screen.getByRole("switch")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is unchecked by default", () => {
|
||||
render(() => <Switch />);
|
||||
expect(screen.getByRole("switch").getAttribute("aria-checked")).toBe("false");
|
||||
});
|
||||
|
||||
it("toggles on click", () => {
|
||||
render(() => <Switch />);
|
||||
fireEvent.click(screen.getByRole("switch"));
|
||||
expect(screen.getByRole("switch").getAttribute("aria-checked")).toBe("true");
|
||||
});
|
||||
|
||||
it("respects defaultChecked=true", () => {
|
||||
render(() => <Switch defaultChecked />);
|
||||
expect(screen.getByRole("switch").getAttribute("aria-checked")).toBe("true");
|
||||
});
|
||||
|
||||
it("data-state reflects checked state", () => {
|
||||
render(() => <Switch />);
|
||||
expect(screen.getByRole("switch").getAttribute("data-state")).toBe("unchecked");
|
||||
fireEvent.click(screen.getByRole("switch"));
|
||||
expect(screen.getByRole("switch").getAttribute("data-state")).toBe("checked");
|
||||
});
|
||||
|
||||
it("does not toggle when disabled", () => {
|
||||
render(() => <Switch disabled />);
|
||||
fireEvent.click(screen.getByRole("switch"));
|
||||
expect(screen.getByRole("switch").getAttribute("aria-checked")).toBe("false");
|
||||
});
|
||||
|
||||
it("controlled: stays at given value", () => {
|
||||
render(() => <Switch checked={false} onCheckedChange={() => {}} />);
|
||||
fireEvent.click(screen.getByRole("switch"));
|
||||
expect(screen.getByRole("switch").getAttribute("aria-checked")).toBe("false");
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user