Input component with storybook and testing

This commit is contained in:
adilallo
2025-10-10 09:07:47 -06:00
parent 04783d3f62
commit 2bc5fcdf45
12 changed files with 1838 additions and 46 deletions
+2 -3
View File
@@ -1,6 +1,6 @@
"use client";
import React, { memo } from "react";
import React, { memo, useId } from "react";
/**
* Checkbox
@@ -83,8 +83,7 @@ const Checkbox = memo(
};
// Generate unique ID for accessibility if not provided
const checkboxId =
id || `checkbox-${Math.random().toString(36).substr(2, 9)}`;
const checkboxId = id || `checkbox-${useId()}`;
const accessibilityProps = {
role: "checkbox",
+183
View File
@@ -0,0 +1,183 @@
"use client";
import React, { memo, useCallback, forwardRef, useId } from "react";
const Input = forwardRef(
(
{
size = "medium",
labelVariant = "default",
state = "default",
disabled = false,
error = false,
label,
placeholder,
value,
onChange,
onFocus,
onBlur,
id,
name,
type = "text",
className = "",
...props
},
ref
) => {
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const inputId = id || `input-${generatedId}`;
// Size variants
const sizeStyles = {
small: {
input: "h-[30px] px-[12px] text-[10px]",
label: "text-[12px] leading-[14px] font-medium",
container: "gap-[4px]",
radius: "var(--measures-radius-small)",
},
medium: {
input: "h-[36px] px-[16px] text-[14px] leading-[20px]",
label: "text-[14px] leading-[16px] font-medium",
container: "gap-[8px]",
radius: "var(--measures-radius-medium)",
},
large: {
input: "h-[40px] px-[20px] text-[16px] leading-[24px]",
label: "text-[16px] leading-[20px] font-medium",
container: "gap-[12px]",
radius: "var(--measures-radius-large)",
},
};
// State styles
const getStateStyles = () => {
if (disabled) {
return {
input:
"bg-[var(--color-content-default-secondary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] cursor-not-allowed",
label: "text-[var(--color-content-default-primary)]",
};
}
if (error) {
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-utility-negative)]",
label: "text-[var(--color-content-default-primary)]",
};
}
switch (state) {
case "active":
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-primary)]",
};
case "hover":
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-2 border-[var(--color-border-default-brand-primary)]",
label: "text-[var(--color-content-default-primary)]",
};
case "focus":
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-utility-info)] shadow-[0_0_5px_3px_#3281F8]",
label: "text-[var(--color-content-default-primary)]",
};
default:
return {
input:
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] hover:outline hover:outline-2 hover:outline-[var(--color-border-default-tertiary)]",
label: "text-[var(--color-content-default-primary)]",
};
}
};
const stateStyles = getStateStyles();
const currentSize = sizeStyles[size];
// Container classes based on label variant
const containerClasses =
labelVariant === "horizontal"
? `flex items-center gap-[12px]`
: `flex flex-col ${currentSize.container}`;
const labelClasses =
labelVariant === "horizontal"
? `${currentSize.label} font-inter min-w-fit`
: `${currentSize.label} font-inter`;
const inputClasses = `
w-full border transition-all duration-200 ease-in-out
focus:outline-none focus:ring-0
${currentSize.input}
${stateStyles.input}
${className}
`.trim();
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]
);
return (
<div className={containerClasses}>
{label && (
<label
htmlFor={inputId}
className={`${labelClasses} font-inter font-medium`}
style={{ color: "var(--color-content-default-secondary)" }}
>
{label}
</label>
)}
<div className={disabled ? "opacity-40" : ""}>
<input
ref={ref}
id={inputId}
name={name}
type={type}
value={value}
placeholder={placeholder}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
className={inputClasses}
style={{ borderRadius: currentSize.radius }}
{...props}
/>
</div>
</div>
);
}
);
Input.displayName = "Input";
export default memo(Input);
+2 -2
View File
@@ -1,6 +1,6 @@
"use client";
import React, { memo, useCallback } from "react";
import React, { memo, useCallback, useId } from "react";
const RadioButton = ({
checked = false,
@@ -71,7 +71,7 @@ const RadioButton = ({
"focus:outline focus:outline-1 focus:outline-[var(--color-border-default-utility-info)] focus:shadow-[0_0_10px_1px_var(--color-surface-inverse-brand-primary)]";
// Generate unique ID for accessibility if not provided
const radioId = id || `radio-${Math.random().toString(36).substr(2, 9)}`;
const radioId = id || `radio-${useId()}`;
const handleToggle = useCallback(
(e) => {
+2 -3
View File
@@ -1,6 +1,6 @@
"use client";
import React, { memo, useCallback } from "react";
import React, { memo, useCallback, useId } from "react";
import RadioButton from "./RadioButton";
const RadioGroup = ({
@@ -15,8 +15,7 @@ const RadioGroup = ({
...props
}) => {
// Generate unique ID for accessibility if not provided
const groupId =
name || `radio-group-${Math.random().toString(36).substr(2, 9)}`;
const groupId = name || `radio-group-${useId()}`;
const handleChange = useCallback(
(optionValue) => {
+85 -30
View File
@@ -3,14 +3,21 @@
import React, { useState } from "react";
import Checkbox from "../components/Checkbox";
import RadioButton from "../components/RadioButton";
import RadioGroup from "../components/RadioGroup";
import Input from "../components/Input";
export default function FormsPlayground() {
const [standardChecked, setStandardChecked] = useState(false);
const [inverseChecked, setInverseChecked] = useState(true);
const [radioValue, setRadioValue] = useState("option1");
const [standardRadioValue, setStandardRadioValue] = useState("option1");
const [inverseRadioValue, setInverseRadioValue] = useState("option2");
const [smallValue, setSmallValue] = useState("Data");
const [mediumValue, setMediumValue] = useState("Data");
const [largeValue, setLargeValue] = useState("Data");
const [defaultLabelValue, setDefaultLabelValue] = useState("Data");
const [horizontalLabelValue, setHorizontalLabelValue] = useState("Data");
const [smallHorizontalValue, setSmallHorizontalValue] = useState("Data");
const [smallDefaultValue, setSmallDefaultValue] = useState("Data");
const [errorStateValue, setErrorStateValue] = useState("Data");
const [disabledStateValue, setDisabledStateValue] = useState("Data");
return (
<div className="p-[24px] space-y-[24px]">
@@ -59,38 +66,86 @@ export default function FormsPlayground() {
</section>
<section className="space-y-[12px]">
<h2 className="font-space text-[18px]">Radio Group</h2>
<h2 className="font-space text-[18px]">Input Examples</h2>
<div className="max-w-[520px] space-y-[16px]">
<div>
<h3 className="font-space text-[14px] mb-[8px]">Standard Mode</h3>
<RadioGroup
name="standard-radio"
value={standardRadioValue}
mode="standard"
state="default"
onChange={({ value }) => setStandardRadioValue(value)}
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
/>
<h3 className="font-space text-[14px] mb-[8px]">Sizes</h3>
<div className="space-y-[12px]">
<Input
label="Small"
size="small"
value={smallValue}
onChange={(e) => setSmallValue(e.target.value)}
/>
<Input
label="Medium"
size="medium"
value={mediumValue}
onChange={(e) => setMediumValue(e.target.value)}
/>
<Input
label="Large"
size="large"
value={largeValue}
onChange={(e) => setLargeValue(e.target.value)}
/>
</div>
</div>
<div>
<h3 className="font-space text-[14px] mb-[8px]">Inverse Mode</h3>
<RadioGroup
name="inverse-radio"
value={inverseRadioValue}
mode="inverse"
state="default"
onChange={({ value }) => setInverseRadioValue(value)}
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
/>
<h3 className="font-space text-[14px] mb-[8px]">Label Variants</h3>
<div className="space-y-[12px]">
<Input
label="Default (Top Label)"
labelVariant="default"
size="medium"
value={defaultLabelValue}
onChange={(e) => setDefaultLabelValue(e.target.value)}
/>
<Input
label="Small Default"
labelVariant="default"
size="small"
value={smallDefaultValue}
onChange={(e) => setSmallDefaultValue(e.target.value)}
/>
<Input
label="Horizontal (Left Label)"
labelVariant="horizontal"
size="medium"
value={horizontalLabelValue}
onChange={(e) => setHorizontalLabelValue(e.target.value)}
/>
<Input
label="Small Horizontal"
labelVariant="horizontal"
size="small"
value={smallHorizontalValue}
onChange={(e) => setSmallHorizontalValue(e.target.value)}
/>
</div>
</div>
<div>
<h3 className="font-space text-[14px] mb-[8px]">States</h3>
<div className="space-y-[12px]">
<Input
label="Error"
size="medium"
state="default"
error={true}
value={errorStateValue}
onChange={(e) => setErrorStateValue(e.target.value)}
/>
<Input
label="Disabled"
size="medium"
state="default"
disabled={true}
value={disabledStateValue}
onChange={(e) => setDisabledStateValue(e.target.value)}
/>
</div>
</div>
</div>
</section>
+5 -8
View File
@@ -31,15 +31,12 @@
--color-*: initial;
/* Font families */
--font-sans:
var(--font-inter), ui-sans-serif, system-ui, -apple-system, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif;
--font-display:
var(--font-bricolage-grotesque), ui-sans-serif, system-ui, -apple-system,
--font-sans: var(--font-inter), ui-sans-serif, system-ui, -apple-system,
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono:
var(--font-space-grotesk), ui-monospace, SFMono-Regular, "SF Mono",
Consolas, "Liberation Mono", Menlo, monospace;
--font-display: var(--font-bricolage-grotesque), ui-sans-serif, system-ui,
-apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: var(--font-space-grotesk), ui-monospace, SFMono-Regular,
"SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
/* Dimension */
--spacing-scale-000: 0px;