Toggle component with storybook and testing
This commit is contained in:
@@ -0,0 +1,194 @@
|
|||||||
|
import React, { memo, useCallback, useId, forwardRef } from "react";
|
||||||
|
|
||||||
|
const Toggle = forwardRef(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
checked = false,
|
||||||
|
onChange,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
disabled = false,
|
||||||
|
state = "default",
|
||||||
|
showIcon = false,
|
||||||
|
showText = false,
|
||||||
|
icon = "I",
|
||||||
|
text = "Toggle",
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const toggleId = useId();
|
||||||
|
const labelId = useId();
|
||||||
|
|
||||||
|
// Size styles - single size with specific dimensions
|
||||||
|
const sizeStyles = {
|
||||||
|
toggle: "h-[var(--measures-sizing-032)] px-[16px] py-[8px] gap-[4px]",
|
||||||
|
label: "text-[12px] leading-[16px]",
|
||||||
|
};
|
||||||
|
|
||||||
|
// State styles
|
||||||
|
const getStateStyles = () => {
|
||||||
|
if (disabled) {
|
||||||
|
return {
|
||||||
|
toggle:
|
||||||
|
"bg-[var(--color-surface-default-tertiary)] text-[var(--color-content-default-tertiary)] cursor-not-allowed",
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
switch (state) {
|
||||||
|
case "hover":
|
||||||
|
return {
|
||||||
|
toggle:
|
||||||
|
"bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)]",
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
};
|
||||||
|
case "focus":
|
||||||
|
return {
|
||||||
|
toggle:
|
||||||
|
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] shadow-[0_0_5px_1px_#3281F8]",
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
toggle:
|
||||||
|
"bg-[var(--color-magenta-magenta100)] text-[var(--color-content-default-primary)] shadow-[0_0_0_1px_var(--color-border-default-brand-primary)]",
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (state) {
|
||||||
|
case "hover":
|
||||||
|
return {
|
||||||
|
toggle:
|
||||||
|
"bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)]",
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
};
|
||||||
|
case "focus":
|
||||||
|
return {
|
||||||
|
toggle:
|
||||||
|
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] shadow-[0_0_5px_1px_#3281F8]",
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
toggle:
|
||||||
|
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)]",
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateStyles = getStateStyles();
|
||||||
|
const currentSize = sizeStyles;
|
||||||
|
|
||||||
|
// Container classes
|
||||||
|
const containerClasses = "flex flex-col gap-[4px]";
|
||||||
|
|
||||||
|
const labelClasses = `${currentSize.label} font-inter font-medium`;
|
||||||
|
|
||||||
|
const toggleClasses = `
|
||||||
|
${currentSize.toggle}
|
||||||
|
${stateStyles.toggle}
|
||||||
|
rounded-full
|
||||||
|
font-inter
|
||||||
|
font-normal
|
||||||
|
text-[12px]
|
||||||
|
leading-[16px]
|
||||||
|
cursor-pointer
|
||||||
|
transition-all
|
||||||
|
duration-200
|
||||||
|
focus:outline-none
|
||||||
|
focus-visible:shadow-[0_0_5px_1px_#3281F8]
|
||||||
|
${!checked ? "hover:!bg-[var(--color-surface-default-secondary)]" : ""}
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
gap-[4px]
|
||||||
|
${className}
|
||||||
|
`
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, " ");
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (!disabled && onChange) {
|
||||||
|
onChange(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disabled, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (!disabled && onFocus) {
|
||||||
|
onFocus(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disabled, onFocus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (!disabled && onBlur) {
|
||||||
|
onBlur(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disabled, onBlur]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (!disabled && (e.key === "Enter" || e.key === " ")) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (onChange) {
|
||||||
|
onChange(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disabled, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={containerClasses}>
|
||||||
|
{label && (
|
||||||
|
<label
|
||||||
|
id={labelId}
|
||||||
|
htmlFor={toggleId}
|
||||||
|
className={`${labelClasses} text-[var(--color-content-default-secondary)]`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div className={disabled ? "opacity-40" : ""}>
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
id={toggleId}
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
aria-labelledby={label ? labelId : undefined}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
className={toggleClasses}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{showIcon && <span className="italic">{icon}</span>}
|
||||||
|
{showText && <span>{text}</span>}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Toggle.displayName = "Toggle";
|
||||||
|
|
||||||
|
export default memo(Toggle);
|
||||||
+78
-93
@@ -1,111 +1,96 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import TextArea from "../components/TextArea";
|
import Toggle from "../components/Toggle";
|
||||||
|
|
||||||
export default function FormsPlayground() {
|
export default function FormsPlayground() {
|
||||||
const [smallValue, setSmallValue] = useState("");
|
const [toggleStates, setToggleStates] = useState({
|
||||||
const [mediumValue, setMediumValue] = useState("");
|
default: false,
|
||||||
const [largeValue, setLargeValue] = useState("");
|
hover: false,
|
||||||
const [defaultLabelValue, setDefaultLabelValue] = useState("");
|
selected: true,
|
||||||
const [horizontalLabelValue, setHorizontalLabelValue] = useState("");
|
focus: false,
|
||||||
const [smallHorizontalValue, setSmallHorizontalValue] = useState("");
|
disabled: false,
|
||||||
const [smallDefaultValue, setSmallDefaultValue] = useState("");
|
icon: false,
|
||||||
const [errorStateValue, setErrorStateValue] = useState("");
|
text: false,
|
||||||
const [disabledStateValue, setDisabledStateValue] = useState("");
|
both: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggleChange = (key) => (e) => {
|
||||||
|
setToggleStates((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: !prev[key],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-[24px] space-y-[24px]">
|
<div className="p-[24px] space-y-[24px]">
|
||||||
<h1 className="font-bricolage text-[24px]">Forms Playground</h1>
|
<h1 className="font-bricolage text-[24px]">Forms Playground</h1>
|
||||||
|
|
||||||
<section className="space-y-[12px]">
|
<section className="space-y-[12px]">
|
||||||
<h2 className="font-space text-[18px]">TextArea Examples</h2>
|
<h2 className="font-space text-[18px]">Toggle Examples</h2>
|
||||||
<div className="max-w-[520px] space-y-[16px]">
|
<div
|
||||||
<div>
|
className="max-w-[520px] space-y-[16px] bg-white p-6 rounded-lg border border-gray-200 shadow-lg"
|
||||||
<h3 className="font-space text-[14px] mb-[8px]">Sizes</h3>
|
//style={{ backgroundColor: "white" }}
|
||||||
<div className="space-y-[12px]">
|
>
|
||||||
<TextArea
|
|
||||||
label="Small"
|
|
||||||
size="small"
|
|
||||||
value={smallValue}
|
|
||||||
onChange={(e) => setSmallValue(e.target.value)}
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
|
||||||
<TextArea
|
|
||||||
label="Medium"
|
|
||||||
size="medium"
|
|
||||||
value={mediumValue}
|
|
||||||
onChange={(e) => setMediumValue(e.target.value)}
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
|
||||||
<TextArea
|
|
||||||
label="Large"
|
|
||||||
size="large"
|
|
||||||
value={largeValue}
|
|
||||||
onChange={(e) => setLargeValue(e.target.value)}
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-space text-[14px] mb-[8px]">Label Variants</h3>
|
|
||||||
<div className="space-y-[12px]">
|
|
||||||
<TextArea
|
|
||||||
label="Default (Top Label)"
|
|
||||||
labelVariant="default"
|
|
||||||
size="medium"
|
|
||||||
value={defaultLabelValue}
|
|
||||||
onChange={(e) => setDefaultLabelValue(e.target.value)}
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
|
||||||
<TextArea
|
|
||||||
label="Small Default"
|
|
||||||
labelVariant="default"
|
|
||||||
size="small"
|
|
||||||
value={smallDefaultValue}
|
|
||||||
onChange={(e) => setSmallDefaultValue(e.target.value)}
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
|
||||||
<TextArea
|
|
||||||
label="Horizontal (Left Label)"
|
|
||||||
labelVariant="horizontal"
|
|
||||||
size="medium"
|
|
||||||
value={horizontalLabelValue}
|
|
||||||
onChange={(e) => setHorizontalLabelValue(e.target.value)}
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
|
||||||
<TextArea
|
|
||||||
label="Small Horizontal"
|
|
||||||
labelVariant="horizontal"
|
|
||||||
size="small"
|
|
||||||
value={smallHorizontalValue}
|
|
||||||
onChange={(e) => setSmallHorizontalValue(e.target.value)}
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-space text-[14px] mb-[8px]">States</h3>
|
<h3 className="font-space text-[14px] mb-[8px]">States</h3>
|
||||||
<div className="space-y-[12px]">
|
<div className="space-y-[12px]">
|
||||||
<TextArea
|
<Toggle
|
||||||
label="Error"
|
label="Default State"
|
||||||
size="medium"
|
checked={toggleStates.default}
|
||||||
state="default"
|
onChange={handleToggleChange("default")}
|
||||||
error={true}
|
|
||||||
value={errorStateValue}
|
|
||||||
onChange={(e) => setErrorStateValue(e.target.value)}
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<Toggle
|
||||||
label="Disabled"
|
label="Hover State"
|
||||||
size="medium"
|
checked={toggleStates.hover}
|
||||||
state="default"
|
onChange={handleToggleChange("hover")}
|
||||||
disabled={true}
|
state="hover"
|
||||||
value={disabledStateValue}
|
/>
|
||||||
onChange={(e) => setDisabledStateValue(e.target.value)}
|
<Toggle
|
||||||
placeholder="Enter text..."
|
label="Selected State"
|
||||||
|
checked={toggleStates.selected}
|
||||||
|
onChange={handleToggleChange("selected")}
|
||||||
|
/>
|
||||||
|
<Toggle
|
||||||
|
label="Focus State"
|
||||||
|
checked={toggleStates.focus}
|
||||||
|
onChange={handleToggleChange("focus")}
|
||||||
|
state="focus"
|
||||||
|
/>
|
||||||
|
<Toggle
|
||||||
|
label="Disabled State"
|
||||||
|
checked={toggleStates.disabled}
|
||||||
|
onChange={handleToggleChange("disabled")}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-space text-[14px] mb-[8px]">Content Types</h3>
|
||||||
|
<div className="space-y-[12px]">
|
||||||
|
<Toggle
|
||||||
|
label="Icon Only"
|
||||||
|
checked={toggleStates.icon}
|
||||||
|
onChange={handleToggleChange("icon")}
|
||||||
|
showIcon={true}
|
||||||
|
icon="I"
|
||||||
|
/>
|
||||||
|
<Toggle
|
||||||
|
label="Text Only"
|
||||||
|
checked={toggleStates.text}
|
||||||
|
onChange={handleToggleChange("text")}
|
||||||
|
showText={true}
|
||||||
|
text="Toggle"
|
||||||
|
/>
|
||||||
|
<Toggle
|
||||||
|
label="Icon and Text"
|
||||||
|
checked={toggleStates.both}
|
||||||
|
onChange={handleToggleChange("both")}
|
||||||
|
showIcon={true}
|
||||||
|
showText={true}
|
||||||
|
icon="I"
|
||||||
|
text="Toggle"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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) => <Toggle {...args} />;
|
||||||
|
|
||||||
|
export const States = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold">Toggle States</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Toggle label="Default State" checked={false} />
|
||||||
|
<Toggle label="Hover State" checked={false} state="hover" />
|
||||||
|
<Toggle label="Selected State" checked={true} />
|
||||||
|
<Toggle label="Focus State" checked={false} state="focus" />
|
||||||
|
<Toggle label="Disabled State" checked={false} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold">Interactive Toggle</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Toggle
|
||||||
|
label="Interactive Toggle"
|
||||||
|
checked={checked}
|
||||||
|
onChange={() => setChecked(!checked)}
|
||||||
|
state={state}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-md font-semibold">Controls</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="checked"
|
||||||
|
checked={checked}
|
||||||
|
onChange={(e) => setChecked(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="checked" className="text-sm">
|
||||||
|
Checked
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">State:</label>
|
||||||
|
<select
|
||||||
|
value={state}
|
||||||
|
onChange={(e) => setState(e.target.value)}
|
||||||
|
className="px-3 py-1 border border-gray-300 rounded"
|
||||||
|
>
|
||||||
|
<option value="default">Default</option>
|
||||||
|
<option value="focus">Focus</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="disabled"
|
||||||
|
checked={disabled}
|
||||||
|
onChange={(e) => setDisabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="disabled" className="text-sm">
|
||||||
|
Disabled
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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(<Toggle label="Test Toggle" />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveAttribute("aria-checked", "false");
|
||||||
|
expect(toggle).toHaveAttribute("type", "button");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has proper ARIA attributes when checked", () => {
|
||||||
|
render(<Toggle label="Test Toggle" checked={true} />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveAttribute("aria-checked", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has proper ARIA attributes when disabled", () => {
|
||||||
|
render(<Toggle label="Test Toggle" disabled={true} />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveAttribute("disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has proper label association", () => {
|
||||||
|
render(<Toggle label="Test Toggle" />);
|
||||||
|
|
||||||
|
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(<Toggle label="Test Toggle" onChange={handleChange} />);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<Toggle label="Test Toggle" disabled={true} onChange={handleChange} />
|
||||||
|
);
|
||||||
|
|
||||||
|
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(<Toggle label="Test Toggle" />);
|
||||||
|
|
||||||
|
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(<Toggle label="Test Toggle" />);
|
||||||
|
const results = await axe(container);
|
||||||
|
expect(results).toHaveNoViolations();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has no accessibility violations when checked", async () => {
|
||||||
|
const { container } = render(<Toggle label="Test Toggle" checked={true} />);
|
||||||
|
const results = await axe(container);
|
||||||
|
expect(results).toHaveNoViolations();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has no accessibility violations when disabled", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Toggle label="Test Toggle" disabled={true} />
|
||||||
|
);
|
||||||
|
const results = await axe(container);
|
||||||
|
expect(results).toHaveNoViolations();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has no accessibility violations with icon", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Toggle label="Test Toggle" showIcon={true} icon="I" />
|
||||||
|
);
|
||||||
|
const results = await axe(container);
|
||||||
|
expect(results).toHaveNoViolations();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has no accessibility violations with text", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Toggle label="Test Toggle" showText={true} text="Toggle" />
|
||||||
|
);
|
||||||
|
const results = await axe(container);
|
||||||
|
expect(results).toHaveNoViolations();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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(
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Toggle label="Test Toggle" name="toggle" />
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<div>
|
||||||
|
<Toggle label="First Toggle" />
|
||||||
|
<Toggle label="Second Toggle" />
|
||||||
|
<Toggle label="Third Toggle" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(<Toggle label="Test Toggle" checked={false} />);
|
||||||
|
|
||||||
|
let toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveAttribute("aria-checked", "false");
|
||||||
|
|
||||||
|
rerender(<Toggle label="Test Toggle" checked={true} />);
|
||||||
|
toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveAttribute("aria-checked", "true");
|
||||||
|
|
||||||
|
rerender(<Toggle label="Test Toggle" disabled={true} />);
|
||||||
|
toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveAttribute("disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles multiple toggles in form", () => {
|
||||||
|
const handleChange1 = vi.fn();
|
||||||
|
const handleChange2 = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<div>
|
||||||
|
<Toggle label="First Toggle" onChange={handleChange1} />
|
||||||
|
<Toggle label="Second Toggle" onChange={handleChange2} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(<Toggle label="Test Toggle" state="default" />);
|
||||||
|
|
||||||
|
let toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||||
|
|
||||||
|
rerender(<Toggle label="Test Toggle" state="focus" />);
|
||||||
|
toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles content changes", () => {
|
||||||
|
const { rerender } = render(<Toggle label="Test Toggle" />);
|
||||||
|
|
||||||
|
let toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).not.toHaveTextContent("I");
|
||||||
|
expect(toggle).not.toHaveTextContent("Toggle");
|
||||||
|
|
||||||
|
rerender(<Toggle label="Test Toggle" showIcon={true} icon="I" />);
|
||||||
|
toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveTextContent("I");
|
||||||
|
|
||||||
|
rerender(<Toggle label="Test Toggle" showText={true} text="Toggle" />);
|
||||||
|
toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveTextContent("Toggle");
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<Toggle
|
||||||
|
label="Test Toggle"
|
||||||
|
showIcon={true}
|
||||||
|
showText={true}
|
||||||
|
icon="I"
|
||||||
|
text="Toggle"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
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) => (
|
||||||
|
<Toggle key={i} label={`Toggle ${i}`} />
|
||||||
|
));
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
render(<div>{toggles}</div>);
|
||||||
|
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(<Toggle label="Test Toggle" onChange={handleChange} />);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<div>
|
||||||
|
<Toggle label="Icon Toggle" showIcon={true} icon="I" />
|
||||||
|
<Toggle label="Text Toggle" showText={true} text="Toggle" />
|
||||||
|
<Toggle
|
||||||
|
label="Both Toggle"
|
||||||
|
showIcon={true}
|
||||||
|
showText={true}
|
||||||
|
icon="I"
|
||||||
|
text="Toggle"
|
||||||
|
/>
|
||||||
|
<Toggle label="Empty Toggle" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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(<Toggle label="Test Toggle" />);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<Toggle
|
||||||
|
label="Custom Toggle"
|
||||||
|
checked={true}
|
||||||
|
disabled={true}
|
||||||
|
className="custom-class"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(<Toggle label="Test Toggle" checked={false} />);
|
||||||
|
|
||||||
|
let toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveAttribute("aria-checked", "false");
|
||||||
|
|
||||||
|
rerender(<Toggle label="Test Toggle" checked={true} />);
|
||||||
|
toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveAttribute("aria-checked", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles disabled state", () => {
|
||||||
|
render(<Toggle label="Test Toggle" disabled={true} />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveAttribute("disabled");
|
||||||
|
expect(toggle).toHaveClass("cursor-not-allowed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles state prop", () => {
|
||||||
|
const { rerender } = render(<Toggle label="Test Toggle" state="focus" />);
|
||||||
|
|
||||||
|
let toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||||
|
|
||||||
|
rerender(<Toggle label="Test Toggle" state="default" />);
|
||||||
|
toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles showIcon and icon props", () => {
|
||||||
|
render(<Toggle label="Test Toggle" showIcon={true} icon="I" />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveTextContent("I");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles showText and text props", () => {
|
||||||
|
render(<Toggle label="Test Toggle" showText={true} text="Toggle" />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveTextContent("Toggle");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles both icon and text", () => {
|
||||||
|
render(
|
||||||
|
<Toggle
|
||||||
|
label="Test Toggle"
|
||||||
|
showIcon={true}
|
||||||
|
showText={true}
|
||||||
|
icon="I"
|
||||||
|
text="Toggle"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveTextContent("I");
|
||||||
|
expect(toggle).toHaveTextContent("Toggle");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calls onChange when clicked", () => {
|
||||||
|
const handleChange = vi.fn();
|
||||||
|
render(<Toggle label="Test Toggle" onChange={handleChange} />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
|
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not call onChange when disabled", () => {
|
||||||
|
const handleChange = vi.fn();
|
||||||
|
render(
|
||||||
|
<Toggle label="Test Toggle" disabled={true} onChange={handleChange} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
|
expect(handleChange).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("applies correct classes for different states", () => {
|
||||||
|
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
|
||||||
|
|
||||||
|
let toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveClass("bg-[var(--color-surface-default-primary)]");
|
||||||
|
|
||||||
|
rerender(<Toggle label="Test Toggle" checked={true} />);
|
||||||
|
toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveClass("bg-[var(--color-magenta-magenta100)]");
|
||||||
|
|
||||||
|
rerender(<Toggle label="Test Toggle" disabled={true} />);
|
||||||
|
toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("applies hover classes when not checked", () => {
|
||||||
|
render(<Toggle label="Test Toggle" checked={false} />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveClass(
|
||||||
|
"hover:!bg-[var(--color-surface-default-secondary)]"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not apply hover classes when checked", () => {
|
||||||
|
render(<Toggle label="Test Toggle" checked={true} />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).not.toHaveClass(
|
||||||
|
"hover:!bg-[var(--color-surface-default-secondary)]"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("applies focus-visible classes", () => {
|
||||||
|
render(<Toggle label="Test Toggle" />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveClass("focus-visible:shadow-[0_0_5px_1px_#3281F8]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("applies correct size classes", () => {
|
||||||
|
render(<Toggle label="Test Toggle" />);
|
||||||
|
|
||||||
|
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(<Toggle label="Test Toggle" />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveClass("text-[12px]");
|
||||||
|
expect(toggle).toHaveClass("leading-[16px]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("applies correct label classes", () => {
|
||||||
|
render(<Toggle label="Test Toggle" />);
|
||||||
|
|
||||||
|
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(<Toggle label="Test Toggle" ref={ref} />);
|
||||||
|
|
||||||
|
expect(ref).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("applies custom className", () => {
|
||||||
|
render(<Toggle label="Test Toggle" className="custom-class" />);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch");
|
||||||
|
expect(toggle).toHaveClass("custom-class");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user