import { type ChangeEvent, type InputHTMLAttributes, type ForwardedRef, type ComponentProps, useCallback, forwardRef, } from 'react'; import { useDebounce } from '../hooks/useDebounce'; import { cn } from '@/styles/theme'; type TextInputProps = Omit, 'onChange'> & { delayMs?: number; /** specify 'decimal' to trigger numeric keyboards on mobile devices */ inputMode?: InputHTMLAttributes['inputMode']; onChange: (s: string) => void; placeholder: string; setValue?: (s: string) => void; inputValidator?: (s: string) => boolean; /** specify 'error' to show error state (change in color), field is used for a11y purposes, not actually rendered currently, can be either boolean flag or string error message */ error?: string | boolean; }; export const TextInput = forwardRef( ( { 'aria-label': ariaLabel, className, delayMs = 0, disabled = false, onBlur, onChange, onFocus, placeholder, setValue, inputMode, value, inputValidator = () => true, error, ...rest }: TextInputProps, ref: ForwardedRef, ) => { const handleDebounce = useDebounce((value: string) => { onChange(value); }, delayMs); const handleChange = useCallback( (evt: ChangeEvent) => { const value = evt.target.value; if (inputValidator(value)) { setValue?.(value); if (delayMs > 0) { handleDebounce(value); } else { onChange(value); } } }, [onChange, handleDebounce, delayMs, setValue, inputValidator], ); return ( ); }, ); TextInput.displayName = 'TextInput';