veza/apps/web/src/components/ui/FormField.tsx

218 lines
4.7 KiB
TypeScript
Raw Normal View History

import React from 'react';
import { cn } from '@/lib/utils';
import { Input as DesignSystemInput } from './input';
import { Textarea as DesignSystemTextarea } from './textarea';
import { Select as DesignSystemSelect } from './select';
/**
* FormFieldProps - Propriétés du composant FormField
*
* @interface FormFieldProps
*/
interface FormFieldProps {
/**
* Label du champ de formulaire
*
* @example
* ```tsx
* <FormField label="Email">
* <Input type="email" />
* </FormField>
* ```
*/
label: string;
/**
* Message d'erreur à afficher sous le champ
*
* @example
* ```tsx
* <FormField label="Email" error={errors.email}>
* <Input type="email" />
* </FormField>
* ```
*/
error?: string;
/**
* Si `true`, affiche un indicateur requis (*)
*
* @default false
*/
required?: boolean;
/**
* Champ de formulaire enfant (Input, Textarea, Select, etc.)
*/
children: React.ReactNode;
/**
* Classes CSS personnalisées
*/
className?: string;
/**
* Texte d'aide à afficher sous le champ (si pas d'erreur)
*
* @example
* ```tsx
* <FormField
* label="Mot de passe"
* helpText="Minimum 8 caractères"
* >
* <Input type="password" />
* </FormField>
* ```
*/
helpText?: string;
}
/**
* FormField - Composant de champ de formulaire avec label et validation
*
* Composant wrapper pour les champs de formulaire avec support pour :
* - Label avec indicateur requis
* - Message d'erreur
* - Texte d'aide
*
* @example
* ```tsx
* // Champ simple
* <FormField label="Nom">
* <Input type="text" />
* </FormField>
*
* // Champ requis avec erreur
* <FormField
* label="Email"
* required
* error={errors.email}
* >
* <Input type="email" />
* </FormField>
*
* // Champ avec texte d'aide
* <FormField
* label="Description"
* helpText="Maximum 500 caractères"
* >
* <Textarea />
* </FormField>
* ```
*
* @component
* @param {FormFieldProps} props - Propriétés du composant
* @returns {JSX.Element} Élément div contenant le label, le champ et les messages
*/
export const FormField: React.FC<FormFieldProps> = ({
label,
error,
required = false,
children,
className,
helpText,
}) => {
return (
<div className={cn('space-y-2', className)}>
<label className="text-sm font-medium text-kodo-text-main dark:text-kodo-text-main">
{label}
{required && <span className="text-destructive ml-1">*</span>}
</label>
{children}
{helpText && !error && (
<p className="text-xs text-muted-foreground dark:text-muted-foreground">{helpText}</p>
)}
{error && (
<p className="text-xs text-destructive dark:text-destructive">{error}</p>
)}
</div>
);
};
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
error?: boolean;
}
2025-12-13 02:34:34 +00:00
export const Input: React.FC<InputProps> = ({
error = false,
className,
...props
}) => {
return (
<DesignSystemInput
className={cn(
error && 'border-kodo-red focus-visible:border-kodo-red',
2025-12-13 02:34:34 +00:00
className,
)}
{...props}
/>
);
};
2025-12-13 02:34:34 +00:00
interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
error?: boolean;
}
2025-12-13 02:34:34 +00:00
export const Textarea: React.FC<TextareaProps> = ({
error = false,
className,
...props
}) => {
return (
<DesignSystemTextarea
className={cn(
error && 'border-kodo-red focus-visible:border-kodo-red',
2025-12-13 02:34:34 +00:00
className,
)}
{...props}
/>
);
};
interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'onChange'> {
error?: boolean;
options: Array<{ value: string; label: string }>;
placeholder?: string;
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
}
2025-12-13 02:34:34 +00:00
export const Select: React.FC<SelectProps> = ({
error = false,
className,
options,
value,
onChange,
placeholder,
disabled,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
}) => {
const selectValue = value as string | undefined;
const handleChange = (newValue: string | string[]) => {
if (onChange) {
const event = {
target: { value: Array.isArray(newValue) ? newValue[0] : newValue },
} as React.ChangeEvent<HTMLSelectElement>;
onChange(event);
}
};
return (
<DesignSystemSelect
options={options.map((opt) => ({ value: opt.value, label: opt.label }))}
value={selectValue}
onChange={handleChange}
disabled={disabled}
placeholder={placeholder || 'Select an option...'}
className={cn(
error && 'border-kodo-red',
2025-12-13 02:34:34 +00:00
className,
)}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
/>
);
};