PSA/packages/ui/src/components/TextArea.tsx
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

171 lines
4.5 KiB
TypeScript

'use client';
import React, { useLayoutEffect, useEffect, useRef, useCallback } from 'react';
import { FormFieldComponent, AutomationProps } from '../ui-reflection/types';
import { useAutomationIdAndRegister } from '../ui-reflection/useAutomationIdAndRegister';
import { cn } from '../lib/utils';
type TextAreaSize = 'sm' | 'md' | 'lg';
const textAreaSizeClasses: Record<TextAreaSize, string> = {
sm: 'py-1 px-2 text-xs',
md: 'py-2 px-3',
lg: 'py-3 px-4 text-base',
};
interface TextAreaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'id'> {
label?: string;
/** Unique identifier for UI reflection system */
id?: string;
/** Whether the textarea is required */
required?: boolean;
/** Ref for the textarea element */
ref?: React.Ref<HTMLTextAreaElement>;
/** Optional wrapper class overrides */
wrapperClassName?: string;
/** Size variant */
size?: TextAreaSize;
}
export function TextArea({
label,
onChange,
className,
value = '',
id,
disabled,
required,
size = 'md',
ref: forwardedRef,
wrapperClassName,
"data-automation-id": dataAutomationId,
...props
}: TextAreaProps & AutomationProps) {
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const mergedRef = useCallback(
(node: HTMLTextAreaElement | null) => {
textareaRef.current = node;
if (typeof forwardedRef === 'function') {
forwardedRef(node);
} else if (forwardedRef) {
forwardedRef.current = node;
}
},
[forwardedRef]
);
const adjustHeight = (element: HTMLTextAreaElement) => {
// Temporarily collapse to get the minimum height
element.style.height = 'auto';
// Get the computed line height to ensure proper minimum height
const computedStyle = window.getComputedStyle(element);
const lineHeight = parseInt(computedStyle.lineHeight);
// Calculate height based on content
const newHeight = Math.max(
element.scrollHeight,
lineHeight * 1.5 // Minimum height of ~1.5 lines
);
// Set the new height
element.style.height = `${newHeight}px`;
};
// Initial setup and content-based adjustment
useEffect(() => {
if (textareaRef.current) {
const element = textareaRef.current;
// Ensure proper initial display
element.style.height = 'auto';
element.style.overflow = 'hidden';
// Force a reflow and adjust height
void element.offsetHeight;
adjustHeight(element);
}
}, []);
// Handle value changes
useLayoutEffect(() => {
if (textareaRef.current) {
adjustHeight(textareaRef.current);
}
}, [value]);
// Use provided data-automation-id or register normally
const { automationIdProps: textAreaProps, updateMetadata } = useAutomationIdAndRegister<FormFieldComponent>({
type: 'formField',
fieldType: 'textField',
id,
label,
value: typeof value === 'string' ? value : undefined,
disabled,
required
}, true, dataAutomationId);
// Always use the generated automation props (which include our override ID if provided)
const finalAutomationProps = textAreaProps;
// Update metadata when field props change
useEffect(() => {
if (updateMetadata && typeof value === 'string') {
updateMetadata({
value,
label,
disabled,
required
});
}
}, [value, updateMetadata, label, disabled, required]);
const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (textareaRef.current) {
adjustHeight(textareaRef.current);
}
if (onChange) {
onChange(e);
}
};
return (
<div className={cn('mb-4 px-0.5', wrapperClassName)}>
{label && (
<label className="block text-sm font-medium text-[rgb(var(--color-text-700))] mb-1">
{label}
</label>
)}
<textarea
ref={mergedRef}
rows={1}
className={`
w-full max-w-4xl
${textAreaSizeClasses[size]}
border
border-[rgb(var(--color-border-400))]
rounded-md
shadow-sm
focus:outline-none
focus:ring-2
focus:ring-[rgb(var(--color-primary-500))]
focus:border-transparent
resize-none
overflow-hidden
whitespace-pre-wrap break-words
placeholder:text-[rgb(var(--color-text-400))]
${className}
`}
onChange={handleInput}
value={value}
disabled={disabled}
required={required}
{...finalAutomationProps}
{...props}
/>
</div>
);
}