'use client'; import React, { ChangeEvent, ClipboardEvent, FocusEvent, KeyboardEvent, forwardRef, useCallback, useImperativeHandle, useRef, useState, } from 'react'; import { Input } from '../index.js'; import { styles as passCodeStyles } from './pass-code.styles.js'; import { PassCodeProps, PassCodeRef } from './pass-code.types.js'; export const PassCode = forwardRef( ( { length, value, onChange, onComplete, onPasteComplete, className, type = 'alphanumeric', onBlur, ...props }, ref, ) => { const [internalPasscode, setInternalPasscode] = useState(Array.from({ length }).map(() => '')); const passcode = value ? value : internalPasscode; const inputRefs = useRef<(HTMLInputElement | null)[]>([]); const styles = passCodeStyles({}); useImperativeHandle(ref, () => ({ focus: () => { inputRefs.current[0]?.focus(); }, clear: () => { setInternalPasscode(Array.from({ length }).map(() => '')); }, })); const handleChange = useCallback( (index: number, event: ChangeEvent) => { const inputValue = event.target.value.slice(-1); if ( (type === 'numbers' && /^\d$/.test(inputValue)) || (type === 'letters' && /^[a-zA-Z]$/.test(inputValue)) || (type === 'alphanumeric' && /^[a-zA-Z0-9]$/.test(inputValue)) ) { const newPasscode = [...passcode.slice(0, index), inputValue, ...passcode.slice(index + 1)]; if (onChange) { onChange(newPasscode); } else { setInternalPasscode(newPasscode); } // Move to the next input if available if (index < length - 1 && inputValue !== '') { inputRefs.current[index + 1]?.focus(); } if (newPasscode.filter(passcode => !passcode).length === 0 && onComplete) { onComplete(newPasscode.join('')); } } }, [passcode, length, onChange, onComplete, type], ); const handlePaste = useCallback( (index: number, event: ClipboardEvent) => { event.preventDefault(); const pastedData = event.clipboardData.getData('text'); const validData = pastedData .slice(0, length - index) .split('') .filter(char => { if (type === 'numbers') return /^\d$/.test(char); if (type === 'letters') return /^[a-zA-Z]$/.test(char); return /^[a-zA-Z0-9]$/.test(char); }); const previousSlice = passcode.slice(0, index); const afterSlice = passcode.slice(index); const newPasscode = [...previousSlice, ...[...validData, ...afterSlice.slice(validData.length)]].slice( 0, length, ); if (onChange) { onChange(newPasscode); } else { setInternalPasscode(newPasscode); } if (newPasscode.filter(passcode => !passcode).length === 0) { if (onPasteComplete) { onPasteComplete(newPasscode.join('')); } else if (onComplete) { onComplete(newPasscode.join('')); } } }, [passcode, length, onChange, onComplete, onPasteComplete, type], ); const handleKeyDown = useCallback( (index: number, event: KeyboardEvent) => { if (event.key === 'Backspace') { event.preventDefault(); const newPasscode = [...passcode.slice(0, index), '', ...passcode.slice(index + 1)]; if (onChange) { onChange(newPasscode); } else { setInternalPasscode(newPasscode); } const previousInput = inputRefs.current[index - 1]; const currentInput = inputRefs.current[index]; if (previousInput) { previousInput.focus(); } if (currentInput) { currentInput.value = ''; } } }, [passcode, onChange], ); const handleFocus = useCallback( (index: number) => { inputRefs.current[index]?.select(); }, [inputRefs], ); const handleBlur = useCallback( (index: number, event: FocusEvent) => { if (onBlur) { onBlur(index, event); } }, [onBlur], ); return (
{Array.from({ length }).map((_, index) => ( handleChange(index, e)} onPaste={e => handlePaste(index, e)} onKeyDown={e => handleKeyDown(index, e)} onFocus={() => handleFocus(index)} onBlur={e => handleBlur(index, e)} ref={input => (inputRefs.current[index] = input)} className={styles.input({})} aria-label={`Passcode digit ${index + 1}`} inputMode={type === 'numbers' ? 'numeric' : 'text'} /> ))}
); }, ); PassCode.displayName = 'PassCode';