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>({
|
const [isPressed, setPressed] = createControllableSignal<boolean>({
|
||||||
value: () => local.pressed,
|
value: () => local.pressed,
|
||||||
defaultValue: () => local.defaultPressed ?? false,
|
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 (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-pressed={isPressed()}
|
aria-pressed={isPressed()}
|
||||||
data-state={isPressed() ? "on" : "off"}
|
data-state={isPressed() ? "on" : "off"}
|
||||||
disabled={local.disabled}
|
disabled={local.disabled}
|
||||||
onClick={handleClick}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
|
onClick={() => {
|
||||||
|
if (!local.disabled) setPressed(!isPressed());
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{local.children}
|
{local.children}
|
||||||
</button>
|
</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