f6a0673082
CI Pipeline / test (20) (pull_request) Failing after 1m17s
CI Pipeline / test (18) (pull_request) Failing after 1m28s
CI Pipeline / e2e (chromium) (pull_request) Failing after 1m33s
CI Pipeline / e2e (firefox) (pull_request) Failing after 1m27s
CI Pipeline / e2e (webkit) (pull_request) Failing after 1m34s
CI Pipeline / visual-regression (pull_request) Failing after 2m9s
CI Pipeline / storybook (pull_request) Failing after 1m5s
CI Pipeline / performance (pull_request) Failing after 1m42s
CI Pipeline / lint (pull_request) Failing after 49s
CI Pipeline / build (pull_request) Failing after 1m29s
222 lines
6.7 KiB
TypeScript
222 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import React, { memo, useCallback, forwardRef, useId } from "react";
|
|
|
|
interface TextAreaProps
|
|
extends Omit<
|
|
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
"size" | "onChange" | "onFocus" | "onBlur"
|
|
> {
|
|
size?: "small" | "medium" | "large";
|
|
labelVariant?: "default" | "horizontal";
|
|
state?: "default" | "active" | "hover" | "focus";
|
|
disabled?: boolean;
|
|
error?: boolean;
|
|
label?: string;
|
|
placeholder?: string;
|
|
value?: string;
|
|
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
|
onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
|
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
|
className?: string;
|
|
rows?: number;
|
|
}
|
|
|
|
const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|
(
|
|
{
|
|
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: Record<
|
|
string,
|
|
{
|
|
textarea: string;
|
|
label: string;
|
|
container: string;
|
|
radius: string;
|
|
}
|
|
> = {
|
|
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 = (): {
|
|
textarea: string;
|
|
label: string;
|
|
} => {
|
|
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: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
if (!disabled && onChange) {
|
|
onChange(e);
|
|
}
|
|
},
|
|
[disabled, onChange]
|
|
);
|
|
|
|
const handleFocus = useCallback(
|
|
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
if (!disabled && onFocus) {
|
|
onFocus(e);
|
|
}
|
|
},
|
|
[disabled, onFocus]
|
|
);
|
|
|
|
const handleBlur = useCallback(
|
|
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
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);
|