Toggle component with storybook and testing

This commit is contained in:
adilallo
2025-10-14 15:40:51 -06:00
parent b71f0a7dea
commit 929729a67f
6 changed files with 886 additions and 93 deletions
+194
View File
@@ -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
View File
@@ -1,111 +1,96 @@
"use client";
import React, { useState } from "react";
import TextArea from "../components/TextArea";
import Toggle from "../components/Toggle";
export default function FormsPlayground() {
const [smallValue, setSmallValue] = useState("");
const [mediumValue, setMediumValue] = useState("");
const [largeValue, setLargeValue] = useState("");
const [defaultLabelValue, setDefaultLabelValue] = useState("");
const [horizontalLabelValue, setHorizontalLabelValue] = useState("");
const [smallHorizontalValue, setSmallHorizontalValue] = useState("");
const [smallDefaultValue, setSmallDefaultValue] = useState("");
const [errorStateValue, setErrorStateValue] = useState("");
const [disabledStateValue, setDisabledStateValue] = useState("");
const [toggleStates, setToggleStates] = useState({
default: false,
hover: false,
selected: true,
focus: false,
disabled: false,
icon: false,
text: false,
both: false,
});
const handleToggleChange = (key) => (e) => {
setToggleStates((prev) => ({
...prev,
[key]: !prev[key],
}));
};
return (
<div className="p-[24px] space-y-[24px]">
<h1 className="font-bricolage text-[24px]">Forms Playground</h1>
<section className="space-y-[12px]">
<h2 className="font-space text-[18px]">TextArea Examples</h2>
<div className="max-w-[520px] space-y-[16px]">
<div>
<h3 className="font-space text-[14px] mb-[8px]">Sizes</h3>
<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>
<h2 className="font-space text-[18px]">Toggle Examples</h2>
<div
className="max-w-[520px] space-y-[16px] bg-white p-6 rounded-lg border border-gray-200 shadow-lg"
//style={{ backgroundColor: "white" }}
>
<div>
<h3 className="font-space text-[14px] mb-[8px]">States</h3>
<div className="space-y-[12px]">
<TextArea
label="Error"
size="medium"
state="default"
error={true}
value={errorStateValue}
onChange={(e) => setErrorStateValue(e.target.value)}
placeholder="Enter text..."
<Toggle
label="Default State"
checked={toggleStates.default}
onChange={handleToggleChange("default")}
/>
<TextArea
label="Disabled"
size="medium"
state="default"
disabled={true}
value={disabledStateValue}
onChange={(e) => setDisabledStateValue(e.target.value)}
placeholder="Enter text..."
<Toggle
label="Hover State"
checked={toggleStates.hover}
onChange={handleToggleChange("hover")}
state="hover"
/>
<Toggle
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>