import React, { DetailedHTMLProps, InputHTMLAttributes, LabelHTMLAttributes, RefObject, TextareaHTMLAttributes, } from "react"; import { twMerge } from "tailwind-merge"; import Span from "../layout/Span"; let timeout: any; const autocompleteOptions = [ // Personal Information "name", "honorific-prefix", "given-name", "additional-name", "family-name", "honorific-suffix", "nickname", // Contact Information "email", "username", "new-password", "current-password", "one-time-code", "organization-title", "organization", // Address Fields "street-address", "address-line1", "address-line2", "address-line3", "address-level4", "address-level3", "address-level2", "address-level1", "country", "country-name", "postal-code", // Phone Numbers "tel", "tel-country-code", "tel-national", "tel-area-code", "tel-local", "tel-extension", // Dates "bday", "bday-day", "bday-month", "bday-year", // Payment Information "cc-name", "cc-given-name", "cc-additional-name", "cc-family-name", "cc-number", "cc-exp", "cc-exp-month", "cc-exp-year", "cc-csc", "cc-type", // Additional Options "sex", "url", "photo", // Special Values "on", // Enables autofill (default) "off", // Disables autofill ] as const; export type InputProps = DetailedHTMLProps< InputHTMLAttributes, HTMLInputElement > & DetailedHTMLProps< TextareaHTMLAttributes, HTMLTextAreaElement > & { label?: string; variant?: "normal" | "warning" | "error" | "inactive"; prefix?: string | React.ReactNode; suffix?: string | React.ReactNode; showLabel?: boolean; istextarea?: boolean; wrapperProps?: DetailedHTMLProps< InputHTMLAttributes, HTMLDivElement >; labelProps?: DetailedHTMLProps< LabelHTMLAttributes, HTMLLabelElement >; componentRef?: RefObject; validationRegex?: RegExp; debounce?: number; invalidMessage?: string; validationFunction?: (value: string) => Promise; autoComplete?: (typeof autocompleteOptions)[number]; name?: KeyType; }; /** * # Input Element * @className twui-input * @className twui-input-wrapper * @className twui-input-invalid */ export default function Input({ label, variant, prefix, suffix, componentRef, labelProps, wrapperProps, showLabel, istextarea, debounce, invalidMessage, autoComplete, validationFunction, validationRegex, ...props }: InputProps) { const [focus, setFocus] = React.useState(false); const [value, setValue] = React.useState( props.defaultValue ? String(props.defaultValue) : "" ); delete props.defaultValue; const [isValid, setIsValid] = React.useState(true); const DEFAULT_DEBOUNCE = 500; const finalDebounce = debounce || DEFAULT_DEBOUNCE; React.useEffect(() => { if (!value.match(/./)) return setIsValid(true); window.clearTimeout(timeout); if (validationRegex) { timeout = setTimeout(() => { setIsValid(validationRegex.test(value)); }, finalDebounce); } if (validationFunction) { timeout = setTimeout(() => { validationFunction(value).then((res) => { setIsValid(res); }); }, finalDebounce); } }, [value]); const targetComponent = istextarea ? ( { setFocus(true); props?.onFocus?.(e); }} onBlur={(e) => { setFocus(false); props?.onBlur?.(e); }} value={value} onChange={(e) => setValue(e.target.value)} autoComplete={autoComplete} rows={props.height ? Number(props.height) : 4} /> ) : ( { setFocus(true); props?.onFocus?.(e); }} onBlur={(e) => { setFocus(false); props?.onBlur?.(e); }} value={value} onChange={(e) => { setValue(e.target.value); props?.onChange?.(e); }} /> ); return ( {showLabel && ( {label || props.placeholder || props.name} )} {prefix && ( {prefix} )} {targetComponent} {suffix && ( {suffix} )} {!isValid && ( {invalidMessage || "Invalid"} )} ); }