Text area component with storybook and testing
This commit is contained in:
@@ -59,7 +59,7 @@ const Input = forwardRef(
|
||||
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)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ const Input = forwardRef(
|
||||
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)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,25 +76,25 @@ const Input = forwardRef(
|
||||
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)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
case "hover":
|
||||
return {
|
||||
input:
|
||||
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
||||
label: "text-[var(--color-content-default-primary)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
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)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
input:
|
||||
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
||||
label: "text-[var(--color-content-default-primary)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -153,8 +153,7 @@ const Input = forwardRef(
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className={`${labelClasses} font-inter font-medium`}
|
||||
style={{ color: "var(--color-content-default-secondary)" }}
|
||||
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-secondary)]`}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
@@ -133,14 +133,14 @@ const Select = forwardRef(
|
||||
return {
|
||||
select:
|
||||
"bg-[var(--color-content-default-secondary)] border-[var(--color-border-default-tertiary)] cursor-not-allowed opacity-40",
|
||||
label: "text-[var(--color-content-default-primary)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
select: "border-[var(--color-border-default-utility-negative)]",
|
||||
label: "text-[var(--color-content-default-primary)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -149,18 +149,18 @@ const Select = forwardRef(
|
||||
return {
|
||||
select:
|
||||
"border-[var(--color-border-default-tertiary)] shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
||||
label: "text-[var(--color-content-default-primary)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
case "focus":
|
||||
return {
|
||||
select:
|
||||
"border-[var(--color-border-default-utility-info)] shadow-[0_0_5px_3px_#3281F8]",
|
||||
label: "text-[var(--color-content-default-primary)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
select: "border-[var(--color-border-default-tertiary)]",
|
||||
label: "text-[var(--color-content-default-primary)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -244,7 +244,11 @@ const Select = forwardRef(
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{label && (
|
||||
<label id={labelId} htmlFor={selectId} className={labelClasses}>
|
||||
<label
|
||||
id={labelId}
|
||||
htmlFor={selectId}
|
||||
className={`${labelClasses} text-[var(--color-content-default-secondary)]`}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
"use client";
|
||||
|
||||
import React, { memo, useCallback, forwardRef, useId } from "react";
|
||||
|
||||
const TextArea = forwardRef(
|
||||
(
|
||||
{
|
||||
size = "medium",
|
||||
labelVariant = "default",
|
||||
state = "default",
|
||||
disabled = false,
|
||||
error = false,
|
||||
label,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
id,
|
||||
name,
|
||||
className = "",
|
||||
rows,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
// Generate unique ID for accessibility if not provided
|
||||
const generatedId = useId();
|
||||
const textareaId = id || `textarea-${generatedId}`;
|
||||
|
||||
// Size variants with specific heights and radius for TextArea
|
||||
const sizeStyles = {
|
||||
small: {
|
||||
textarea:
|
||||
labelVariant === "horizontal"
|
||||
? "h-[60px] px-[12px] py-[8px] text-[10px]"
|
||||
: "h-[60px] px-[12px] py-[8px] text-[10px]",
|
||||
label: "text-[12px] leading-[14px] font-medium",
|
||||
container: "gap-[4px]",
|
||||
radius: "var(--measures-radius-xsmall)",
|
||||
},
|
||||
medium: {
|
||||
textarea:
|
||||
labelVariant === "horizontal"
|
||||
? "h-[110px] px-[12px] py-[8px] text-[14px] leading-[20px]"
|
||||
: "h-[100px] px-[12px] py-[8px] text-[14px] leading-[20px]",
|
||||
label: "text-[14px] leading-[16px] font-medium",
|
||||
container: "gap-[8px]",
|
||||
radius: "var(--measures-radius-xsmall)",
|
||||
},
|
||||
large: {
|
||||
textarea: "h-[150px] px-[12px] py-[8px] text-[16px] leading-[24px]",
|
||||
label: "text-[16px] leading-[20px] font-medium",
|
||||
container: "gap-[12px]",
|
||||
radius: "var(--measures-radius-small)",
|
||||
},
|
||||
};
|
||||
|
||||
// State styles
|
||||
const getStateStyles = () => {
|
||||
if (disabled) {
|
||||
return {
|
||||
textarea:
|
||||
"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-secondary)]",
|
||||
};
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
textarea:
|
||||
"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-secondary)]",
|
||||
};
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case "active":
|
||||
return {
|
||||
textarea:
|
||||
"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-secondary)]",
|
||||
};
|
||||
case "hover":
|
||||
return {
|
||||
textarea:
|
||||
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
case "focus":
|
||||
return {
|
||||
textarea:
|
||||
"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-secondary)]",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
textarea:
|
||||
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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 textareaClasses = `
|
||||
w-full border transition-all duration-200 ease-in-out
|
||||
focus:outline-none focus:ring-0 resize-none
|
||||
${currentSize.textarea}
|
||||
${stateStyles.textarea}
|
||||
${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={textareaId}
|
||||
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-secondary)]`}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className={disabled ? "opacity-40" : ""}>
|
||||
<textarea
|
||||
ref={ref}
|
||||
id={textareaId}
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={disabled}
|
||||
rows={rows}
|
||||
className={textareaClasses}
|
||||
style={{ borderRadius: currentSize.radius }}
|
||||
aria-disabled={disabled}
|
||||
aria-invalid={error}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TextArea.displayName = "TextArea";
|
||||
|
||||
export default memo(TextArea);
|
||||
+29
-96
@@ -1,11 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Select from "../components/Select";
|
||||
import ContextMenu from "../components/ContextMenu";
|
||||
import ContextMenuItem from "../components/ContextMenuItem";
|
||||
import ContextMenuSection from "../components/ContextMenuSection";
|
||||
import ContextMenuDivider from "../components/ContextMenuDivider";
|
||||
import TextArea from "../components/TextArea";
|
||||
|
||||
export default function FormsPlayground() {
|
||||
const [smallValue, setSmallValue] = useState("");
|
||||
@@ -23,157 +19,94 @@ export default function FormsPlayground() {
|
||||
<h1 className="font-bricolage text-[24px]">Forms Playground</h1>
|
||||
|
||||
<section className="space-y-[12px]">
|
||||
<h2 className="font-space text-[18px]">Select Examples</h2>
|
||||
<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]">
|
||||
<Select
|
||||
<TextArea
|
||||
label="Small"
|
||||
size="small"
|
||||
value={smallValue}
|
||||
onChange={(e) => setSmallValue(e.target.value)}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="item1">Context Menu Item 1</option>
|
||||
<option value="item2">Context Menu Item 2</option>
|
||||
<option value="item3">Context Menu Item 3</option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
<TextArea
|
||||
label="Medium"
|
||||
size="medium"
|
||||
value={mediumValue}
|
||||
onChange={(e) => setMediumValue(e.target.value)}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="item1">Context Menu Item 1</option>
|
||||
<option value="item2">Context Menu Item 2</option>
|
||||
<option value="item3">Context Menu Item 3</option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
<TextArea
|
||||
label="Large"
|
||||
size="large"
|
||||
value={largeValue}
|
||||
onChange={(e) => setLargeValue(e.target.value)}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="item1">Context Menu Item 1</option>
|
||||
<option value="item2">Context Menu Item 2</option>
|
||||
<option value="item3">Context Menu Item 3</option>
|
||||
</Select>
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-space text-[14px] mb-[8px]">Label Variants</h3>
|
||||
<div className="space-y-[12px]">
|
||||
<Select
|
||||
<TextArea
|
||||
label="Default (Top Label)"
|
||||
labelVariant="default"
|
||||
size="medium"
|
||||
value={defaultLabelValue}
|
||||
onChange={(e) => setDefaultLabelValue(e.target.value)}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="item1">Context Menu Item 1</option>
|
||||
<option value="item2">Context Menu Item 2</option>
|
||||
<option value="item3">Context Menu Item 3</option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
<TextArea
|
||||
label="Small Default"
|
||||
labelVariant="default"
|
||||
size="small"
|
||||
value={smallDefaultValue}
|
||||
onChange={(e) => setSmallDefaultValue(e.target.value)}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="item1">Context Menu Item 1</option>
|
||||
<option value="item2">Context Menu Item 2</option>
|
||||
<option value="item3">Context Menu Item 3</option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
<TextArea
|
||||
label="Horizontal (Left Label)"
|
||||
labelVariant="horizontal"
|
||||
size="medium"
|
||||
value={horizontalLabelValue}
|
||||
onChange={(e) => setHorizontalLabelValue(e.target.value)}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="item1">Context Menu Item 1</option>
|
||||
<option value="item2">Context Menu Item 2</option>
|
||||
<option value="item3">Context Menu Item 3</option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
<TextArea
|
||||
label="Small Horizontal"
|
||||
labelVariant="horizontal"
|
||||
size="small"
|
||||
value={smallHorizontalValue}
|
||||
onChange={(e) => setSmallHorizontalValue(e.target.value)}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="item1">Context Menu Item 1</option>
|
||||
<option value="item2">Context Menu Item 2</option>
|
||||
<option value="item3">Context Menu Item 3</option>
|
||||
</Select>
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-space text-[14px] mb-[8px]">States</h3>
|
||||
<div className="space-y-[12px]">
|
||||
<Select
|
||||
<TextArea
|
||||
label="Error"
|
||||
size="medium"
|
||||
state="default"
|
||||
error={true}
|
||||
value={errorStateValue}
|
||||
onChange={(e) => setErrorStateValue(e.target.value)}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="item1">Context Menu Item 1</option>
|
||||
<option value="item2">Context Menu Item 2</option>
|
||||
<option value="item3">Context Menu Item 3</option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
<TextArea
|
||||
label="Disabled"
|
||||
size="medium"
|
||||
state="default"
|
||||
disabled={true}
|
||||
value={disabledStateValue}
|
||||
onChange={(e) => setDisabledStateValue(e.target.value)}
|
||||
placeholder="Select"
|
||||
>
|
||||
<option value="item1">Context Menu Item 1</option>
|
||||
<option value="item2">Context Menu Item 2</option>
|
||||
<option value="item3">Context Menu Item 3</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-[12px]">
|
||||
<h2 className="font-space text-[18px]">Context Menu Examples</h2>
|
||||
<div className="max-w-[520px] space-y-[16px]">
|
||||
<div>
|
||||
<h3 className="font-space text-[14px] mb-[8px]">
|
||||
Context Menu Demo
|
||||
</h3>
|
||||
<div className="space-y-[12px]">
|
||||
<ContextMenu>
|
||||
<ContextMenuItem>Context Menu Item</ContextMenuItem>
|
||||
<ContextMenuItem>Context Menu Item</ContextMenuItem>
|
||||
<ContextMenuItem hasSubmenu>Context Menu Item</ContextMenuItem>
|
||||
<ContextMenuItem hasSubmenu>Context Menu Item</ContextMenuItem>
|
||||
<ContextMenuDivider />
|
||||
<ContextMenuItem selected>Context Menu Item</ContextMenuItem>
|
||||
<ContextMenuItem>Context Menu Item</ContextMenuItem>
|
||||
<ContextMenuDivider />
|
||||
<ContextMenuSection title="Section Title">
|
||||
<ContextMenuItem>Context Menu Item</ContextMenuItem>
|
||||
<ContextMenuItem>Context Menu Item</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user