import { useEffect, useRef, useState } from 'react'; import type { ChangeEvent, ClipboardEvent, KeyboardEvent } from 'react'; import type { Promisable } from 'type-fest'; import { useNotificationsStore, useTranslation } from '#hooks'; import { cn } from '#utils'; const CODE_LENGTH = 6; const EMPTY_CODE = Object.freeze(Array(CODE_LENGTH).fill(null)); type OneTimePasswordInputProps = { [key: `data-${string}`]: unknown; className?: string; onComplete: (code: number) => Promisable; }; function getUpdatedDigits(digits: (null | number)[], index: number, value: null | number) { const updatedDigits = [...digits]; updatedDigits[index] = value; return updatedDigits; } export const OneTimePasswordInput = ({ className, onComplete, ...props }: OneTimePasswordInputProps) => { const notifications = useNotificationsStore(); const { t } = useTranslation('libui'); const [digits, setDigits] = useState<(null | number)[]>([...EMPTY_CODE]); const inputRefs = digits.map(() => useRef(null)); useEffect(() => { const isComplete = digits.every((value) => Number.isInteger(value)); if (isComplete) { void onComplete(parseInt(digits.join(''))); setDigits([...EMPTY_CODE]); } }, [digits]); const focusNext = (index: number) => inputRefs[index + 1 === digits.length ? 0 : index + 1]?.current?.focus(); const focusPrev = (index: number) => inputRefs[index - 1 >= 0 ? index - 1 : digits.length - 1]?.current?.focus(); const handleChange = (e: ChangeEvent, index: number) => { let value: null | number; if (e.target.value === '') { value = null; } else if (Number.isInteger(parseInt(e.target.value))) { value = parseInt(e.target.value); } else { return; } setDigits((prevDigits) => getUpdatedDigits(prevDigits, index, value)); focusNext(index); }; const handleKeyDown = (e: KeyboardEvent, index: number) => { switch (e.key) { case 'ArrowLeft': focusPrev(index); break; case 'ArrowRight': focusNext(index); break; case 'Backspace': setDigits((prevDigits) => getUpdatedDigits(prevDigits, index - 1, null)); focusPrev(index); } }; const handlePaste = (e: ClipboardEvent) => { e.preventDefault(); const pastedDigits = e.clipboardData .getData('text/plain') .split('') .slice(0, CODE_LENGTH) .map((value) => parseInt(value)); const isValid = pastedDigits.length === CODE_LENGTH && pastedDigits.every((value) => Number.isInteger(value)); if (isValid) { setDigits(pastedDigits); } else { notifications.addNotification({ message: t('oneTimePasswordInput.invalidCodeFormat'), type: 'warning' }); } }; return (
{digits.map((_, index) => ( { handleChange(e, index); }} onKeyDown={(e) => { handleKeyDown(e, index); }} onPaste={handlePaste} /> ))}
); };