Skip to content

Instantly share code, notes, and snippets.

@imkarimkarim
Last active December 13, 2023 07:43
Show Gist options
  • Save imkarimkarim/fdc7ae01b84161719c30ca1942b3e647 to your computer and use it in GitHub Desktop.
Save imkarimkarim/fdc7ae01b84161719c30ca1942b3e647 to your computer and use it in GitHub Desktop.
Pure React OTP input
import React from "react";
export default function Example() {
return <OtpInput value={otp} onChange={(value: string) => setOtp(value)} />;
}
// copied from https://dominicarrojado.com/posts/how-to-create-your-own-otp-input-in-react-and-typescript-with-tests-part-1/
// this is just a result of the following article in one file so you don't need to go step by step and copy the whole component at once
import React, { useMemo } from "react";
export const RE_DIGIT = new RegExp(/^\d+$/);
export const focusOnFirstOtpInput = () => {
document.getElementById("otp-input-0")?.focus();
};
export default function OtpInput({
value,
valueLength = 6,
onChange,
}: {
value: string;
valueLength?: number;
onChange: (value: string) => void;
}) {
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 valueItems = useMemo(() => {
const valueArray = value.split("");
const items: Array<string> = [];
for (let i = 0; i < valueLength; i++) {
const char = valueArray[i];
if (RE_DIGIT.test(char)) {
items.push(char);
} else {
items.push("");
}
}
return items;
}, [value, valueLength]);
const inputOnChange = (
e: React.ChangeEvent<HTMLInputElement>,
idx: number
) => {
const target = e.target;
let targetValue = target.value.trim();
targetValue = toEnglishNumber(targetValue);
const isTargetValueDigit = RE_DIGIT.test(targetValue);
if (!isTargetValueDigit && targetValue !== "") {
return;
}
const nextInputEl =
target.nextElementSibling as HTMLInputElement | null;
// only delete digit if next input element has no value
if (!isTargetValueDigit && nextInputEl && nextInputEl.value !== "") {
return;
}
targetValue = isTargetValueDigit ? targetValue : " ";
const targetValueLength = targetValue.length;
if (targetValueLength === 1) {
const newValue =
value.substring(0, idx) +
targetValue +
value.substring(idx + 1);
onChange(newValue);
if (targetValue.length === 1) {
if (!isTargetValueDigit) {
return;
}
focusToNextInput(target);
} else {
const nextElementSibling =
target.nextElementSibling as HTMLInputElement | null;
if (nextElementSibling) {
nextElementSibling.focus();
}
}
} else if (targetValueLength === valueLength) {
onChange(targetValue);
target.blur();
}
};
const inputOnKeyDown = (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 previousElementSibling =
target.previousElementSibling as HTMLInputElement | null;
if (previousElementSibling) {
previousElementSibling.focus();
}
};
const inputOnFocus = (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
className={classNames(
bootstrapStyle.activationCode,
bootstrapStyle.inForm
)}
id="activationCode"
>
{valueItems.map((digit, idx) => (
<input
key={idx}
autoFocus={idx == 0}
id={`otp-input-${idx}`}
placeholder="-"
type="text"
inputMode="numeric"
autoComplete="one-time-code"
pattern="\d{1}"
maxLength={valueLength}
className={classNames(
"otp-input",
bootstrapStyle.persianDigits
)}
value={digit}
onChange={(e) => inputOnChange(e, idx)}
onKeyDown={inputOnKeyDown}
onFocus={inputOnFocus}
/>
))}
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment