headplane/app/components/Input.tsx

85 lines
1.8 KiB
TypeScript
Raw Normal View History

import { Asterisk } from 'lucide-react';
2025-01-24 08:17:12 -05:00
import { useRef } from 'react';
2025-01-28 16:04:42 -05:00
import { type AriaTextFieldProps, useId, useTextField } from 'react-aria';
2025-01-24 08:17:12 -05:00
import cn from '~/utils/cn';
export interface InputProps extends AriaTextFieldProps<HTMLInputElement> {
2025-02-04 11:27:29 -05:00
label: string;
labelHidden?: boolean;
2025-01-24 08:17:12 -05:00
isRequired?: boolean;
2025-01-28 16:04:42 -05:00
className?: string;
2025-01-24 08:17:12 -05:00
}
// TODO: Custom isInvalid logic for custom error messages
2025-01-24 08:17:12 -05:00
export default function Input(props: InputProps) {
2025-02-04 11:27:29 -05:00
const { label, labelHidden, className } = props;
2025-01-24 08:17:12 -05:00
const ref = useRef<HTMLInputElement | null>(null);
2025-01-28 16:04:42 -05:00
const id = useId(props.id);
2025-01-24 08:17:12 -05:00
const {
labelProps,
inputProps,
descriptionProps,
errorMessageProps,
isInvalid,
validationErrors,
2025-02-04 11:27:29 -05:00
} = useTextField(
{
...props,
label,
'aria-label': label,
},
ref,
);
2025-01-24 08:17:12 -05:00
return (
2025-02-04 11:27:29 -05:00
<div className="flex flex-col w-full" aria-label={label}>
2025-01-24 08:17:12 -05:00
<label
{...labelProps}
2025-01-28 16:04:42 -05:00
htmlFor={id}
2025-01-24 08:17:12 -05:00
className={cn(
'text-xs font-medium px-3 mb-0.5',
'text-headplane-700 dark:text-headplane-100',
2025-02-04 11:27:29 -05:00
labelHidden && 'sr-only',
2025-01-24 08:17:12 -05:00
)}
>
{label}
{props.isRequired && (
<Asterisk className="inline w-3.5 text-red-500 pb-1 ml-0.5" />
)}
2025-01-24 08:17:12 -05:00
</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',
2025-01-28 16:04:42 -05:00
className,
2025-01-24 08:17:12 -05:00
)}
/>
{props.description && (
<div
{...descriptionProps}
className={cn(
'text-xs px-3 mt-1',
'text-headplane-500 dark:text-headplane-400',
)}
>
{props.description}
</div>
)}
2025-04-22 09:51:03 -04:00
{isInvalid ? (
2025-01-24 08:17:12 -05:00
<div
{...errorMessageProps}
className={cn('text-xs px-3 mt-1', 'text-red-500 dark:text-red-400')}
>
{validationErrors.join(' ')}
</div>
2025-04-22 09:51:03 -04:00
) : null}
2025-01-24 08:17:12 -05:00
</div>
);
}