Toggle Group component with storybook and testing
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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 ToggleGroup from "../../app/components/ToggleGroup";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("ToggleGroup Accessibility", () => {
|
||||
it("has proper ARIA attributes", () => {
|
||||
render(<ToggleGroup>Toggle Item</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveAttribute("type", "button");
|
||||
expect(toggleGroup).toHaveAttribute("role", "button");
|
||||
});
|
||||
|
||||
it("has proper ARIA attributes when focused", () => {
|
||||
render(<ToggleGroup state="focus">Focused Toggle</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveAttribute("type", "button");
|
||||
expect(toggleGroup).toHaveAttribute("role", "button");
|
||||
});
|
||||
|
||||
it("handles keyboard navigation", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<ToggleGroup onChange={handleChange}>Keyboard Toggle</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
|
||||
// Test Enter key
|
||||
fireEvent.keyDown(toggleGroup, { key: "Enter" });
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Test Space key
|
||||
fireEvent.keyDown(toggleGroup, { key: " " });
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("handles focus state accessibility", () => {
|
||||
render(<ToggleGroup state="focus">Focus Toggle</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||
});
|
||||
|
||||
it("handles selected state accessibility", () => {
|
||||
render(<ToggleGroup state="selected">Selected Toggle</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]"
|
||||
);
|
||||
});
|
||||
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(<ToggleGroup>Accessible Toggle</ToggleGroup>);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations when focused", async () => {
|
||||
const { container } = render(
|
||||
<ToggleGroup state="focus">Focused Toggle</ToggleGroup>
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations when selected", async () => {
|
||||
const { container } = render(
|
||||
<ToggleGroup state="selected">Selected Toggle</ToggleGroup>
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations with text", async () => {
|
||||
const { container } = render(
|
||||
<ToggleGroup showText={true}>Text Toggle</ToggleGroup>
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has no accessibility violations without text", async () => {
|
||||
const { container } = render(
|
||||
<ToggleGroup showText={false} ariaLabel="Icon Toggle">
|
||||
Icon Toggle
|
||||
</ToggleGroup>
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
import React, { useState } from "react";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import ToggleGroup from "../../app/components/ToggleGroup";
|
||||
|
||||
// Test component for form integration
|
||||
const TestForm = () => {
|
||||
const [selectedToggle, setSelectedToggle] = useState("left");
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="flex">
|
||||
<ToggleGroup
|
||||
position="left"
|
||||
state={selectedToggle === "left" ? "selected" : "default"}
|
||||
onChange={() => setSelectedToggle("left")}
|
||||
>
|
||||
Left Option
|
||||
</ToggleGroup>
|
||||
<ToggleGroup
|
||||
position="middle"
|
||||
state={selectedToggle === "middle" ? "selected" : "default"}
|
||||
onChange={() => setSelectedToggle("middle")}
|
||||
>
|
||||
Middle Option
|
||||
</ToggleGroup>
|
||||
<ToggleGroup
|
||||
position="right"
|
||||
state={selectedToggle === "right" ? "selected" : "default"}
|
||||
onChange={() => setSelectedToggle("right")}
|
||||
>
|
||||
Right Option
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// Dynamic component for prop changes
|
||||
const DynamicToggleGroup = ({ position, state, showText }) => {
|
||||
return (
|
||||
<ToggleGroup position={position} state={state} showText={showText}>
|
||||
Dynamic Content
|
||||
</ToggleGroup>
|
||||
);
|
||||
};
|
||||
|
||||
describe("ToggleGroup Integration", () => {
|
||||
it("handles form submission", async () => {
|
||||
const handleSubmit = vi.fn();
|
||||
render(
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex">
|
||||
<ToggleGroup position="left" onChange={() => {}}>
|
||||
First Option
|
||||
</ToggleGroup>
|
||||
<ToggleGroup position="middle" onChange={() => {}}>
|
||||
Second Option
|
||||
</ToggleGroup>
|
||||
<ToggleGroup position="right" onChange={() => {}}>
|
||||
Third Option
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
|
||||
const submitButton = screen.getByRole("button", { name: "Submit" });
|
||||
fireEvent.click(submitButton);
|
||||
expect(handleSubmit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handles keyboard navigation between toggle groups", () => {
|
||||
render(<TestForm />);
|
||||
const toggleGroups = screen.getAllByRole("button");
|
||||
|
||||
// Focus first toggle group
|
||||
toggleGroups[0].focus();
|
||||
expect(toggleGroups[0]).toHaveFocus();
|
||||
|
||||
// Test keyboard navigation
|
||||
fireEvent.keyDown(toggleGroups[0], { key: "Tab" });
|
||||
// Note: Tab navigation behavior depends on browser implementation
|
||||
});
|
||||
|
||||
it("handles dynamic prop changes", () => {
|
||||
const { rerender } = render(
|
||||
<DynamicToggleGroup position="left" state="default" showText={true} />
|
||||
);
|
||||
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"rounded-l-[var(--measures-radius-medium)]",
|
||||
"rounded-r-none"
|
||||
);
|
||||
expect(toggleGroup).toHaveTextContent("Dynamic Content");
|
||||
|
||||
rerender(
|
||||
<DynamicToggleGroup position="middle" state="selected" showText={false} />
|
||||
);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("rounded-none");
|
||||
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
|
||||
expect(toggleGroup).toHaveTextContent("Dynamic Content");
|
||||
});
|
||||
|
||||
it("handles multiple toggle groups in form", () => {
|
||||
render(<TestForm />);
|
||||
const toggleGroups = screen.getAllByRole("button");
|
||||
expect(toggleGroups).toHaveLength(3);
|
||||
|
||||
// Test clicking different toggle groups
|
||||
fireEvent.click(toggleGroups[0]);
|
||||
fireEvent.click(toggleGroups[1]);
|
||||
fireEvent.click(toggleGroups[2]);
|
||||
});
|
||||
|
||||
it("handles state changes", async () => {
|
||||
const { rerender } = render(<TestForm />);
|
||||
const toggleGroups = screen.getAllByRole("button");
|
||||
|
||||
// Initially, left should be selected
|
||||
expect(toggleGroups[0]).toHaveClass("bg-[var(--color-magenta-magenta100)]");
|
||||
|
||||
// Click middle toggle
|
||||
fireEvent.click(toggleGroups[1]);
|
||||
await waitFor(() => {
|
||||
expect(toggleGroups[1]).toHaveClass(
|
||||
"bg-[var(--color-magenta-magenta100)]"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("handles content changes", () => {
|
||||
const { rerender } = render(
|
||||
<ToggleGroup showText={true}>Initial Content</ToggleGroup>
|
||||
);
|
||||
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveTextContent("Initial Content");
|
||||
|
||||
rerender(<ToggleGroup showText={true}>Updated Content</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveTextContent("Updated Content");
|
||||
|
||||
rerender(<ToggleGroup showText={false}>Hidden Content</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveTextContent("Hidden Content");
|
||||
});
|
||||
|
||||
it("handles performance with many toggle groups", () => {
|
||||
const ManyToggleGroups = () => {
|
||||
const [selected, setSelected] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
{Array.from({ length: 10 }, (_, i) => (
|
||||
<ToggleGroup
|
||||
key={i}
|
||||
position={i === 0 ? "left" : i === 9 ? "right" : "middle"}
|
||||
state={selected === i ? "selected" : "default"}
|
||||
onChange={() => setSelected(i)}
|
||||
>
|
||||
Option {i + 1}
|
||||
</ToggleGroup>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render(<ManyToggleGroups />);
|
||||
const toggleGroups = screen.getAllByRole("button");
|
||||
expect(toggleGroups).toHaveLength(10);
|
||||
|
||||
// Test clicking different toggle groups
|
||||
fireEvent.click(toggleGroups[5]);
|
||||
expect(toggleGroups[5]).toHaveClass("bg-[var(--color-magenta-magenta100)]");
|
||||
});
|
||||
|
||||
it("handles rapid state changes", async () => {
|
||||
const { rerender } = render(<TestForm />);
|
||||
const toggleGroups = screen.getAllByRole("button");
|
||||
|
||||
// Rapidly change states
|
||||
for (let i = 0; i < 5; i++) {
|
||||
fireEvent.click(toggleGroups[i % 3]);
|
||||
await waitFor(() => {
|
||||
expect(toggleGroups[i % 3]).toHaveClass(
|
||||
"bg-[var(--color-magenta-magenta100)]"
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("handles mixed content types", () => {
|
||||
render(
|
||||
<div className="flex">
|
||||
<ToggleGroup position="left" showText={true}>
|
||||
Text Only
|
||||
</ToggleGroup>
|
||||
<ToggleGroup position="middle" showText={false}>
|
||||
Icon Only
|
||||
</ToggleGroup>
|
||||
<ToggleGroup position="right" showText={true}>
|
||||
Text Only
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
);
|
||||
|
||||
const toggleGroups = screen.getAllByRole("button");
|
||||
expect(toggleGroups[0]).toHaveTextContent("Text Only");
|
||||
expect(toggleGroups[1]).toHaveTextContent("Icon Only");
|
||||
expect(toggleGroups[2]).toHaveTextContent("Text Only");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import ToggleGroup from "../../app/components/ToggleGroup";
|
||||
|
||||
describe("ToggleGroup Component", () => {
|
||||
it("renders with default props", () => {
|
||||
render(<ToggleGroup>Test Content</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toBeInTheDocument();
|
||||
expect(toggleGroup).toHaveTextContent("Test Content");
|
||||
});
|
||||
|
||||
it("renders with custom props", () => {
|
||||
render(
|
||||
<ToggleGroup position="middle" state="selected" showText={true}>
|
||||
Custom Content
|
||||
</ToggleGroup>
|
||||
);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toBeInTheDocument();
|
||||
expect(toggleGroup).toHaveTextContent("Custom Content");
|
||||
});
|
||||
|
||||
it("handles position prop correctly", () => {
|
||||
const { rerender } = render(
|
||||
<ToggleGroup position="left">Left</ToggleGroup>
|
||||
);
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"rounded-l-[var(--measures-radius-medium)]",
|
||||
"rounded-r-none"
|
||||
);
|
||||
|
||||
rerender(<ToggleGroup position="middle">Middle</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("rounded-none");
|
||||
|
||||
rerender(<ToggleGroup position="right">Right</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"rounded-r-[var(--measures-radius-medium)]",
|
||||
"rounded-l-none"
|
||||
);
|
||||
});
|
||||
|
||||
it("handles state prop correctly", () => {
|
||||
const { rerender } = render(
|
||||
<ToggleGroup state="default">Default</ToggleGroup>
|
||||
);
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"bg-[var(--color-surface-default-primary)]"
|
||||
);
|
||||
|
||||
rerender(<ToggleGroup state="hover">Hover</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
|
||||
|
||||
rerender(<ToggleGroup state="focus">Focus</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"bg-[var(--color-surface-default-primary)]",
|
||||
"shadow-[0_0_5px_1px_#3281F8]"
|
||||
);
|
||||
|
||||
rerender(<ToggleGroup state="selected">Selected</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"bg-[var(--color-magenta-magenta100)]",
|
||||
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]"
|
||||
);
|
||||
});
|
||||
|
||||
it("handles showText prop correctly", () => {
|
||||
const { rerender } = render(
|
||||
<ToggleGroup showText={true}>Visible Text</ToggleGroup>
|
||||
);
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveTextContent("Visible Text");
|
||||
|
||||
rerender(<ToggleGroup showText={false}>☰</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveTextContent("☰");
|
||||
});
|
||||
|
||||
it("calls onChange when clicked", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<ToggleGroup onChange={handleChange}>Clickable</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
|
||||
fireEvent.click(toggleGroup);
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onFocus when focused", () => {
|
||||
const handleFocus = vi.fn();
|
||||
render(<ToggleGroup onFocus={handleFocus}>Focusable</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
|
||||
fireEvent.focus(toggleGroup);
|
||||
expect(handleFocus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onBlur when blurred", () => {
|
||||
const handleBlur = vi.fn();
|
||||
render(<ToggleGroup onBlur={handleBlur}>Blurable</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
|
||||
fireEvent.blur(toggleGroup);
|
||||
expect(handleBlur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handles keyboard events correctly", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<ToggleGroup onChange={handleChange}>Keyboard</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
|
||||
// Test Enter key
|
||||
fireEvent.keyDown(toggleGroup, { key: "Enter" });
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Test Space key
|
||||
fireEvent.keyDown(toggleGroup, { key: " " });
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Test other key (should not trigger)
|
||||
fireEvent.keyDown(toggleGroup, { key: "Escape" });
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("applies correct classes for different states", () => {
|
||||
const { rerender } = render(
|
||||
<ToggleGroup state="default">Default</ToggleGroup>
|
||||
);
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"bg-[var(--color-surface-default-primary)]"
|
||||
);
|
||||
|
||||
rerender(<ToggleGroup state="hover">Hover</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
|
||||
|
||||
rerender(<ToggleGroup state="focus">Focus</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||
|
||||
rerender(<ToggleGroup state="selected">Selected</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"bg-[var(--color-magenta-magenta100)]",
|
||||
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]"
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct position classes", () => {
|
||||
const { rerender } = render(
|
||||
<ToggleGroup position="left">Left</ToggleGroup>
|
||||
);
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"rounded-l-[var(--measures-radius-medium)]",
|
||||
"rounded-r-none"
|
||||
);
|
||||
|
||||
rerender(<ToggleGroup position="middle">Middle</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("rounded-none");
|
||||
|
||||
rerender(<ToggleGroup position="right">Right</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"rounded-r-[var(--measures-radius-medium)]",
|
||||
"rounded-l-none"
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct base classes", () => {
|
||||
render(<ToggleGroup>Base</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"py-[var(--measures-spacing-008)]",
|
||||
"px-[var(--measures-spacing-008)]",
|
||||
"gap-[var(--measures-spacing-008)]",
|
||||
"font-inter",
|
||||
"font-medium",
|
||||
"text-[12px]",
|
||||
"leading-[12px]",
|
||||
"cursor-pointer",
|
||||
"transition-all",
|
||||
"duration-200",
|
||||
"focus:outline-none",
|
||||
"focus-visible:shadow-[0_0_5px_1px_#3281F8]",
|
||||
"hover:bg-[var(--color-magenta-magenta100)]",
|
||||
"flex",
|
||||
"items-center",
|
||||
"justify-center"
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards ref correctly", () => {
|
||||
const ref = React.createRef();
|
||||
render(<ToggleGroup ref={ref}>Ref Test</ToggleGroup>);
|
||||
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(<ToggleGroup className="custom-class">Custom</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("custom-class");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user