109 lines
2.9 KiB
TypeScript
109 lines
2.9 KiB
TypeScript
import React, { useRef, useEffect } from "react";
|
|
|
|
interface OtpInputProps {
|
|
value: string;
|
|
onChange: (code: string) => void;
|
|
length?: number;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function OtpInput({
|
|
value,
|
|
onChange,
|
|
length = 6,
|
|
disabled = false,
|
|
}: OtpInputProps) {
|
|
const inputsRef = useRef<(HTMLInputElement | null)[]>([]);
|
|
|
|
// Array of digits according to desired length
|
|
const digits = Array.from({ length }, (_, i) => value[i] || "");
|
|
|
|
useEffect(() => {
|
|
// Focus the first empty input or the first one on mount
|
|
const firstEmptyIndex = digits.findIndex((d) => !d);
|
|
const targetIndex = firstEmptyIndex === -1 ? 0 : firstEmptyIndex;
|
|
inputsRef.current[targetIndex]?.focus();
|
|
}, []);
|
|
|
|
const handleChange = (
|
|
e: React.ChangeEvent<HTMLInputElement>,
|
|
index: number
|
|
) => {
|
|
const rawVal = e.target.value.replace(/\D/g, "");
|
|
if (!rawVal) {
|
|
// Cleared current digit
|
|
const newDigits = [...digits];
|
|
newDigits[index] = "";
|
|
onChange(newDigits.join(""));
|
|
return;
|
|
}
|
|
|
|
const digit = rawVal.slice(-1);
|
|
const newDigits = [...digits];
|
|
newDigits[index] = digit;
|
|
const combined = newDigits.join("");
|
|
onChange(combined);
|
|
|
|
// Auto advance focus to the next input
|
|
if (index < length - 1) {
|
|
inputsRef.current[index + 1]?.focus();
|
|
inputsRef.current[index + 1]?.select();
|
|
}
|
|
};
|
|
|
|
const handleKeyDown = (
|
|
e: React.KeyboardEvent<HTMLInputElement>,
|
|
index: number
|
|
) => {
|
|
if (e.key === "Backspace") {
|
|
if (!digits[index] && index > 0) {
|
|
// Current box is empty, jump to previous and clear it
|
|
const newDigits = [...digits];
|
|
newDigits[index - 1] = "";
|
|
onChange(newDigits.join(""));
|
|
inputsRef.current[index - 1]?.focus();
|
|
}
|
|
} else if (e.key === "ArrowLeft" && index > 0) {
|
|
inputsRef.current[index - 1]?.focus();
|
|
} else if (e.key === "ArrowRight" && index < length - 1) {
|
|
inputsRef.current[index + 1]?.focus();
|
|
}
|
|
};
|
|
|
|
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
e.preventDefault();
|
|
const pasted = e.clipboardData
|
|
.getData("text")
|
|
.replace(/\D/g, "")
|
|
.slice(0, length);
|
|
if (!pasted) return;
|
|
|
|
onChange(pasted);
|
|
const nextIndex = Math.min(pasted.length, length - 1);
|
|
inputsRef.current[nextIndex]?.focus();
|
|
};
|
|
|
|
return (
|
|
<div className="otp-container">
|
|
{digits.map((digit, index) => (
|
|
<input
|
|
key={index}
|
|
ref={(el) => {
|
|
inputsRef.current[index] = el;
|
|
}}
|
|
type="text"
|
|
inputMode="numeric"
|
|
pattern="[0-9]*"
|
|
maxLength={1}
|
|
value={digit}
|
|
disabled={disabled}
|
|
onChange={(e) => handleChange(e, index)}
|
|
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
onPaste={handlePaste}
|
|
className="otp-digit-input"
|
|
autoComplete="one-time-code"
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|