Files
community-rule/app/components/controls/TextArea/TextArea.container.tsx
T
adilalloandCursor a820739d07 Open create-flow method modals with editable fields and tertiary default copy.
Seeded section text should look like the form default state, not locked primary, and Customize stays on the kebab instead of unlocking the body.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 12:59:53 -06:00

217 lines
7.3 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 === "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" ? "rounded-[var(--radius-300,12px)]" : "border"}
${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);