// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file TextArea.tsx * @input Uses React, useId, ChangeEvent, ClipboardEvent, FocusEvent, Field, Icon, Spinner * @output Exports TextArea component, TextAreaProps, TextAreaStatus, TextAreaStatusType * @position Core implementation; consumed by index.ts, tested by TextArea.test.tsx * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/TextArea/TextArea.doc.mjs (props table, features, implementation notes) * - /packages/core/src/TextArea/TextArea.test.tsx (tests for new/changed behavior) * - /packages/core/src/TextArea/index.ts (exports if types change) * - /apps/storybook/stories/TextArea.stories.tsx (storybook stories) * - /packages/cli/assets/templates/blocks/components/TextArea/ (showcase blocks) */ import { useId, useOptimistic, useTransition, useRef, useCallback, useMemo, type ChangeEvent, type ClipboardEvent, type FocusEvent, type ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { colorVars, spacingVars, typographyVars, typeScaleVars } from "../theme/tokens.stylex.js"; import { Field, inputWrapperStyles, inputStatusBorderStyles, inputStatusHoverShadowStyles, inputStatusFocusWithinStyles, type FieldStatusVariant } from "../Field/index.js"; import { Icon, renderIconSlot, type IconType } from "../Icon/index.js"; import { Spinner } from "../Spinner/index.js"; import { useTooltip } from "../Tooltip/index.js"; import { mergeProps } from "../utils/index.js"; import type { BaseProps } from "../BaseProps.js"; import type { SizeValue } from "../utils/types.js"; import { useInputContainer } from "../hooks/useInputContainer.js"; import { useInputStatusIcon } from "../hooks/useInputStatusIcon.js"; import { useAnnounce } from "../hooks/useAnnounce.js"; import { useResolvedRequired } from "../hooks/useResolvedRequired.js"; import { useSize } from "../SizeContext/SizeContext.js"; import { themeProps } from "../utils/themeProps.js"; import { characterCount } from "../utils/characters.js"; import { useTranslator } from "../i18n/index.js"; import { useMergedRefs } from "../hooks/useMergedRefs.js"; const COUNTER_WARNING_THRESHOLD = 0.8; const styles = { wrapper: { kY2c9j: "x1vjfegm", k1xSpc: "x1lliihq", k8WAf4: "xt970qd", kg3NbH: "xnjsko4", "--_textarea-inline-padding": "x1kdpo6e", $$css: true as const }, textarea: { k1xSpc: "x1lliihq", kzqmXN: "xh8yej3", kB7OPa: "x9f619", k7Eaqz: "xeuugli", kMzoRj: "xc342km", ksu8eU: "xng3xce", k8WAf4: "xu0wf1k", kg3NbH: "x1w6f3ag", kMv6JI: "x9ynric", kGuDYH: "xjm74w1 xf9zqsd", kLWn49: "xw6l6zx", kMwMTN: "x1tgivj0", kWkggS: "xjbqb8w", kI3sdo: "x1a2a7pz", k8Qsv1: "xeyghm5", kHenm0: "x288g5", $$css: true as const }, textareaDisabled: { kkrTdU: "xt0e3qv", $$css: true as const }, textareaWithStartIcon: { kZCmMZ: "x1b6te9z", $$css: true as const }, textareaWithStatus: { kwRFfy: "xp2dy4z", $$css: true as const }, textareaWithBusyStatus: { kwRFfy: "x1g2apga", $$css: true as const }, textareaWithCounter: { kGO01o: "xekm6fs", $$css: true as const } }; const textareaSizeStyles = { sm: { $$css: true as const }, md: { $$css: true as const }, lg: { k8WAf4: "xce4md1", $$css: true as const } }; export type TextAreaStatusType = 'warning' | 'error' | 'success'; export type TextAreaSize = 'sm' | 'md' | 'lg'; export interface TextAreaStatus { /** * The type of status to display. */ type: TextAreaStatusType; /** * Optional message to display below the textarea. */ message?: string; } export interface TextAreaProps extends Omit { /** Ref forwarded to the root element */ ref?: React.Ref; /** * Label text for the textarea (always rendered for accessibility). */ label: string; /** * Whether to visually hide the label (still accessible to screen readers). * @default false */ isLabelHidden?: boolean; /** * Description text displayed between the label and textarea. */ description?: string; /** * Whether the field is optional. Mutually exclusive with isRequired. * @default false */ isOptional?: boolean; /** * Whether the field is required. Mutually exclusive with isOptional. * @default false */ isRequired?: boolean; /** * Callback fired when the textarea value changes. */ onChange?: (value: string, e: ChangeEvent) => void; /** Async action on change. Fires after onChange if not prevented. */ changeAction?: (value: string, e: ChangeEvent) => void | Promise; /** Whether the input is in a loading state. @default false */ isLoading?: boolean; /** * The current value of the textarea. */ value: string; /** * Placeholder text shown when the textarea is empty. */ placeholder?: string; /** * The number of visible text rows. * @default 3 */ rows?: number; /** * Whether the textarea is disabled. * @default false */ isDisabled?: boolean; /** * Whether the textarea is read-only. * The value is shown at full opacity and still submits with the form, but * cannot be edited. Unlike `isDisabled`, a read-only textarea is not dimmed * and stays in the tab order — use it for a value the user should see and * send but not change. `isDisabled` takes precedence when both are set. * @default false */ isReadOnly?: boolean; /** * Explains why the textarea is disabled. When set together with * `isDisabled`, the textarea shows a tooltip with this text on hover and * keyboard focus, and stays focusable (via `aria-disabled`) so the reason is * discoverable by keyboard and assistive technology. The field cannot be * edited (it becomes read-only) while disabled. * * Use this instead of wrapping a disabled textarea in `Tooltip` — disabled * controls don't emit the pointer events an external tooltip needs. * * @example * ``` *