import type { FormEventHandler } from 'react'; import { useCallback, useEffect, useMemo } from 'react'; import { useFieldArray, useForm } from 'react-hook-form'; import { Input } from '../shadcn/input'; const DIGITS = 6; export function OtpInput({ onValid, onInvalid, }: React.PropsWithChildren<{ onValid: (code: string) => void; onInvalid: () => void; }>) { const digitsArray = useMemo( () => Array.from({ length: DIGITS }, (_, i) => i), [], ); const { control, register, watch, setFocus, formState, setValue } = useForm({ mode: 'onChange', reValidateMode: 'onChange', defaultValues: { values: digitsArray.map(() => ({ value: '' })), }, }); useFieldArray({ control, name: 'values', shouldUnregister: true, }); const { values } = watch(); const isFormValid = formState.isValid; const code = (values ?? []).map(({ value }) => value).join(''); useEffect(() => { if (!isFormValid) { onInvalid(); return; } if (code.length === DIGITS) { onValid(code); return; } onInvalid(); }, [onInvalid, onValid, code, isFormValid]); useEffect(() => { setFocus('values.0.value'); }, [setFocus]); const onInput: FormEventHandler = useCallback( (target) => { const element = target.currentTarget; const isValid = element.reportValidity(); if (isValid) { const nextIndex = Number(element.dataset.index) + 1; if (nextIndex >= DIGITS) { return; } setFocus(`values.${nextIndex}.value`); } }, [setFocus], ); const onPaste = useCallback( (event: React.ClipboardEvent) => { const pasted = event.clipboardData.getData('text/plain'); // check if value is numeric if (isNumeric(pasted)) { const digits = getDigits(pasted, digitsArray); digits.forEach((value, index) => { setValue(`values.${index}.value`, value); setFocus(`values.${index + 1}.value`); }); } }, [digitsArray, setFocus, setValue], ); const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { if (event.key === 'Backspace') { event.preventDefault(); const index = Number(event.currentTarget.dataset.inputIndex); setValue(`values.${index}.value`, ''); setFocus(`values.${index - 1}.value`); } }, [setFocus, setValue], ); return (
{digitsArray.map((digit, index) => { const control = { ...register(`values.${digit}.value`) }; return ( ); })}
); } function isNumeric(pasted: string) { const isNumericRegExp = /^-?\d+$/; return isNumericRegExp.test(pasted); } function getDigits(pasted: string, digitsArray: number[]) { return pasted.split('').slice(0, digitsArray.length); }