Toggle component

This commit is contained in:
Mats Bosson 2026-03-29 07:08:51 +07:00
parent 230133415b
commit d522090872
3 changed files with 99 additions and 0 deletions

View File

@ -0,0 +1,2 @@
export { Toggle } from "./toggle";
export type { ToggleProps } from "./toggle";

View File

@ -0,0 +1,57 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
import { createControllableSignal } from "../../primitives/create-controllable-signal";
export interface ToggleProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
/** Controlled pressed state. */
pressed?: boolean;
/** Default pressed state (uncontrolled). @default false */
defaultPressed?: boolean;
/** Called when pressed state changes. */
onPressedChange?: (pressed: boolean) => void;
children?: JSX.Element;
}
/**
* A two-state button that can be toggled on and off.
* Uses aria-pressed to communicate state to assistive technology.
*/
export function Toggle(props: ToggleProps): JSX.Element {
const [local, rest] = splitProps(props, [
"pressed",
"defaultPressed",
"onPressedChange",
"disabled",
"children",
"onClick",
]);
const options: Parameters<typeof createControllableSignal<boolean>>[0] = {
value: () => local.pressed,
defaultValue: () => local.defaultPressed ?? false,
};
if (local.onPressedChange) {
options.onChange = local.onPressedChange;
}
const [isPressed, setPressed] = createControllableSignal<boolean>(options);
const handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (e) => {
if (typeof local.onClick === "function") 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}
>
{local.children}
</button>
);
}

View File

@ -0,0 +1,40 @@
import { fireEvent, render, screen } from "@solidjs/testing-library";
import { describe, expect, it } from "vitest";
import { Toggle } from "../../../src/components/toggle/index";
describe("Toggle", () => {
it("is off by default", () => {
render(() => <Toggle>Bold</Toggle>);
expect(screen.getByRole("button").getAttribute("aria-pressed")).toBe("false");
});
it("toggles on click", () => {
render(() => <Toggle>Bold</Toggle>);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByRole("button").getAttribute("aria-pressed")).toBe("true");
});
it("respects defaultPressed=true", () => {
render(() => <Toggle defaultPressed>Bold</Toggle>);
expect(screen.getByRole("button").getAttribute("aria-pressed")).toBe("true");
});
it("controlled: stays at given value", () => {
render(() => <Toggle pressed={false} onPressedChange={() => {}}>Bold</Toggle>);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByRole("button").getAttribute("aria-pressed")).toBe("false");
});
it("data-state reflects pressed state", () => {
render(() => <Toggle>Bold</Toggle>);
expect(screen.getByRole("button").getAttribute("data-state")).toBe("off");
fireEvent.click(screen.getByRole("button"));
expect(screen.getByRole("button").getAttribute("data-state")).toBe("on");
});
it("does not toggle when disabled", () => {
render(() => <Toggle disabled>Bold</Toggle>);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByRole("button").getAttribute("aria-pressed")).toBe("false");
});
});