-
Content Filter
-
-
- All
-
-
- Featured
-
-
- Recent
-
-
-
-
-
-
Featured Article
-
- This is a featured article that shows when "All" or "Featured"
- is selected.
-
-
-
-
Recent Post
-
- This is a recent post that shows when "All" or "Recent" is
- selected.
-
-
-
-
General Content
-
- This content only shows when "All" is selected.
-
+
+ handleSwitchChange("switch1")}
+ label="Enable notifications"
+ />
+ handleSwitchChange("switch2")}
+ label="Auto-save documents"
+ />
+ handleSwitchChange("switch3")}
+ label="Dark mode"
+ />
+ handleSwitchChange("switch4")}
+ label="Email updates"
+ />
diff --git a/stories/Switch.stories.js b/stories/Switch.stories.js
new file mode 100644
index 0000000..8dd184c
--- /dev/null
+++ b/stories/Switch.stories.js
@@ -0,0 +1,128 @@
+import React from "react";
+import Switch from "../app/components/Switch";
+
+export default {
+ title: "Forms/Switch",
+ component: Switch,
+ parameters: {
+ layout: "centered",
+ },
+ argTypes: {
+ checked: {
+ control: "boolean",
+ description: "Whether the switch is checked (on) or not (off)",
+ },
+ state: {
+ control: "select",
+ options: ["default", "focus"],
+ description: "Visual state of the switch",
+ },
+ label: {
+ control: "text",
+ description: "Label text displayed next to the switch",
+ },
+ onChange: {
+ action: "changed",
+ description: "Callback fired when the switch is toggled",
+ },
+ onFocus: {
+ action: "focused",
+ description: "Callback fired when the switch receives focus",
+ },
+ onBlur: {
+ action: "blurred",
+ description: "Callback fired when the switch loses focus",
+ },
+ },
+};
+
+const Template = (args) =>
;
+
+export const Default = Template.bind({});
+Default.args = {
+ checked: false,
+ label: "Switch label",
+};
+
+export const Checked = Template.bind({});
+Checked.args = {
+ checked: true,
+ label: "Switch label",
+};
+
+export const Focus = Template.bind({});
+Focus.args = {
+ checked: false,
+ state: "focus",
+ label: "Switch label",
+};
+
+export const FocusChecked = Template.bind({});
+FocusChecked.args = {
+ checked: true,
+ state: "focus",
+ label: "Switch label",
+};
+
+export const States = () => (
+
+
+
Switch States
+
+
+
+
+
+
+
+
+);
+
+export const Interactive = () => {
+ const [checked, setChecked] = React.useState(false);
+ const [state, setState] = React.useState("default");
+
+ return (
+
+
+
Interactive Switch
+ setChecked(!checked)}
+ label="Enable notifications"
+ />
+
+
+
Controls
+
+
+
+
+
+
+
+
+ );
+};
+
+export const WithText = () => (
+
+
+
Switch with Different Labels
+
+
+
+
+
+
+
+
+);
diff --git a/tests/accessibility/Switch.a11y.test.jsx b/tests/accessibility/Switch.a11y.test.jsx
new file mode 100644
index 0000000..ad73576
--- /dev/null
+++ b/tests/accessibility/Switch.a11y.test.jsx
@@ -0,0 +1,98 @@
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { describe, it, expect, vi } from "vitest";
+import { axe, toHaveNoViolations } from "jest-axe";
+import Switch from "../../app/components/Switch";
+
+expect.extend(toHaveNoViolations);
+
+describe("Switch Accessibility", () => {
+ it("has proper ARIA attributes", () => {
+ render(
);
+ const switchButton = screen.getByRole("switch");
+
+ expect(switchButton).toHaveAttribute("role", "switch");
+ expect(switchButton).toHaveAttribute("aria-checked", "false");
+ expect(switchButton).toHaveAttribute("aria-label", "Test Switch");
+ });
+
+ it("has proper ARIA attributes when checked", () => {
+ render(
);
+ const switchButton = screen.getByRole("switch");
+
+ expect(switchButton).toHaveAttribute("aria-checked", "true");
+ });
+
+ it("has proper ARIA attributes when focused", () => {
+ render(
);
+ const switchButton = screen.getByRole("switch");
+
+ expect(switchButton).toHaveAttribute("aria-checked", "false");
+ expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
+ expect(switchButton).toHaveClass("rounded-full");
+ expect(switchButton).toHaveAttribute("aria-label", "Test Switch");
+ });
+
+ it("handles keyboard navigation", () => {
+ const handleChange = vi.fn();
+ render(
);
+ const switchButton = screen.getByRole("switch");
+
+ // Test Enter key
+ fireEvent.keyDown(switchButton, { key: "Enter" });
+ expect(handleChange).toHaveBeenCalledTimes(1);
+
+ // Test Space key
+ fireEvent.keyDown(switchButton, { key: " " });
+ expect(handleChange).toHaveBeenCalledTimes(2);
+ });
+
+ it("handles focus state accessibility", () => {
+ const handleFocus = vi.fn();
+ render(
);
+ const switchButton = screen.getByRole("switch");
+
+ fireEvent.focus(switchButton);
+ expect(handleFocus).toHaveBeenCalledTimes(1);
+ });
+
+ it("handles checked state accessibility", () => {
+ const { rerender } = render(
);
+ let switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveAttribute("aria-checked", "false");
+
+ rerender(
);
+ switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveAttribute("aria-checked", "true");
+ });
+
+ it("has no accessibility violations", async () => {
+ const { container } = render(
);
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has no accessibility violations when checked", async () => {
+ const { container } = render(
);
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has no accessibility violations when focused", async () => {
+ const { container } = render(
);
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has no accessibility violations with text", async () => {
+ const { container } = render(
);
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("has no accessibility violations without text", async () => {
+ const { container } = render(
);
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/tests/integration/Switch.integration.test.jsx b/tests/integration/Switch.integration.test.jsx
new file mode 100644
index 0000000..d538e6b
--- /dev/null
+++ b/tests/integration/Switch.integration.test.jsx
@@ -0,0 +1,265 @@
+import React from "react";
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import { describe, it, expect, vi } from "vitest";
+import userEvent from "@testing-library/user-event";
+import Switch from "../../app/components/Switch";
+
+// Test form component
+const TestForm = ({ onSubmit }) => {
+ const [switch1, setSwitch1] = React.useState(false);
+ const [switch2, setSwitch2] = React.useState(true);
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ onSubmit({ switch1, switch2 });
+ };
+
+ return (
+
+ );
+};
+
+// Dynamic switch component
+const DynamicSwitch = ({ initialState = false }) => {
+ const [checked, setChecked] = React.useState(initialState);
+
+ // Update state when initialState prop changes
+ React.useEffect(() => {
+ setChecked(initialState);
+ }, [initialState]);
+
+ return (
+
+ setChecked(!checked)}
+ label="Dynamic Switch"
+ />
+
+ );
+};
+
+describe("Switch Integration", () => {
+ it("handles form submission", async () => {
+ const user = userEvent.setup();
+ const handleSubmit = vi.fn();
+
+ render(
);
+
+ const submitButton = screen.getByRole("button", { name: "Submit" });
+ await user.click(submitButton);
+
+ expect(handleSubmit).toHaveBeenCalledWith({
+ switch1: false,
+ switch2: true,
+ });
+ });
+
+ it("handles keyboard navigation between switches", async () => {
+ const user = userEvent.setup();
+ render(
+
+
+
+
+
+ );
+
+ const switches = screen.getAllByRole("switch");
+ expect(switches).toHaveLength(3);
+
+ // Focus first switch
+ await user.tab();
+ expect(switches[0]).toHaveFocus();
+
+ // Tab to second switch
+ await user.tab();
+ expect(switches[1]).toHaveFocus();
+
+ // Tab to third switch
+ await user.tab();
+ expect(switches[2]).toHaveFocus();
+ });
+
+ it("handles dynamic prop changes", () => {
+ const { rerender } = render(
);
+
+ let switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveAttribute("aria-checked", "false");
+
+ // Change initial state - the DynamicSwitch component should handle this internally
+ rerender(
);
+ switchButton = screen.getByRole("switch");
+ // The DynamicSwitch component manages its own state, so it should be checked
+ expect(switchButton).toHaveAttribute("aria-checked", "true");
+ });
+
+ it("handles multiple switches in form", async () => {
+ const user = userEvent.setup();
+ const handleSubmit = vi.fn();
+
+ const TestForm = () => {
+ const [switch1, setSwitch1] = React.useState(false);
+ const [switch2, setSwitch2] = React.useState(false);
+ const [switch3, setSwitch3] = React.useState(false);
+
+ return (
+
+ );
+ };
+
+ render(
);
+
+ const switches = screen.getAllByRole("switch");
+ expect(switches).toHaveLength(3);
+
+ // Toggle first switch
+ await user.click(switches[0]);
+ expect(switches[0]).toHaveAttribute("aria-checked", "true");
+
+ // Toggle second switch
+ await user.click(switches[1]);
+ expect(switches[1]).toHaveAttribute("aria-checked", "true");
+
+ // Submit form
+ const submitButton = screen.getByRole("button", { name: "Submit" });
+ await user.click(submitButton);
+ expect(handleSubmit).toHaveBeenCalled();
+ });
+
+ it("handles state changes", async () => {
+ const user = userEvent.setup();
+ const TestComponent = () => {
+ const [checked, setChecked] = React.useState(false);
+
+ return (
+
+ setChecked(!checked)}
+ label="Test Switch"
+ />
+
+ );
+ };
+
+ render(
);
+
+ const switchButton = screen.getByRole("switch");
+
+ // Initially unchecked
+ expect(switchButton).toHaveAttribute("aria-checked", "false");
+
+ // Toggle checked state
+ await user.click(switchButton);
+ expect(switchButton).toHaveAttribute("aria-checked", "true");
+ });
+
+ it("handles content changes", () => {
+ const { rerender } = render(
);
+ expect(screen.getByText("Original Label")).toBeInTheDocument();
+
+ rerender(
);
+ expect(screen.getByText("Updated Label")).toBeInTheDocument();
+ expect(screen.queryByText("Original Label")).not.toBeInTheDocument();
+ });
+
+ it("handles performance with many switches", () => {
+ const switches = Array.from({ length: 100 }, (_, i) => (
+
+ ));
+
+ const startTime = performance.now();
+ render(
{switches}
);
+ const endTime = performance.now();
+
+ // Should render within reasonable time (less than 1 second)
+ expect(endTime - startTime).toBeLessThan(1000);
+
+ const renderedSwitches = screen.getAllByRole("switch");
+ expect(renderedSwitches).toHaveLength(100);
+ });
+
+ it("handles rapid state changes", async () => {
+ const user = userEvent.setup();
+ const TestComponent = () => {
+ const [checked, setChecked] = React.useState(false);
+
+ return (
+
setChecked(!checked)}
+ label="Rapid Toggle Switch"
+ />
+ );
+ };
+
+ render();
+
+ const switchButton = screen.getByRole("switch");
+
+ // Rapidly toggle the switch
+ for (let i = 0; i < 10; i++) {
+ await user.click(switchButton);
+ await waitFor(() => {
+ expect(switchButton).toHaveAttribute(
+ "aria-checked",
+ i % 2 === 0 ? "true" : "false"
+ );
+ });
+ }
+ });
+
+ it("handles mixed content types", () => {
+ render(
+
+
+
+
+
+
+ );
+
+ const switches = screen.getAllByRole("switch");
+ expect(switches).toHaveLength(4);
+
+ // Check that labels are rendered correctly
+ expect(screen.getByText("Text Switch")).toBeInTheDocument();
+ expect(screen.getByText("Another Text Switch")).toBeInTheDocument();
+ expect(screen.getByText("Final Switch")).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/Switch.test.jsx b/tests/unit/Switch.test.jsx
new file mode 100644
index 0000000..3fbd348
--- /dev/null
+++ b/tests/unit/Switch.test.jsx
@@ -0,0 +1,184 @@
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { describe, it, expect, vi } from "vitest";
+import Switch from "../../app/components/Switch";
+
+describe("Switch Component", () => {
+ it("renders with default props", () => {
+ render();
+ const switchButton = screen.getByRole("switch");
+ expect(switchButton).toBeInTheDocument();
+ expect(switchButton).toHaveAttribute("aria-checked", "false");
+ });
+
+ it("renders with custom props", () => {
+ const handleChange = vi.fn();
+ render(
+
+ );
+
+ const switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveAttribute("aria-checked", "true");
+ expect(screen.getByText("Test Switch")).toBeInTheDocument();
+ });
+
+ it("handles checked prop correctly", () => {
+ const { rerender } = render();
+ let switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveAttribute("aria-checked", "false");
+
+ rerender();
+ switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveAttribute("aria-checked", "true");
+ });
+
+ it("handles state prop correctly", () => {
+ const { rerender } = render();
+ let switchButton = screen.getByRole("switch");
+ expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
+
+ rerender();
+ switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
+ });
+
+ it("calls onChange when clicked", () => {
+ const handleChange = vi.fn();
+ render();
+
+ const switchButton = screen.getByRole("switch");
+ fireEvent.click(switchButton);
+ expect(handleChange).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls onFocus when focused", () => {
+ const handleFocus = vi.fn();
+ render();
+
+ const switchButton = screen.getByRole("switch");
+ fireEvent.focus(switchButton);
+ expect(handleFocus).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls onBlur when blurred", () => {
+ const handleBlur = vi.fn();
+ render();
+
+ const switchButton = screen.getByRole("switch");
+ fireEvent.blur(switchButton);
+ expect(handleBlur).toHaveBeenCalledTimes(1);
+ });
+
+ it("handles keyboard events correctly", () => {
+ const handleChange = vi.fn();
+ render();
+
+ const switchButton = screen.getByRole("switch");
+
+ // Test Enter key
+ fireEvent.keyDown(switchButton, { key: "Enter" });
+ expect(handleChange).toHaveBeenCalledTimes(1);
+
+ // Test Space key
+ fireEvent.keyDown(switchButton, { key: " " });
+ expect(handleChange).toHaveBeenCalledTimes(2);
+
+ // Test other key (should not trigger)
+ fireEvent.keyDown(switchButton, { key: "Tab" });
+ expect(handleChange).toHaveBeenCalledTimes(2);
+ });
+
+ it("applies correct classes for different states", () => {
+ const { rerender } = render();
+ let switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveClass("cursor-pointer");
+
+ rerender();
+ switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveClass("cursor-pointer");
+ });
+
+ it("applies correct track styles based on checked state", () => {
+ const { rerender } = render();
+ let switchButton = screen.getByRole("switch");
+ let track = switchButton.querySelector("div");
+ expect(track).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
+
+ rerender();
+ switchButton = screen.getByRole("switch");
+ track = switchButton.querySelector("div");
+ expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
+
+ switchButton = screen.getByRole("switch");
+ track = switchButton.querySelector("div");
+ expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
+ });
+
+ it("applies correct focus styles", () => {
+ const { rerender } = render();
+ let switchButton = screen.getByRole("switch");
+ expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
+
+ rerender();
+ switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
+ });
+
+ it("applies correct base classes", () => {
+ render();
+ const switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveClass(
+ "relative",
+ "inline-flex",
+ "items-center",
+ "cursor-pointer",
+ "transition-all",
+ "duration-200",
+ "focus:outline-none",
+ "focus-visible:shadow-[0_0_5px_3px_#3281F8]"
+ );
+ });
+
+ it("forwards ref correctly", () => {
+ const ref = React.createRef();
+ render();
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement);
+ });
+
+ it("applies custom className", () => {
+ render();
+ const switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveClass("custom-class");
+ });
+
+ it("renders label when provided", () => {
+ render();
+ expect(screen.getByText("Test Label")).toBeInTheDocument();
+ });
+
+ it("does not render label when not provided", () => {
+ render();
+ expect(screen.queryByText("Switch label")).not.toBeInTheDocument();
+ // Should have aria-label for accessibility
+ const switchButton = screen.getByRole("switch");
+ expect(switchButton).toHaveAttribute("aria-label", "Toggle switch");
+ });
+
+ it("applies correct label styles", () => {
+ render();
+ const label = screen.getByText("Test Label");
+ expect(label).toHaveClass(
+ "ml-[var(--measures-spacing-008)]",
+ "font-inter",
+ "font-normal",
+ "text-[14px]",
+ "leading-[20px]",
+ "text-[var(--color-content-default-primary)]"
+ );
+ });
+});