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";
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user