platform-codebase/@packages/@infrastructure/sso-client/src/components/MFAChallenge/CodeInput.tsx

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