Skip to content

Instantly share code, notes, and snippets.

@thelamina
Created February 4, 2023 17:02
Show Gist options
  • Save thelamina/18797c86e010c3b9d4285bf25b7fba79 to your computer and use it in GitHub Desktop.
Save thelamina/18797c86e010c3b9d4285bf25b7fba79 to your computer and use it in GitHub Desktop.
react-otp-input
import clsx from 'clsx';
import React, { useMemo } from 'react';
type onChangeType = {
target: {
name: string;
value: string;
};
};
const REGEX_TYPES = {
numeric: /^\d+$/,
alphabet: /^[A-Z]+$/i,
alpa_numeric: /^\w+$/,
};
export type OTPInputProps = {
value: string;
length?: number;
className?: string;
containerClassName?: string;
name?: string;
type?: keyof typeof REGEX_TYPES;
disabled?: boolean;
onChange: ({ target }: onChangeType) => void;
};
export const OTPInput = (props: Partial<OTPInputProps>) => {
const {
value = '',
length = 4,
onChange,
type = 'numeric',
name = '',
disabled = false,
className = '',
containerClassName = '',
} = props;
const valueItems = useMemo(() => {
const valueArray = value.split('');
const items: Array<string> = [];
for (let i = 0; i < length; i++) {
const char = valueArray[i];
if (REGEX_TYPES[type].test(char)) {
items.push(char);
} else {
items.push('');
}
}
return items;
}, [value, length, type]);
const customOnchange = (value: string) => {
if (onChange) {
onChange({
target: {
name,
value,
},
});
}
};
const focusToNextInput = (target: HTMLElement) => {
const nextElementSibling = target.nextElementSibling as HTMLInputElement | null;
if (nextElementSibling) {
nextElementSibling.focus();
}
};
const focusToPrevInput = (target: HTMLElement) => {
const previousElementSibling = target.previousElementSibling as HTMLInputElement | null;
if (previousElementSibling) {
previousElementSibling.focus();
}
};
const handleInputOnChange = (e: React.ChangeEvent<HTMLInputElement>, idx: number) => {
const target = e.target;
let targetValue = target.value.trim();
// const isTargetValueMatch =
const isTargetValueMatch = REGEX_TYPES[type].test(targetValue);
if (!isTargetValueMatch && targetValue !== '') {
return;
}
const nextInputEl = target.nextElementSibling as HTMLInputElement | null;
// only delete value if next input element has no value
if (!isTargetValueMatch && nextInputEl && nextInputEl.value !== '') {
return;
}
targetValue = isTargetValueMatch ? targetValue : ' ';
const targetValueLength = targetValue.length;
if (targetValueLength === 1) {
const newValue = value.substring(0, idx) + targetValue + value.substring(idx + 1);
customOnchange(newValue);
if (!isTargetValueMatch) {
return;
}
focusToNextInput(target);
} else if (targetValueLength === length) {
customOnchange(targetValue);
}
target.blur();
};
const handleInputOnKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
const { key } = e;
const target = e.target as HTMLInputElement;
if (key === 'ArrowRight' || key === 'ArrowDown') {
e.preventDefault();
return focusToNextInput(target);
}
if (key === 'ArrowLeft' || key === 'ArrowUp') {
e.preventDefault();
return focusToPrevInput(target);
}
const targetValue = target.value;
// keep the selection range position
// if the same digit was typed
target.setSelectionRange(0, targetValue.length);
if (e.key !== 'Backspace' || targetValue !== '') {
return;
}
focusToPrevInput(target);
};
const handleInputOnFocus = (e: React.FocusEvent<HTMLInputElement>) => {
const { target } = e;
// keep focusing back until previous input
// element has value
const prevInputEl = target.previousElementSibling as HTMLInputElement | null;
if (prevInputEl && prevInputEl.value === '') {
return prevInputEl.focus();
}
target.setSelectionRange(0, target.value.length);
};
return (
<div role="textbox" className={clsx('flex gap-1 items-center', containerClassName)}>
{valueItems.map((el, idx) => (
<input
key={idx}
type="text"
inputMode={type === 'numeric' ? 'numeric' : 'text'}
autoComplete="one-time-code"
pattern="\d{1}"
maxLength={length}
className={clsx(
'uppercase outline-1 outline-none focus:border-gray-800 hover:border-blue-400 h-10 w-10 text-center bg-transparent border rounded-md border-gray-400',
className
)}
value={el}
onChange={(e) => handleInputOnChange(e, idx)}
onKeyDown={handleInputOnKeyDown}
onFocus={handleInputOnFocus}
disabled={disabled}
/>
))}
</div>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment