Files
community-rule/app/components/controls/TextArea/TextArea.container.tsx
T
adilalloandCursor 20ea4a2f30 Give MultiSelect chips accessible names and start creation fields empty.
Catalog chips now announce Select or Deselect so they are named for assistive tech. Method and value modals open blank; Generate draft inserts the catalog copy that used to be prefilled, and Customize still keeps the section headings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 15:59:00 -06:00

232 lines
8.2 KiB
TypeScript

"use client";
import { memo, forwardRef } from "react";
import { useComponentId, useFormField } from "../../../hooks";
import { useTranslation } from "../../../contexts/MessagesContext";
import { TextAreaView } from "./TextArea.view";
import type { TextAreaProps } from "./TextArea.types";
/**
* Figma: "Control / TextArea". Multi-line text input with size
* variants, an embedded appearance, and an optional label and help glyph.
*/
const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
(
{
size: sizeProp = "medium",
labelVariant: labelVariantProp = "default",
state: stateProp = "default",
disabled = false,
error = false,
label,
placeholder,
value,
onChange,
onFocus,
onBlur,
id,
name,
className = "",
rows,
textHint = false,
formHeader = true,
showHelpIcon = false,
appearance: appearanceProp = "default",
...props
},
ref,
) => {
const t = useTranslation("controlsChrome");
const size = sizeProp;
const labelVariant = labelVariantProp;
const state = stateProp;
const appearance = appearanceProp;
// Generate unique ID for accessibility if not provided
const { id: textareaId, labelId } = useComponentId("textarea", id);
// 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-xx-small-paragraph"
: "h-[60px] px-[12px] py-[8px] text-xx-small-paragraph",
label: "text-x-small-label",
container: "gap-[4px]",
radius: "var(--measures-radius-xsmall)",
},
medium: {
textarea:
labelVariant === "horizontal"
? "h-[110px] px-[12px] py-[8px] text-small-paragraph"
: "h-[100px] px-[12px] py-[8px] text-small-paragraph",
label: "text-small-label",
container: "gap-[8px]",
radius: "var(--measures-radius-xsmall)",
},
large: {
textarea: "h-[150px] px-[12px] py-[8px] text-medium-paragraph",
label: "text-medium-label",
container: "gap-[12px]",
radius: "var(--measures-radius-small)",
},
};
// Embedded (Figma 20736-12668): borderless grey block; default copy is
// tertiary, primary on focus so seeded modal fields stay muted until edit.
const getStateStyles = (): {
textarea: string;
label: string;
} => {
if (appearance === "compose") {
if (disabled) {
return {
textarea:
"border-0 bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-tertiary,#b4b4b4)] cursor-not-allowed opacity-60 min-h-[356px] h-[356px] px-[var(--measures-spacing-300,12px)] py-[var(--space-250,10px)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
return {
textarea:
"border-0 bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] placeholder:text-[var(--color-content-default-tertiary,#b4b4b4)] min-h-[356px] h-[356px] px-[var(--measures-spacing-300,12px)] py-[var(--space-250,10px)] focus:outline-none focus:ring-0",
label: "text-[var(--color-content-default-secondary)]",
};
}
if (appearance === "embedded") {
if (disabled) {
return {
textarea:
"border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-tertiary,#b4b4b4)] cursor-not-allowed opacity-60",
label: "text-[var(--color-content-default-secondary)]",
};
}
return {
textarea:
"border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-tertiary,#b4b4b4)] placeholder:text-[var(--color-content-default-tertiary,#b4b4b4)] focus:text-[var(--color-content-default-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-default-tertiary)] focus:ring-inset",
label: "text-[var(--color-content-default-secondary)]",
};
}
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} min-w-fit`
: `${currentSize.label} `;
const textareaClasses = `
scrollbar-design w-full transition-all duration-200 ease-in-out resize-none
${appearance === "embedded" || appearance === "compose" ? "rounded-[var(--radius-300,12px)]" : "border"}
${appearance === "compose" ? "text-medium-paragraph" : currentSize.textarea}
${stateStyles.textarea}
${className}
`.trim();
// Form field handlers with disabled state handling
const { handleChange, handleFocus, handleBlur } =
useFormField<HTMLTextAreaElement>(disabled, {
onChange,
onFocus,
onBlur,
});
return (
<TextAreaView
ref={ref}
textareaId={textareaId}
labelId={labelId}
size={size}
labelVariant={labelVariant}
state={state}
disabled={disabled}
error={error}
label={label}
placeholder={placeholder}
value={value}
name={name}
className={className}
rows={rows}
containerClasses={containerClasses}
labelClasses={labelClasses}
textareaClasses={textareaClasses}
borderRadius={currentSize.radius}
handleChange={handleChange}
handleFocus={handleFocus}
handleBlur={handleBlur}
aria-invalid={error}
textHint={textHint}
formHeader={formHeader}
showHelpIcon={showHelpIcon}
appearance={appearance}
helpIconAlt={t("helpIconAlt")}
hintDefault={t("hintDefault")}
{...props}
/>
);
},
);
TextAreaContainer.displayName = "TextArea";
export default memo(TextAreaContainer);