import classNames from 'classnames'; import { composeRef } from 'rc-util/lib/ref'; import * as React from 'react'; import { ConfigContext } from '../config-provider'; import SizeContext from '../config-provider/SizeContext'; import type { InputProps, InputRef } from './Input'; import Input from './Input'; import type { CountdownProps } from '../statistic/Countdown'; import Countdown from '../statistic/Countdown'; export interface OtpProps extends Omit { inputPrefixCls?: string; length?: number; onChange?: ( value: string, event?: React.ChangeEvent | React.MouseEvent | React.KeyboardEvent ) => void; onComplete?: ( value: string, event?: React.ChangeEvent | React.MouseEvent | React.KeyboardEvent ) => void; type?: 'number' | 'text' | 'password'; countdown?: CountdownProps; } // Códigos keyboar const BACKSPACE = 8; const LEFT_ARROW = 37; const RIGHT_ARROW = 39; const DELETE = 46; const SPACEBAR = 32; const Otp = React.forwardRef((props, ref) => { const { prefixCls: customizePrefixCls, inputPrefixCls: customizeInputPrefixCls, prefix = undefined, suffix = undefined, className, size: customizeSize, length, type, disabled, value, onInput: customOnInput, onPaste: customOnPaste, onKeyDown: customOnKeyDown, onComplete: customOnComplete, onChange: customOnChange, onCompositionStart, onCompositionEnd, children, countdown, ...restProps } = props; const { getPrefixCls, direction } = React.useContext(ConfigContext); const contextSize = React.useContext(SizeContext); const composedRef = React.useRef(false); const size = customizeSize || contextSize; const inputRefs = Array(length).fill(null).map(() => React.useRef(null)); const [inputActive, setInputActive] = React.useState(0); const [otpValue, setOtpValue] = React.useState(`${value}`.split('')); //#region Eventos Focus const focusInput = (input: number) => { const _inputActive = Math.max(Math.min(length! - 1, input), 0); setInputActive(_inputActive); const _input = inputRefs[_inputActive].current; _input?.focus(); if (input < 0 || input >= length!) { _input?.blur(); } }; const focusNextInput = () => focusInput(inputActive + 1); const focusPrevInput = () => focusInput(inputActive - 1); // Eventos del teclado const onKeyDown = (event: React.KeyboardEvent) => { if (event.keyCode === BACKSPACE || event.key === 'Backspace') { onChangeOtp('', event); focusPrevInput(); } else if (event.keyCode === DELETE || event.key === 'Delete') { onChangeOtp('', event); } else if (event.keyCode === LEFT_ARROW || event.key === 'ArrowLeft') { event.preventDefault(); focusPrevInput(); } else if (event.keyCode === RIGHT_ARROW || event.key === 'ArrowRight') { event.preventDefault(); focusNextInput(); } else if (event.keyCode === SPACEBAR || event.key === ' ' || event.key === 'Spacebar' || event.key === 'Space') { event.preventDefault(); } if (customOnKeyDown) { customOnKeyDown(event); } }; //#endregion //#region Eventos change const onChangeOtp = (value: string, event?: React.ChangeEvent | React.MouseEvent | React.KeyboardEvent) => setOtpValue(_otpValue => { if (_otpValue[inputActive] === value[0]) { return _otpValue; } _otpValue[inputActive] = value[0]; const OTP = _otpValue.join(''); if (customOnComplete && OTP.length === length) { customOnComplete(OTP, event); return _otpValue; } if (customOnChange) { customOnChange(OTP, event); } return _otpValue; }); /** Detectar cambios en el input. */ const onChange = (event: React.ChangeEvent) => { const { value } = event.target; onChangeOtp(value, event); }; const onInput = (event: React.ChangeEvent) => { const { value } = event.target; if (!!value) { focusNextInput(); } else { onChangeOtp('', event); } if (customOnInput) { customOnInput(event); } }; const onPaste = (event: React.ClipboardEvent) => { event.preventDefault(); if (props.disabled) { return; } const otp: string[] = otpValue; // Get pastedData in an array of max size (num of inputs - current position) const pastedData = event.clipboardData.getData('text/plain'); if (type === 'number' && isNaN(Number(pastedData))) { return; } const pastedDataArray = pastedData.slice(0, length! - inputActive).split(''); // Paste data from focused input onwards for (let pos: number = 0; pos < length!; ++pos) { if (pos >= inputActive && pastedData.length > 0) { otp[pos] = pastedDataArray.shift() as string; } } const _otp = otp.join(''); const nextActiveInput = _otp.length; focusInput(nextActiveInput); setOtpValue(otp); if (customOnComplete && _otp.length === length) { customOnComplete(_otp); return; } if (customOnChange) { customOnChange(_otp); } if (customOnPaste) { customOnPaste(event) } }; //#endregion const prefixCls = getPrefixCls('input-otp', customizePrefixCls); const inputPrefixCls = getPrefixCls('input', customizeInputPrefixCls); const cls = classNames( prefixCls, { [`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-${size}`]: !!size, }, className ); const handleOnCompositionStart: React.CompositionEventHandler = e => { composedRef.current = true; onCompositionStart?.(e); }; const handleOnCompositionEnd: React.CompositionEventHandler = e => { composedRef.current = false; onCompositionEnd?.(e); }; const autoFocus = (index) => { if (index === 0) { return true; } return false; } return (
{new Array(length).fill(null).map((_, index) => { const inputValue = (otpValue && otpValue[index]) ?? ''; return ( (inputRefs[index], ref)} {...restProps} type={type} size={size} value={inputValue} onCompositionStart={handleOnCompositionStart} onCompositionEnd={handleOnCompositionEnd} min={0} max={9} minLength={1} maxLength={1} prefixCls={inputPrefixCls} prefix suffix onChange={onChange} onInput={onInput} onPaste={onPaste} onKeyDown={onKeyDown} className={cls} disabled={disabled} autoFocus={autoFocus(index)} onFocus={(e: React.FocusEvent) => { setInputActive(index); e.target.select(); }} onBlur={() => setInputActive(-1)} /> ); })}
{children} {countdown && }
); }); Otp.defaultProps = { length: 4, type: 'number', value: '' } if (process.env.NODE_ENV !== 'production') { Otp.displayName = 'Otp'; } export default Otp;