Toggle Group component with storybook and testing
This commit is contained in:
@@ -0,0 +1,137 @@
|
|||||||
|
import React, { memo, useCallback, useId, forwardRef } from "react";
|
||||||
|
|
||||||
|
const ToggleGroup = memo(
|
||||||
|
forwardRef((props, ref) => {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
position = "left",
|
||||||
|
state = "default",
|
||||||
|
showText = true,
|
||||||
|
ariaLabel,
|
||||||
|
onChange,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const groupId = useId();
|
||||||
|
|
||||||
|
// Position-based styling for border radius
|
||||||
|
const getPositionStyles = useCallback((pos) => {
|
||||||
|
switch (pos) {
|
||||||
|
case "left":
|
||||||
|
return "rounded-l-[var(--measures-radius-medium)] rounded-r-none";
|
||||||
|
case "middle":
|
||||||
|
return "rounded-none";
|
||||||
|
case "right":
|
||||||
|
return "rounded-r-[var(--measures-radius-medium)] rounded-l-none";
|
||||||
|
default:
|
||||||
|
return "rounded-[var(--measures-radius-medium)]";
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// State-based styling
|
||||||
|
const getStateStyles = useCallback((state) => {
|
||||||
|
switch (state) {
|
||||||
|
case "hover":
|
||||||
|
return "bg-[var(--color-magenta-magenta100)] text-[var(--color-content-default-primary)]";
|
||||||
|
case "focus":
|
||||||
|
return "bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] shadow-[0_0_5px_1px_#3281F8]";
|
||||||
|
case "selected":
|
||||||
|
return "bg-[var(--color-magenta-magenta100)] text-[var(--color-content-default-primary)] shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]";
|
||||||
|
case "default":
|
||||||
|
default:
|
||||||
|
return "bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)]";
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const positionStyles = getPositionStyles(position);
|
||||||
|
const stateStyles = getStateStyles(state);
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (onFocus) {
|
||||||
|
onFocus(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onFocus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (onBlur) {
|
||||||
|
onBlur(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onBlur]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (onChange) {
|
||||||
|
onChange(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleClasses = `
|
||||||
|
${positionStyles}
|
||||||
|
${stateStyles}
|
||||||
|
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
|
||||||
|
${className}
|
||||||
|
`
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, " ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
id={groupId}
|
||||||
|
type="button"
|
||||||
|
role="button"
|
||||||
|
aria-label={ariaLabel || (showText ? undefined : "Toggle option")}
|
||||||
|
onClick={handleClick}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
className={toggleClasses}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{showText ? children : children || "☰"}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
ToggleGroup.displayName = "ToggleGroup";
|
||||||
|
|
||||||
|
export default ToggleGroup;
|
||||||
+251
-62
@@ -1,18 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Toggle from "../components/Toggle";
|
import ToggleGroup from "../components/ToggleGroup";
|
||||||
|
|
||||||
export default function FormsPlayground() {
|
export default function FormsPlayground() {
|
||||||
|
const [selectedToggle, setSelectedToggle] = useState("active");
|
||||||
|
const [selectedFilter, setSelectedFilter] = useState("all");
|
||||||
const [toggleStates, setToggleStates] = useState({
|
const [toggleStates, setToggleStates] = useState({
|
||||||
default: false,
|
default: false,
|
||||||
hover: false,
|
hover: false,
|
||||||
selected: true,
|
selected: true,
|
||||||
focus: false,
|
focus: false,
|
||||||
disabled: false,
|
|
||||||
icon: false,
|
|
||||||
text: false,
|
|
||||||
both: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleToggleChange = (key) => (e) => {
|
const handleToggleChange = (key) => (e) => {
|
||||||
@@ -22,76 +20,267 @@ export default function FormsPlayground() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleGroupChange = (position) => (e) => {
|
||||||
|
setSelectedToggle(position);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterChange = (filter) => (e) => {
|
||||||
|
setSelectedFilter(filter);
|
||||||
|
};
|
||||||
|
|
||||||
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]">Toggle Examples</h2>
|
<h2 className="font-space text-[18px]">Toggle Group Examples</h2>
|
||||||
<div
|
<div
|
||||||
className="max-w-[520px] space-y-[16px] bg-white p-6 rounded-lg border border-gray-200 shadow-lg"
|
className="max-w-[520px] space-y-[16px] bg-white p-6 rounded-lg border border-gray-200 shadow-lg"
|
||||||
//style={{ backgroundColor: "white" }}
|
style={{ backgroundColor: "white" }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-space text-[14px] mb-[8px]">States</h3>
|
<h3 className="font-space text-[14px] mb-[8px]">
|
||||||
<div className="space-y-[12px]">
|
Interactive Toggle Group
|
||||||
<Toggle
|
</h3>
|
||||||
label="Default State"
|
<div className="flex">
|
||||||
checked={toggleStates.default}
|
<ToggleGroup
|
||||||
onChange={handleToggleChange("default")}
|
position="left"
|
||||||
/>
|
state={selectedToggle === "active" ? "selected" : "default"}
|
||||||
<Toggle
|
showText={true}
|
||||||
label="Hover State"
|
onChange={handleToggleGroupChange("active")}
|
||||||
checked={toggleStates.hover}
|
>
|
||||||
onChange={handleToggleChange("hover")}
|
Active Deals
|
||||||
state="hover"
|
</ToggleGroup>
|
||||||
/>
|
<ToggleGroup
|
||||||
<Toggle
|
position="middle"
|
||||||
label="Selected State"
|
state={selectedToggle === "inactive" ? "selected" : "default"}
|
||||||
checked={toggleStates.selected}
|
showText={true}
|
||||||
onChange={handleToggleChange("selected")}
|
onChange={handleToggleGroupChange("inactive")}
|
||||||
/>
|
>
|
||||||
<Toggle
|
Inactive Deals
|
||||||
label="Focus State"
|
</ToggleGroup>
|
||||||
checked={toggleStates.focus}
|
<ToggleGroup
|
||||||
onChange={handleToggleChange("focus")}
|
position="right"
|
||||||
state="focus"
|
state={selectedToggle === "pending" ? "selected" : "default"}
|
||||||
/>
|
showText={true}
|
||||||
<Toggle
|
onChange={handleToggleGroupChange("pending")}
|
||||||
label="Disabled State"
|
>
|
||||||
checked={toggleStates.disabled}
|
Pending Deals
|
||||||
onChange={handleToggleChange("disabled")}
|
</ToggleGroup>
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-space text-[14px] mb-[8px]">Content Types</h3>
|
<h3 className="font-space text-[14px] mb-[8px]">States</h3>
|
||||||
<div className="space-y-[12px]">
|
<div className="flex space-x-2">
|
||||||
<Toggle
|
<ToggleGroup position="left" state="default" showText={true}>
|
||||||
label="Icon Only"
|
Default
|
||||||
checked={toggleStates.icon}
|
</ToggleGroup>
|
||||||
onChange={handleToggleChange("icon")}
|
<ToggleGroup position="middle" state="hover" showText={true}>
|
||||||
showIcon={true}
|
Hover
|
||||||
icon="I"
|
</ToggleGroup>
|
||||||
/>
|
<ToggleGroup position="middle" state="focus" showText={true}>
|
||||||
<Toggle
|
Focus
|
||||||
label="Text Only"
|
</ToggleGroup>
|
||||||
checked={toggleStates.text}
|
<ToggleGroup position="right" state="selected" showText={true}>
|
||||||
onChange={handleToggleChange("text")}
|
Selected
|
||||||
showText={true}
|
</ToggleGroup>
|
||||||
text="Toggle"
|
</div>
|
||||||
/>
|
</div>
|
||||||
<Toggle
|
|
||||||
label="Icon and Text"
|
<div>
|
||||||
checked={toggleStates.both}
|
<h3 className="font-space text-[14px] mb-[8px]">Positions</h3>
|
||||||
onChange={handleToggleChange("both")}
|
<div className="flex">
|
||||||
showIcon={true}
|
<ToggleGroup position="left" state="default" showText={true}>
|
||||||
showText={true}
|
Left
|
||||||
icon="I"
|
</ToggleGroup>
|
||||||
text="Toggle"
|
<ToggleGroup position="middle" state="default" showText={true}>
|
||||||
/>
|
Middle
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup position="middle" state="default" showText={true}>
|
||||||
|
Middle
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup position="right" state="default" showText={true}>
|
||||||
|
Right
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-space text-[14px] mb-[8px]">Without Text</h3>
|
||||||
|
<div className="flex">
|
||||||
|
<ToggleGroup position="left" state="default" showText={false}>
|
||||||
|
Icon
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup position="middle" state="selected" showText={false}>
|
||||||
|
Icon
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup position="right" state="default" showText={false}>
|
||||||
|
Icon
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Content Visibility Examples */}
|
||||||
|
<section className="space-y-[12px]">
|
||||||
|
<h2 className="font-space text-[18px]">Content Visibility Examples</h2>
|
||||||
|
|
||||||
|
{/* Deal Management Example */}
|
||||||
|
<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]">Deal Management</h3>
|
||||||
|
<div className="flex">
|
||||||
|
<ToggleGroup
|
||||||
|
position="left"
|
||||||
|
state={selectedToggle === "active" ? "selected" : "default"}
|
||||||
|
showText={true}
|
||||||
|
onChange={handleToggleGroupChange("active")}
|
||||||
|
>
|
||||||
|
Active Deals
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup
|
||||||
|
position="middle"
|
||||||
|
state={selectedToggle === "inactive" ? "selected" : "default"}
|
||||||
|
showText={true}
|
||||||
|
onChange={handleToggleGroupChange("inactive")}
|
||||||
|
>
|
||||||
|
Inactive Deals
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup
|
||||||
|
position="right"
|
||||||
|
state={selectedToggle === "pending" ? "selected" : "default"}
|
||||||
|
showText={true}
|
||||||
|
onChange={handleToggleGroupChange("pending")}
|
||||||
|
>
|
||||||
|
Pending Deals
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content that changes based on toggle selection */}
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
{selectedToggle === "active" && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-green-700 mb-2">
|
||||||
|
Active Deals
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li className="flex justify-between items-center p-2 bg-white rounded border">
|
||||||
|
<span>Summer Sale - 50% Off</span>
|
||||||
|
<span className="text-green-600 font-semibold">$299</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex justify-between items-center p-2 bg-white rounded border">
|
||||||
|
<span>Black Friday Special</span>
|
||||||
|
<span className="text-green-600 font-semibold">$199</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedToggle === "inactive" && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-gray-700 mb-2">
|
||||||
|
Inactive Deals
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li className="flex justify-between items-center p-2 bg-white rounded border opacity-60">
|
||||||
|
<span>Holiday Sale - Expired</span>
|
||||||
|
<span className="text-gray-500 line-through">$399</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex justify-between items-center p-2 bg-white rounded border opacity-60">
|
||||||
|
<span>Spring Clearance - Ended</span>
|
||||||
|
<span className="text-gray-500 line-through">$149</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedToggle === "pending" && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-yellow-700 mb-2">
|
||||||
|
Pending Deals
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li className="flex justify-between items-center p-2 bg-white rounded border">
|
||||||
|
<span>Cyber Monday - Coming Soon</span>
|
||||||
|
<span className="text-yellow-600 font-semibold">$99</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex justify-between items-center p-2 bg-white rounded border">
|
||||||
|
<span>New Year Sale - Pending</span>
|
||||||
|
<span className="text-yellow-600 font-semibold">$79</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Example */}
|
||||||
|
<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]">Content Filter</h3>
|
||||||
|
<div className="flex">
|
||||||
|
<ToggleGroup
|
||||||
|
position="left"
|
||||||
|
state={selectedFilter === "all" ? "selected" : "default"}
|
||||||
|
showText={true}
|
||||||
|
onChange={handleFilterChange("all")}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup
|
||||||
|
position="middle"
|
||||||
|
state={selectedFilter === "featured" ? "selected" : "default"}
|
||||||
|
showText={true}
|
||||||
|
onChange={handleFilterChange("featured")}
|
||||||
|
>
|
||||||
|
Featured
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup
|
||||||
|
position="right"
|
||||||
|
state={selectedFilter === "recent" ? "selected" : "default"}
|
||||||
|
showText={true}
|
||||||
|
onChange={handleFilterChange("recent")}
|
||||||
|
>
|
||||||
|
Recent
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<div
|
||||||
|
className={`p-4 rounded-lg border ${
|
||||||
|
selectedFilter === "all" || selectedFilter === "featured"
|
||||||
|
? "block"
|
||||||
|
: "hidden"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<h4 className="font-semibold">Featured Article</h4>
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
This is a featured article that shows when "All" or "Featured"
|
||||||
|
is selected.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`p-4 rounded-lg border ${
|
||||||
|
selectedFilter === "all" || selectedFilter === "recent"
|
||||||
|
? "block"
|
||||||
|
: "hidden"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<h4 className="font-semibold">Recent Post</h4>
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
This is a recent post that shows when "All" or "Recent" is
|
||||||
|
selected.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`p-4 rounded-lg border ${
|
||||||
|
selectedFilter === "all" ? "block" : "hidden"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<h4 className="font-semibold">General Content</h4>
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
This content only shows when "All" is selected.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ToggleGroup from "../app/components/ToggleGroup";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Forms/ToggleGroup",
|
||||||
|
component: ToggleGroup,
|
||||||
|
parameters: {
|
||||||
|
layout: "centered",
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
position: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["left", "middle", "right"],
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["default", "hover", "focus", "selected"],
|
||||||
|
},
|
||||||
|
showText: {
|
||||||
|
control: { type: "boolean" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args) => <ToggleGroup {...args}>Toggle Item</ToggleGroup>;
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
Default.args = {
|
||||||
|
position: "left",
|
||||||
|
state: "default",
|
||||||
|
showText: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Middle = Template.bind({});
|
||||||
|
Middle.args = {
|
||||||
|
position: "middle",
|
||||||
|
state: "default",
|
||||||
|
showText: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Right = Template.bind({});
|
||||||
|
Right.args = {
|
||||||
|
position: "right",
|
||||||
|
state: "default",
|
||||||
|
showText: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const States = () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-semibold">Toggle Group States</h3>
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<ToggleGroup position="left" state="default">
|
||||||
|
Default
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup position="middle" state="hover">
|
||||||
|
Hover
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup position="middle" state="focus">
|
||||||
|
Focus
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup position="right" state="selected">
|
||||||
|
Selected
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Positions = () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-semibold">Toggle Group Positions</h3>
|
||||||
|
<div className="flex">
|
||||||
|
<ToggleGroup position="left" state="default">
|
||||||
|
Left
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup position="middle" state="default">
|
||||||
|
Middle
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup position="middle" state="default">
|
||||||
|
Middle
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup position="right" state="default">
|
||||||
|
Right
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const WithText = Template.bind({});
|
||||||
|
WithText.args = {
|
||||||
|
position: "left",
|
||||||
|
state: "default",
|
||||||
|
showText: true,
|
||||||
|
children: "Active Deals",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithoutText = Template.bind({});
|
||||||
|
WithoutText.args = {
|
||||||
|
position: "left",
|
||||||
|
state: "default",
|
||||||
|
showText: false,
|
||||||
|
children: "☰",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithIcons = () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-semibold">Toggle Group with Icons</h3>
|
||||||
|
<div className="flex">
|
||||||
|
<ToggleGroup
|
||||||
|
position="left"
|
||||||
|
state="default"
|
||||||
|
showText={false}
|
||||||
|
ariaLabel="Menu"
|
||||||
|
>
|
||||||
|
☰
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup
|
||||||
|
position="middle"
|
||||||
|
state="selected"
|
||||||
|
showText={false}
|
||||||
|
ariaLabel="Menu"
|
||||||
|
>
|
||||||
|
☰
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup
|
||||||
|
position="right"
|
||||||
|
state="default"
|
||||||
|
showText={false}
|
||||||
|
ariaLabel="Menu"
|
||||||
|
>
|
||||||
|
☰
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Interactive = () => {
|
||||||
|
const [selectedPosition, setSelectedPosition] = React.useState("left");
|
||||||
|
const [state, setState] = React.useState("default");
|
||||||
|
const [showText, setShowText] = React.useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold">Interactive Toggle Group</h3>
|
||||||
|
<div className="flex">
|
||||||
|
<ToggleGroup
|
||||||
|
position="left"
|
||||||
|
state={selectedPosition === "left" ? "selected" : state}
|
||||||
|
showText={showText}
|
||||||
|
onChange={() => setSelectedPosition("left")}
|
||||||
|
ariaLabel={!showText ? "Active Deals" : undefined}
|
||||||
|
>
|
||||||
|
{showText ? "Active Deals" : "☰"}
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup
|
||||||
|
position="middle"
|
||||||
|
state={selectedPosition === "middle" ? "selected" : state}
|
||||||
|
showText={showText}
|
||||||
|
onChange={() => setSelectedPosition("middle")}
|
||||||
|
ariaLabel={!showText ? "Inactive Deals" : undefined}
|
||||||
|
>
|
||||||
|
{showText ? "Inactive Deals" : "☰"}
|
||||||
|
</ToggleGroup>
|
||||||
|
<ToggleGroup
|
||||||
|
position="right"
|
||||||
|
state={selectedPosition === "right" ? "selected" : state}
|
||||||
|
showText={showText}
|
||||||
|
onChange={() => setSelectedPosition("right")}
|
||||||
|
ariaLabel={!showText ? "Pending Deals" : undefined}
|
||||||
|
>
|
||||||
|
{showText ? "Pending Deals" : "☰"}
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-md font-semibold">Controls</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<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="showText"
|
||||||
|
checked={showText}
|
||||||
|
onChange={(e) => setShowText(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="showText" className="text-sm">
|
||||||
|
Show Text
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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