import clsx from "clsx"; import * as React from "react"; import { useGetSet } from "../../hooks"; import { useGetKey } from "../../hooks/useGetKey"; import { useDebugEvents } from "../../utils"; import { composeEventHandlers } from "../../utils/composeEvents"; import { useComposedRefs } from "../../utils/mergeRefs"; import * as styles from "./styles.module.css"; export interface InputProps extends Omit, "size"> { "data-id": string; type: MODES; "data-label"?: string; hideControls?: boolean; size?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl"; variant?: "subtle" | "outline" | "flushed"; } export enum MODES { text = "text", number = "number", email = "email", password = "password", phone = "tel", date = "date", "datetime-local" = "datetime-local", time = "time", search = "search", hidden = "hidden", url = "url", otp = "one-time-code", } function getTypeProps( type: MODES = MODES.text, ): Partial> { switch (type) { case MODES.number: return { type: "number", pattern: "[0-9]*", inputMode: "numeric", }; case MODES.otp: return { type: "text", pattern: "[0-9]*", inputMode: "numeric", autoComplete: "one-time-code", }; case MODES.email: return { type: "email", inputMode: "email", }; case MODES.password: return { type: "password", autoComplete: "current-password", }; default: return { type, }; } } export const Input = React.forwardRef(function Input( { defaultValue, onChange, onFocus, onBlur, size = "md", variant = "outline", ...rest }: InputProps, ref: React.ForwardedRef, ) { const inputRef = React.useRef(null); const key = useGetKey(rest); const initialValue = React.useMemo( () => ({ value: defaultValue as string }), [defaultValue], ); const [{ value }, setState] = useGetSet<{ value: string; focused?: boolean }>( key, initialValue, ); const { className, onChange: handleChange, type, ...props } = useDebugEvents>( Object.assign(rest, { onFocus: composeEventHandlers(() => { setState( { focused: true }, process.env.PREVIEW ? `onFocus` : undefined, ); }, onFocus), onChange: composeEventHandlers( (event: React.ChangeEvent) => { const value = event.target.value; setState({ value }, process.env.PREVIEW ? `onChange` : undefined); }, onChange, ), onBlur: composeEventHandlers(() => { setState( { focused: false }, process.env.PREVIEW ? `onBlur` : undefined, ); }, onBlur), }), ); React.useEffect(() => { const parentForm = inputRef.current?.form; const handler = () => { setState({ value: defaultValue as string }); }; parentForm?.addEventListener("reset", handler); return () => { parentForm?.removeEventListener("reset", handler); }; }, []); const finalRef = useComposedRefs(inputRef, ref); return ( ); }); export const NumberInput = React.forwardRef(function NumberInput( { defaultValue, hideControls, onChange, onFocus, onBlur, size = "md", variant = "outline", ...rest }: InputProps, ref: React.ForwardedRef, ) { const inputRef = React.useRef(null); const key = useGetKey(rest); const initialValue = React.useMemo( () => ({ value: defaultValue as number }), [defaultValue], ); const [{ value }, setState] = useGetSet<{ value: number | ""; focused?: boolean; }>(key, initialValue); const { className, onChange: handleChange, ...props } = useDebugEvents>( Object.assign(rest, { onFocus: composeEventHandlers(() => { setState( { focused: true }, process.env.PREVIEW ? `onFocus` : undefined, ); }, onFocus), onChange: composeEventHandlers( (event: React.ChangeEvent) => { const value = event.target.valueAsNumber; setState( { value: isNaN(value) ? "" : value }, process.env.PREVIEW ? `onChange` : undefined, ); }, onChange, ), onBlur: composeEventHandlers(() => { setState( { focused: false }, process.env.PREVIEW ? `onBlur` : undefined, ); }, onBlur), }), ); React.useEffect(() => { const parentForm = inputRef.current?.form; const handler = () => { setState({ value: defaultValue as number }); }; parentForm?.addEventListener("reset", handler); return () => { parentForm?.removeEventListener("reset", handler); }; }, []); const finalRef = useComposedRefs(inputRef, ref); return ( ); }); function isValidDate(date: any) { return date instanceof Date && !isNaN(date.getTime()); } const formatDate = (value?: Date | null, type: MODES = MODES.date): string => { if (!value) return ""; switch (type) { case MODES.time: return value && isValidDate(value) ? new Date( Date.UTC( value.getFullYear(), value.getMonth(), value.getDate(), value.getHours(), value.getMinutes(), 0, 0, ), ) .toISOString() .slice(11, 16) : ""; case MODES["datetime-local"]: return value && isValidDate(value) ? new Date( Date.UTC( value.getFullYear(), value.getMonth(), value.getDate(), value.getHours(), value.getMinutes(), 0, 0, ), ) .toISOString() .slice(0, 16) : ""; case MODES.date: default: return value && isValidDate(value) ? new Date( Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()), ) .toISOString() .slice(0, 10) : ""; } }; const dateRE = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/; const datetimeRE = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9])$/; const timeRE = /^([01][0-9]|2[0-3]):([0-5][0-9])$/; export const DateInput = React.forwardRef(function DateInput( { defaultValue, onChange, onFocus, onBlur, size = "md", variant = "outline", ...rest }: InputProps, ref: React.ForwardedRef, ) { const key = useGetKey(rest); const inputRef = React.useRef(null); const isUserInputRef = React.useRef(false); // Add this ref to track user input const initialValue = React.useMemo(() => { return { value: defaultValue && typeof defaultValue === "string" ? new Date(defaultValue) : defaultValue != null ? (defaultValue as unknown as Date) : null, }; }, [defaultValue]); const [{ value }, setState] = useGetSet<{ value: Date | null; focused?: boolean; }>(key, initialValue); const [strValue, setStrValue] = React.useState(() => formatDate(value, rest.type ?? MODES.date), ); const { className, onChange: handleChange, type = MODES.date, ...props } = useDebugEvents>( Object.assign(rest, { onFocus: composeEventHandlers(() => { setState( { focused: true }, process.env.PREVIEW ? `onFocus` : undefined, ); }, onFocus), onChange: (event: React.ChangeEvent) => { const valueStr = event.target.value; isUserInputRef.current = true; // Mark as user input setStrValue(valueStr); switch (type) { case MODES.date: if (dateRE.test(valueStr)) { const isoDate = event.target.valueAsDate; if (!isoDate) { return; } const year = isoDate.getUTCFullYear(); if (year < 1000 || year > 9999) { return; } setState( { value: new Date( year, isoDate.getUTCMonth(), isoDate.getUTCDate(), ), }, process.env.PREVIEW ? `onChange` : undefined, ); onChange?.(event); } break; case MODES["datetime-local"]: if (datetimeRE.test(valueStr)) { const isoDate = new Date(`${valueStr}:00.000Z`); setState( { value: new Date( isoDate.getFullYear(), isoDate.getUTCMonth(), isoDate.getUTCDate(), isoDate.getUTCHours(), isoDate.getUTCMinutes(), 0, 0, ), }, process.env.PREVIEW ? `onChange` : undefined, ); onChange?.(event); } break; case MODES.time: if (timeRE.test(valueStr)) { const dateStr = ( value && isValidDate(value) ? new Date(value) : new Date() ).toISOString(); const isoStr = `${dateStr.slice(0, 10)}T${valueStr}:00.000Z`; const isoDate = new Date(isoStr); setState( { value: new Date( isoDate.getFullYear(), isoDate.getUTCMonth(), isoDate.getUTCDate(), isoDate.getUTCHours(), isoDate.getUTCMinutes(), 0, 0, ), }, process.env.PREVIEW ? `onChange` : undefined, ); onChange?.(event); } break; default: return; } }, onBlur: composeEventHandlers(() => { setState( { focused: false }, process.env.PREVIEW ? `onBlur` : undefined, ); }, onBlur), }), ); // Only update strValue from external value changes, not user input React.useEffect(() => { if (isUserInputRef.current) { isUserInputRef.current = false; // Reset the flag return; } const formatted = formatDate(value, type); if (inputRef.current?.value !== formatted) { setStrValue(formatted); } }, [value, type]); React.useEffect(() => { const parentForm = inputRef.current?.form; const handler = () => { setState({ value: defaultValue && typeof defaultValue === "string" ? new Date(defaultValue) : (defaultValue as unknown as Date), }); }; parentForm?.addEventListener("reset", handler); return () => { parentForm?.removeEventListener("reset", handler); }; }, []); const finalRef = useComposedRefs(inputRef, ref); return ( ); });