- Add getFirstValidationError utility for nested FieldErrors extraction - Add mergeWithDefaults utilities (deepMergeWithDefaults, type guards) - Add useValidationErrorHandler hook for toast + tab navigation - Add FormSelect component with Controller integration - Add FormTextarea component with register integration - Refactor AgentTypeForm to use new utilities - Remove verbose debug logging (now handled by hook) - Add comprehensive tests (53 new tests, 100 total)
102 lines
2.8 KiB
TypeScript
102 lines
2.8 KiB
TypeScript
/**
|
|
* FormTextarea Component
|
|
*
|
|
* Reusable Textarea field for react-hook-form with register integration.
|
|
* Handles label, error display, and description automatically.
|
|
*
|
|
* @module components/forms/FormTextarea
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { ComponentProps } from 'react';
|
|
import type { FieldError, UseFormRegisterReturn } from 'react-hook-form';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
|
|
export interface FormTextareaProps extends Omit<ComponentProps<typeof Textarea>, 'children'> {
|
|
/** Field label */
|
|
label: string;
|
|
/** Field name (optional if provided via register) */
|
|
name?: string;
|
|
/** Is field required? Shows asterisk if true */
|
|
required?: boolean;
|
|
/** Form error from react-hook-form */
|
|
error?: FieldError;
|
|
/** Helper text below the field */
|
|
description?: string;
|
|
/** Register return object from useForm */
|
|
registration?: UseFormRegisterReturn;
|
|
}
|
|
|
|
/**
|
|
* FormTextarea - Textarea field for react-hook-form
|
|
*
|
|
* Automatically handles:
|
|
* - Label with required indicator
|
|
* - Error message display
|
|
* - Description/helper text
|
|
* - Accessibility attributes
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <FormTextarea
|
|
* label="Personality Prompt"
|
|
* required
|
|
* error={errors.personality_prompt}
|
|
* rows={10}
|
|
* {...register('personality_prompt')}
|
|
* />
|
|
* ```
|
|
*/
|
|
export function FormTextarea({
|
|
label,
|
|
name: explicitName,
|
|
required = false,
|
|
error,
|
|
description,
|
|
registration,
|
|
...textareaProps
|
|
}: FormTextareaProps) {
|
|
// Extract name from props or registration
|
|
const registerName =
|
|
'name' in textareaProps ? (textareaProps as { name: string }).name : undefined;
|
|
const name = explicitName || registerName || registration?.name;
|
|
|
|
if (!name) {
|
|
throw new Error('FormTextarea: name must be provided either explicitly or via register()');
|
|
}
|
|
|
|
const errorId = error ? `${name}-error` : undefined;
|
|
const descriptionId = description ? `${name}-description` : undefined;
|
|
const ariaDescribedBy = [errorId, descriptionId].filter(Boolean).join(' ') || undefined;
|
|
|
|
// Merge registration props with other props
|
|
const mergedProps = registration ? { ...registration, ...textareaProps } : textareaProps;
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label htmlFor={name}>
|
|
{label}
|
|
{required && <span className="text-destructive"> *</span>}
|
|
</Label>
|
|
{description && (
|
|
<p id={descriptionId} className="text-sm text-muted-foreground">
|
|
{description}
|
|
</p>
|
|
)}
|
|
<Textarea
|
|
id={name}
|
|
aria-invalid={!!error}
|
|
aria-describedby={ariaDescribedBy}
|
|
{...mergedProps}
|
|
/>
|
|
{error && (
|
|
<p id={errorId} className="text-sm text-destructive" role="alert">
|
|
{error.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|