+
+
diff --git a/stories/Toggle.stories.js b/stories/Toggle.stories.js
new file mode 100644
index 0000000..c31f15c
--- /dev/null
+++ b/stories/Toggle.stories.js
@@ -0,0 +1,122 @@
+import React from "react";
+import Toggle from "../app/components/Toggle";
+
+export default {
+ title: "Forms/Toggle",
+ component: Toggle,
+ parameters: {
+ layout: "centered",
+ },
+ argTypes: {
+ state: {
+ control: { type: "select" },
+ options: ["default", "hover", "focus"],
+ },
+ disabled: {
+ control: { type: "boolean" },
+ },
+ checked: {
+ control: { type: "boolean" },
+ },
+ showIcon: {
+ control: { type: "boolean" },
+ },
+ showText: {
+ control: { type: "boolean" },
+ },
+ },
+};
+
+const Template = (args) =>
;
+
+export const States = () => (
+
+
+
Toggle States
+
+
+
+
+
+
+
+
+
+);
+
+export const WithText = Template.bind({});
+WithText.args = {
+ label: "Text Toggle",
+ checked: false,
+ showText: true,
+ text: "Toggle",
+};
+
+export const WithIcon = Template.bind({});
+WithIcon.args = {
+ label: "Icon Toggle",
+ checked: false,
+ showIcon: true,
+ icon: "I",
+};
+
+export const Interactive = () => {
+ const [checked, setChecked] = React.useState(false);
+ const [state, setState] = React.useState("default");
+ const [disabled, setDisabled] = React.useState(false);
+
+ return (
+
+
+
Interactive Toggle
+
+ setChecked(!checked)}
+ state={state}
+ disabled={disabled}
+ />
+
+
+
+
Controls
+
+
+ setChecked(e.target.checked)}
+ />
+
+
+
+
+
+
+
+ setDisabled(e.target.checked)}
+ />
+
+
+
+
+
+ );
+};
diff --git a/tests/accessibility/Toggle.a11y.test.jsx b/tests/accessibility/Toggle.a11y.test.jsx
new file mode 100644
index 0000000..0fc7dfd
--- /dev/null
+++ b/tests/accessibility/Toggle.a11y.test.jsx
@@ -0,0 +1,112 @@
+import { expect, test, describe, it, vi } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { axe, toHaveNoViolations } from "jest-axe";
+import Toggle from "../../app/components/Toggle";
+
+expect.extend(toHaveNoViolations);
+
+describe("Toggle Accessibility", () => {
+ test("has proper ARIA attributes", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("aria-checked", "false");
+ expect(toggle).toHaveAttribute("type", "button");
+ });
+
+ test("has proper ARIA attributes when checked", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("aria-checked", "true");
+ });
+
+ test("has proper ARIA attributes when disabled", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("disabled");
+ });
+
+ test("has proper label association", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ const label = screen.getByText("Test Toggle");
+
+ expect(toggle).toBeInTheDocument();
+ expect(label).toBeInTheDocument();
+ });
+
+ test("handles keyboard navigation", () => {
+ const handleChange = vi.fn();
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ toggle.focus();
+ expect(toggle).toHaveFocus();
+
+ fireEvent.keyDown(toggle, { key: "Enter" });
+ expect(handleChange).toHaveBeenCalledTimes(1);
+
+ fireEvent.keyDown(toggle, { key: " " });
+ expect(handleChange).toHaveBeenCalledTimes(2);
+ });
+
+ test("handles disabled state accessibility", () => {
+ const handleChange = vi.fn();
+ render(
+
+ );
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("disabled");
+ expect(toggle).toHaveClass("cursor-not-allowed");
+
+ fireEvent.click(toggle);
+ expect(handleChange).not.toHaveBeenCalled();
+ });
+
+ test("handles focus state accessibility", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveClass("focus-visible:shadow-[0_0_5px_1px_#3281F8]");
+ });
+
+ test("has no accessibility violations", async () => {
+ const { container } = render(
);
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ test("has no accessibility violations when checked", async () => {
+ const { container } = render(
);
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ test("has no accessibility violations when disabled", async () => {
+ const { container } = render(
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ test("has no accessibility violations with icon", async () => {
+ const { container } = render(
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ test("has no accessibility violations with text", async () => {
+ const { container } = render(
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/tests/integration/Toggle.integration.test.jsx b/tests/integration/Toggle.integration.test.jsx
new file mode 100644
index 0000000..a2474b8
--- /dev/null
+++ b/tests/integration/Toggle.integration.test.jsx
@@ -0,0 +1,185 @@
+import React from "react";
+import { expect, test, describe, it, vi } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import Toggle from "../../app/components/Toggle";
+
+describe("Toggle Integration", () => {
+ test("handles form submission", () => {
+ const handleSubmit = vi.fn();
+
+ render(
+
+ );
+
+ const toggle = screen.getByRole("switch", { name: "Test Toggle" });
+ const submitButton = screen.getByRole("button", { name: "Submit" });
+
+ fireEvent.click(toggle);
+ fireEvent.click(submitButton);
+
+ expect(handleSubmit).toHaveBeenCalledTimes(1);
+ });
+
+ test("handles keyboard navigation between toggles", async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+
+
+
+ );
+
+ const firstToggle = screen.getByRole("switch", { name: "First Toggle" });
+ const secondToggle = screen.getByRole("switch", { name: "Second Toggle" });
+ const thirdToggle = screen.getByRole("switch", { name: "Third Toggle" });
+
+ await user.tab();
+ expect(firstToggle).toHaveFocus();
+
+ await user.tab();
+ expect(secondToggle).toHaveFocus();
+
+ await user.tab();
+ expect(thirdToggle).toHaveFocus();
+ });
+
+ test("handles dynamic prop changes", () => {
+ const { rerender } = render(
);
+
+ let toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("aria-checked", "false");
+
+ rerender(
);
+ toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("aria-checked", "true");
+
+ rerender(
);
+ toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("disabled");
+ });
+
+ test("handles multiple toggles in form", () => {
+ const handleChange1 = vi.fn();
+ const handleChange2 = vi.fn();
+
+ render(
+
+
+
+
+ );
+
+ const firstToggle = screen.getByRole("switch", { name: "First Toggle" });
+ const secondToggle = screen.getByRole("switch", { name: "Second Toggle" });
+
+ fireEvent.click(firstToggle);
+ expect(handleChange1).toHaveBeenCalledTimes(1);
+ expect(handleChange2).not.toHaveBeenCalled();
+
+ fireEvent.click(secondToggle);
+ expect(handleChange2).toHaveBeenCalledTimes(1);
+ expect(handleChange1).toHaveBeenCalledTimes(1);
+ });
+
+ test("handles state changes", () => {
+ const { rerender } = render(
);
+
+ let toggle = screen.getByRole("switch");
+ expect(toggle).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
+
+ rerender(
);
+ toggle = screen.getByRole("switch");
+ expect(toggle).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
+ });
+
+ test("handles content changes", () => {
+ const { rerender } = render(
);
+
+ let toggle = screen.getByRole("switch");
+ expect(toggle).not.toHaveTextContent("I");
+ expect(toggle).not.toHaveTextContent("Toggle");
+
+ rerender(
);
+ toggle = screen.getByRole("switch");
+ expect(toggle).toHaveTextContent("I");
+
+ rerender(
);
+ toggle = screen.getByRole("switch");
+ expect(toggle).toHaveTextContent("Toggle");
+
+ rerender(
+
+ );
+ toggle = screen.getByRole("switch");
+ expect(toggle).toHaveTextContent("I");
+ expect(toggle).toHaveTextContent("Toggle");
+ });
+
+ test("handles performance with many toggles", () => {
+ const toggles = Array.from({ length: 100 }, (_, i) => (
+
+ ));
+
+ const startTime = performance.now();
+ render(
{toggles}
);
+ const endTime = performance.now();
+
+ expect(endTime - startTime).toBeLessThan(1000); // Should render in less than 1 second
+ expect(screen.getAllByRole("switch")).toHaveLength(100);
+ });
+
+ test("handles rapid state changes", () => {
+ const handleChange = vi.fn();
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+
+ // Rapid clicks
+ for (let i = 0; i < 10; i++) {
+ fireEvent.click(toggle);
+ }
+
+ expect(handleChange).toHaveBeenCalledTimes(10);
+ });
+
+ test("handles mixed content types", () => {
+ render(
+
+
+
+
+
+
+ );
+
+ const iconToggle = screen.getByRole("switch", { name: "Icon Toggle" });
+ const textToggle = screen.getByRole("switch", { name: "Text Toggle" });
+ const bothToggle = screen.getByRole("switch", { name: "Both Toggle" });
+ const emptyToggle = screen.getByRole("switch", { name: "Empty Toggle" });
+
+ expect(iconToggle).toHaveTextContent("I");
+ expect(textToggle).toHaveTextContent("Toggle");
+ expect(bothToggle).toHaveTextContent("I");
+ expect(bothToggle).toHaveTextContent("Toggle");
+ expect(emptyToggle).not.toHaveTextContent("I");
+ expect(emptyToggle).not.toHaveTextContent("Toggle");
+ });
+});
diff --git a/tests/unit/Toggle.test.jsx b/tests/unit/Toggle.test.jsx
new file mode 100644
index 0000000..40b55af
--- /dev/null
+++ b/tests/unit/Toggle.test.jsx
@@ -0,0 +1,195 @@
+import { expect, test, describe, it, vi } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import Toggle from "../../app/components/Toggle";
+
+describe("Toggle Component", () => {
+ test("renders with default props", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ const label = screen.getByText("Test Toggle");
+
+ expect(toggle).toBeInTheDocument();
+ expect(label).toBeInTheDocument();
+ expect(toggle).toHaveAttribute("type", "button");
+ });
+
+ test("renders with custom props", () => {
+ render(
+
+ );
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toBeInTheDocument();
+ expect(toggle).toHaveAttribute("aria-checked", "true");
+ expect(toggle).toHaveAttribute("disabled");
+ });
+
+ test("handles checked state", () => {
+ const { rerender } = render(
);
+
+ let toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("aria-checked", "false");
+
+ rerender(
);
+ toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("aria-checked", "true");
+ });
+
+ test("handles disabled state", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("disabled");
+ expect(toggle).toHaveClass("cursor-not-allowed");
+ });
+
+ test("handles state prop", () => {
+ const { rerender } = render(
);
+
+ let toggle = screen.getByRole("switch");
+ expect(toggle).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
+
+ rerender(
);
+ toggle = screen.getByRole("switch");
+ expect(toggle).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
+ });
+
+ test("handles showIcon and icon props", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveTextContent("I");
+ });
+
+ test("handles showText and text props", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveTextContent("Toggle");
+ });
+
+ test("handles both icon and text", () => {
+ render(
+
+ );
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveTextContent("I");
+ expect(toggle).toHaveTextContent("Toggle");
+ });
+
+ test("calls onChange when clicked", () => {
+ const handleChange = vi.fn();
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ fireEvent.click(toggle);
+
+ expect(handleChange).toHaveBeenCalledTimes(1);
+ });
+
+ test("does not call onChange when disabled", () => {
+ const handleChange = vi.fn();
+ render(
+
+ );
+
+ const toggle = screen.getByRole("switch");
+ fireEvent.click(toggle);
+
+ expect(handleChange).not.toHaveBeenCalled();
+ });
+
+ test("applies correct classes for different states", () => {
+ const { rerender } = render(
);
+
+ let toggle = screen.getByRole("switch");
+ expect(toggle).toHaveClass("bg-[var(--color-surface-default-primary)]");
+
+ rerender(
);
+ toggle = screen.getByRole("switch");
+ expect(toggle).toHaveClass("bg-[var(--color-magenta-magenta100)]");
+
+ rerender(
);
+ toggle = screen.getByRole("switch");
+ expect(toggle).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
+ });
+
+ test("applies hover classes when not checked", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveClass(
+ "hover:!bg-[var(--color-surface-default-secondary)]"
+ );
+ });
+
+ test("does not apply hover classes when checked", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).not.toHaveClass(
+ "hover:!bg-[var(--color-surface-default-secondary)]"
+ );
+ });
+
+ test("applies focus-visible classes", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveClass("focus-visible:shadow-[0_0_5px_1px_#3281F8]");
+ });
+
+ test("applies correct size classes", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveClass("h-[var(--measures-sizing-032)]");
+ expect(toggle).toHaveClass("px-[16px]");
+ expect(toggle).toHaveClass("py-[8px]");
+ expect(toggle).toHaveClass("gap-[4px]");
+ });
+
+ test("applies correct text classes", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveClass("text-[12px]");
+ expect(toggle).toHaveClass("leading-[16px]");
+ });
+
+ test("applies correct label classes", () => {
+ render(
);
+
+ const label = screen.getByText("Test Toggle");
+ expect(label).toHaveClass("text-[12px]");
+ expect(label).toHaveClass("leading-[16px]");
+ expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
+ });
+
+ test("forwards ref correctly", () => {
+ const ref = vi.fn();
+ render(
);
+
+ expect(ref).toHaveBeenCalled();
+ });
+
+ test("applies custom className", () => {
+ render(
);
+
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveClass("custom-class");
+ });
+});