has/src/components/OtpInput.tsx

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>
);
}