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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
171 lines
4.5 KiB
TypeScript
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>
|
|
);
|
|
}
|