100 lines
2.7 KiB
TypeScript
Executable file
100 lines
2.7 KiB
TypeScript
Executable file
import { useRef, useCallback, KeyboardEvent, ClipboardEvent } from 'react';
|
|
|
|
export interface CodeInputProps {
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
length?: number;
|
|
disabled?: boolean;
|
|
autoFocus?: boolean;
|
|
error?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export function CodeInput({
|
|
value,
|
|
onChange,
|
|
length = 6,
|
|
disabled = false,
|
|
autoFocus = true,
|
|
error = false,
|
|
className = '',
|
|
}: CodeInputProps) {
|
|
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
|
|
|
const handleChange = useCallback(
|
|
(index: number, char: string) => {
|
|
const sanitized = char.replace(/\D/g, '').slice(0, 1);
|
|
const newValue = value.split('');
|
|
newValue[index] = sanitized;
|
|
|
|
const result = newValue.join('').slice(0, length);
|
|
onChange(result);
|
|
|
|
if (sanitized && index < length - 1) {
|
|
inputRefs.current[index + 1]?.focus();
|
|
}
|
|
},
|
|
[value, length, onChange]
|
|
);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(index: number, e: KeyboardEvent<HTMLInputElement>) => {
|
|
if (e.key === 'Backspace' && !value[index] && index > 0) {
|
|
inputRefs.current[index - 1]?.focus();
|
|
}
|
|
if (e.key === 'ArrowLeft' && index > 0) {
|
|
inputRefs.current[index - 1]?.focus();
|
|
}
|
|
if (e.key === 'ArrowRight' && index < length - 1) {
|
|
inputRefs.current[index + 1]?.focus();
|
|
}
|
|
},
|
|
[value, length]
|
|
);
|
|
|
|
const handlePaste = useCallback(
|
|
(e: ClipboardEvent<HTMLInputElement>) => {
|
|
e.preventDefault();
|
|
const pasted = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, length);
|
|
onChange(pasted);
|
|
|
|
const focusIndex = Math.min(pasted.length, length - 1);
|
|
inputRefs.current[focusIndex]?.focus();
|
|
},
|
|
[length, onChange]
|
|
);
|
|
|
|
const handleFocus = useCallback(
|
|
(index: number) => {
|
|
inputRefs.current[index]?.select();
|
|
},
|
|
[]
|
|
);
|
|
|
|
return (
|
|
<div className={`code-input ${error ? 'code-input-error' : ''} ${className}`}>
|
|
{Array.from({ length }).map((_, index) => (
|
|
<input
|
|
key={index}
|
|
ref={(el) => {
|
|
inputRefs.current[index] = el;
|
|
}}
|
|
type="text"
|
|
inputMode="numeric"
|
|
pattern="[0-9]*"
|
|
maxLength={1}
|
|
value={value[index] || ''}
|
|
onChange={(e) => handleChange(index, e.target.value)}
|
|
onKeyDown={(e) => handleKeyDown(index, e)}
|
|
onPaste={handlePaste}
|
|
onFocus={() => handleFocus(index)}
|
|
disabled={disabled}
|
|
autoFocus={autoFocus && index === 0}
|
|
autoComplete="one-time-code"
|
|
className="code-input-digit"
|
|
aria-label={`Digit ${index + 1}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|