85 lines
1.8 KiB
TypeScript
85 lines
1.8 KiB
TypeScript
import { Asterisk } from 'lucide-react';
|
|
import { useRef } from 'react';
|
|
import { type AriaTextFieldProps, useId, useTextField } from 'react-aria';
|
|
import cn from '~/utils/cn';
|
|
|
|
export interface InputProps extends AriaTextFieldProps<HTMLInputElement> {
|
|
label: string;
|
|
labelHidden?: boolean;
|
|
isRequired?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
// TODO: Custom isInvalid logic for custom error messages
|
|
export default function Input(props: InputProps) {
|
|
const { label, labelHidden, className } = props;
|
|
const ref = useRef<HTMLInputElement | null>(null);
|
|
const id = useId(props.id);
|
|
|
|
const {
|
|
labelProps,
|
|
inputProps,
|
|
descriptionProps,
|
|
errorMessageProps,
|
|
isInvalid,
|
|
validationErrors,
|
|
} = useTextField(
|
|
{
|
|
...props,
|
|
label,
|
|
'aria-label': label,
|
|
},
|
|
ref,
|
|
);
|
|
|
|
return (
|
|
<div className="flex flex-col w-full" aria-label={label}>
|
|
<label
|
|
{...labelProps}
|
|
htmlFor={id}
|
|
className={cn(
|
|
'text-xs font-medium px-3 mb-0.5',
|
|
'text-headplane-700 dark:text-headplane-100',
|
|
labelHidden && 'sr-only',
|
|
)}
|
|
>
|
|
{label}
|
|
{props.isRequired && (
|
|
<Asterisk className="inline w-3.5 text-red-500 pb-1 ml-0.5" />
|
|
)}
|
|
</label>
|
|
<input
|
|
{...inputProps}
|
|
required={props.isRequired}
|
|
ref={ref}
|
|
className={cn(
|
|
'rounded-xl px-3 py-2',
|
|
'focus:outline-none focus:ring',
|
|
'bg-white dark:bg-headplane-900',
|
|
'border border-headplane-100 dark:border-headplane-800',
|
|
className,
|
|
)}
|
|
/>
|
|
{props.description && (
|
|
<div
|
|
{...descriptionProps}
|
|
className={cn(
|
|
'text-xs px-3 mt-1',
|
|
'text-headplane-500 dark:text-headplane-400',
|
|
)}
|
|
>
|
|
{props.description}
|
|
</div>
|
|
)}
|
|
{isInvalid && (
|
|
<div
|
|
{...errorMessageProps}
|
|
className={cn('text-xs px-3 mt-1', 'text-red-500 dark:text-red-400')}
|
|
>
|
|
{validationErrors.join(' ')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|