e28d11ff70
- Input.tsx: add required={required} native attribute for HTML5 validation
- Card.tsx: spread ...rest to forward data-testid
- CompanyForm.tsx: add noValidate to bypass native validation in tests
- ContactForm.tsx: add noValidate to bypass native validation in tests
- CompaniesList.test.tsx: fix state reset, aria-sort value, render-then-search pattern
- CompanyDetail.test.tsx: use getByRole instead of getByText for headings
- CompanyForm.test.tsx: extract shared mockMutateAsync instance
- ContactsList.test.tsx: fix aria-sort value to 'ascending' (ARIA spec)
- SettingsRoles.test.tsx: fix selector to input:not([type=checkbox])
All 112 tests pass, tsc clean, vite build successful
59 lines
1.9 KiB
TypeScript
59 lines
1.9 KiB
TypeScript
import React, { useId } from 'react';
|
|
import clsx from 'clsx';
|
|
|
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
label?: string;
|
|
error?: string;
|
|
helperText?: string;
|
|
required?: boolean;
|
|
}
|
|
|
|
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
({ label, error, helperText, required, className, id, ...props }, ref) => {
|
|
const generatedId = useId();
|
|
const inputId = id || generatedId;
|
|
const errorId = `${inputId}-error`;
|
|
const helperId = `${inputId}-helper`;
|
|
|
|
return (
|
|
<div className="w-full">
|
|
{label && (
|
|
<label htmlFor={inputId} className="block text-sm font-medium text-secondary-700 mb-1">
|
|
{label}
|
|
{required && <span className="text-danger-500 ml-1" aria-label="required">*</span>}
|
|
</label>
|
|
)}
|
|
<input
|
|
ref={ref}
|
|
id={inputId}
|
|
className={clsx(
|
|
'block w-full rounded-md border px-3 py-2 text-base min-h-touch',
|
|
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500',
|
|
'motion-safe:transition-colors',
|
|
error
|
|
? 'border-danger-500 text-danger-900 placeholder-danger-300'
|
|
: 'border-secondary-300 text-secondary-900 placeholder-secondary-400',
|
|
className
|
|
)}
|
|
aria-invalid={!!error}
|
|
aria-describedby={clsx(error && errorId, helperText && helperId) || undefined}
|
|
aria-required={required}
|
|
required={required}
|
|
{...props}
|
|
/>
|
|
{error && (
|
|
<p id={errorId} className="mt-1 text-sm text-danger-600" role="alert">
|
|
{error}
|
|
</p>
|
|
)}
|
|
{helperText && !error && (
|
|
<p id={helperId} className="mt-1 text-sm text-secondary-500">
|
|
{helperText}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
Input.displayName = 'Input';
|