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>
217 lines
7.3 KiB
TypeScript
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);
|