All files CodeInput.tsx

92.3% Statements 36/39
72% Branches 18/25
100% Functions 12/12
92.1% Lines 35/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101            10x 411x 411x   411x 202x     411x 136x 136x 136x   136x 96x       411x       136x 6x 130x   130x         411x       3x 3x 3x   12x   3x 3x 12x     3x   3x 12x   3x     3x       411x             1644x       3252x             136x   136x 3x                              
import { useState, useRef, useEffect } from "react";
 
interface CodeInputProps {
	onCodeChange: (code: string[]) => void;
}
 
export const CodeInput: React.FC<CodeInputProps> = ({ onCodeChange }) => {
	const [code, setCode] = useState<string[]>(["", "", "", ""]);
	const inputRefs = useRef<(HTMLInputElement | null)[]>(Array(4).fill(null));
 
	useEffect(() => {
		onCodeChange(code);
	}, [code, onCodeChange]);
 
	const handleInputChange = (index: number, value: string) => {
		const newCode = [...code];
		newCode[index] = value.slice(0, 1);
		setCode(newCode);
 
		if (newCode[index] && index < 3) {
			inputRefs.current[index + 1]?.focus();
		}
	};
 
	const handleKeyDown = (
		index: number,
		e: React.KeyboardEvent<HTMLInputElement>,
	) => {
		if (e.key === "Backspace" && !code[index] && index > 0) {
			inputRefs.current[index - 1]?.focus();
		} elseI if (e.key === "ArrowRight" && index < 3) {
			inputRefs.current[index + 1]?.focus();
		} elseI if (e.key === "ArrowLeft" && index > 0) {
			inputRefs.current[index - 1]?.focus();
		}
	};
 
	const handlePaste = (
		e: React.ClipboardEvent<HTMLInputElement>,
		index: number,
	) => {
		e.preventDefault();
		const pastedData = e.clipboardData.getData("text").slice(0, 4);
		const chars = pastedData
			.split("")
			.filter((char) => /^[A-Za-z0-9]$/.test(char));
 
		const newCode = [...code];
		for (let i = 0; i < Math.min(chars.length, 4 - index); i++) {
			newCode[index + i] = chars[i];
		}
 
		setCode(newCode);
 
		const nextEmptyIndex = newCode.findIndex(
			(char, idx) => char === "" && idx >= index,
		);
		if (neIxtEmptyIndex !== -1 && nextEmptyIndex < 4) {
			inputRefs.current[nextEmptyIndex]?.focus();
		} else {
			inputRefs.current[3]?.focus();
		}
	};
 
	return (
		<div
			className="flex justify-between gap-4"
			role="group"
			aria-label="Access code input"
		>
			{code.map((char, index) => (
				<div key={index} className="relative flex-1">
					<input
						id={index === 0 ? "access-code" : undefined} // only first input gets ID
						ref={(el) => {
							inputRefs.current[index] = el;
						}}
						type="text"
						className="input input-accent input-lg w-full h-16 text-center font-mono text-3xl font-semibold"
						maxLength={1}
						value={char}
						onChange={(e) =>
							handleInputChange(index, e.target.value)
						}
						onKeyDown={(e) => handleKeyDown(index, e)}
						onPaste={(e) => handlePaste(e, index)}
						pattern="[A-Za-z0-9]"
						required
						aria-label={`Character ${index + 1} of access code`}
						aria-required="true"
						aria-invalid={char === "" ? "true" : "false"}
						inputMode="text"
						autoComplete="one-time-code"
						placeholder="•"
					/>
				</div>
			))}
		</div>
	);
};