formik-otp-input
Version:
A React component for one-time-password.
259 lines (249 loc) • 15 kB
JavaScript
import React, { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
import * as Yup from 'yup';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
function __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
function __makeTemplateObject(cooked, raw) {
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
return cooked;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var createOtpSchema = function (inputType, length) {
var regexMap = {
alphanumeric: /^[a-zA-Z0-9]*$/,
alphabetic: /^[a-zA-Z]*$/,
numeric: /^[0-9]*$/
};
var regex = regexMap[inputType] || regexMap.numeric;
return Yup.string()
.matches(regex, "Only ".concat(inputType, " characters are allowed"))
.required("Please fill in all OTP fields before submitting")
.length(length, "OTP must be exactly ".concat(length, " characters long"));
};
var isValidInput = function (value, inputType) {
var _a;
var regexMap = {
alphanumeric: /^[a-z0-9]$/i,
alphabetic: /^[a-z]$/i,
numeric: /^[0-9]$/i
};
return ((_a = regexMap[inputType]) === null || _a === void 0 ? void 0 : _a.test(value)) || false;
};
var validateOtp = function (otpValue, inputType, length, setFieldError) { return __awaiter(void 0, void 0, void 0, function () {
var otpSchema, err_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
otpSchema = createOtpSchema(inputType, length);
_a.label = 1;
case 1:
_a.trys.push([1, 3, , 4]);
return [4 /*yield*/, otpSchema.validate(otpValue)];
case 2:
_a.sent();
setFieldError("otp", undefined); // Clear any previous error
return [3 /*break*/, 4];
case 3:
err_1 = _a.sent();
if (err_1 instanceof Yup.ValidationError) {
setFieldError("otp", err_1.message); // Set the validation error
}
return [3 /*break*/, 4];
case 4: return [2 /*return*/];
}
});
}); };
var Container = styled.div(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n display: flex;\n justify-content: center;\n"], ["\n display: flex;\n justify-content: center;\n"])));
var Input = styled.input(templateObject_2 || (templateObject_2 = __makeTemplateObject(["\n width: 34px;\n height: 43px;\n margin: 0 4px;\n text-align: center;\n font-size: 20px;\n font-family: Monospaced, monospace;\n border: 1.5px solid ", ";\n border-radius: 8px;\n caret: block;\n color: ", ";\n background: ", ";\n box-shadow: 0.25px 0.5px 1px 0 rgb(229, 229, 229);\n transition: background-color 0.3s, border-color 0.1s;\n\n\n &[type='number'] {\n -moz-appearance: textfield; // For Firefox\n &::-webkit-inner-spin-button,\n &::-webkit-outer-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n }\n\n &:focus {\n border-color: ", ";\n outline: none;\n }\n\n &::placeholder {\n color: #ced4da;\n }\n\n &.error {\n border-color: #dc3545;\n }\n\n @media (max-width: 600px) {\n width: 26px;\n height: 40px;\n }\n"], ["\n width: 34px;\n height: 43px;\n margin: 0 4px;\n text-align: center;\n font-size: 20px;\n font-family: Monospaced, monospace;\n border: 1.5px solid ", ";\n border-radius: 8px;\n caret: block;\n color: ", ";\n background: ", ";\n box-shadow: 0.25px 0.5px 1px 0 rgb(229, 229, 229);\n transition: background-color 0.3s, border-color 0.1s;\n\n\n &[type='number'] {\n -moz-appearance: textfield; // For Firefox\n &::-webkit-inner-spin-button,\n &::-webkit-outer-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n }\n\n &:focus {\n border-color: ", ";\n outline: none;\n }\n\n &::placeholder {\n color: #ced4da;\n }\n\n &.error {\n border-color: #dc3545;\n }\n\n @media (max-width: 600px) {\n width: 26px;\n height: 40px;\n }\n"])), function (props) { return (props.borderColor || '#DDDDDD'); }, function (props) { return props.textColor || '#000000'; }, function (props) { return props.backgroundColor || '#fff'; }, function (props) { return props.highlightColor || '#ff8000'; });
var Spacer = styled.div(templateObject_3 || (templateObject_3 = __makeTemplateObject(["\n display: flex;\n justify-content: center;\n align-items: center;\n font-size: 12px;\n color: #DDDDDD;\n margin-right: 3px;\n margin-left: 3px;\n"], ["\n display: flex;\n justify-content: center;\n align-items: center;\n font-size: 12px;\n color: #DDDDDD;\n margin-right: 3px;\n margin-left: 3px;\n"])));
var OtpInput = function (_a) {
var length = _a.length, onChange = _a.onChange, onFullFill = _a.onFullFill, _b = _a.autoComplete, autoComplete = _b === void 0 ? "one-time-code" : _b, onBlur = _a.onBlur, setFieldError = _a.setFieldError, setFieldTouched = _a.setFieldTouched, _c = _a.inputType, inputType = _c === void 0 ? 'numeric' : _c, _d = _a.autoFocus, autoFocus = _d === void 0 ? true : _d, _e = _a.autoSubmit, autoSubmit = _e === void 0 ? true : _e, textColor = _a.textColor, backgroundColor = _a.backgroundColor, highlightColor = _a.highlightColor, borderColor = _a.borderColor;
var _f = useState(new Array(length).fill('')), localOtp = _f[0], setLocalOtp = _f[1];
var inputRefs = useRef(new Array(length).fill(null));
var _g = useState(false), hasUserStartedTyping = _g[0], setHasUserStartedTyping = _g[1];
// Custom hook for OTP validation
useEffect(function () {
var otpValue = localOtp.join('');
if (hasUserStartedTyping) {
validateOtp(otpValue, inputType, length, setFieldError);
}
else {
setFieldError("otp", undefined);
}
}, [localOtp, inputType, length, setFieldError, hasUserStartedTyping]);
useEffect(function () {
if (autoFocus && inputRefs.current[0]) {
inputRefs.current[0].focus();
setFieldTouched("otp", true);
}
}, [autoFocus, setFieldTouched]); // Include setFieldTouched in the dependency array
var resetOtp = function () {
setLocalOtp(new Array(length).fill(''));
if (inputRefs.current[0]) {
inputRefs.current[0].focus();
setFieldTouched("otp", true);
}
};
var handleChange = function (event, index) {
var _a;
if (!hasUserStartedTyping) {
setHasUserStartedTyping(true);
}
setFieldTouched("otp", true);
var newValue = event.target.value.slice(-1); // Take the last character
var newOtp = __spreadArray([], localOtp, true);
newOtp[index] = newValue;
// Update state with the new OTP value
setLocalOtp(newOtp);
var isOtpComplete = newOtp.every(function (val) { return val.length === 1; });
if (isOtpComplete) {
// Manually update Formik values
onChange({
target: {
name: "otp",
value: newOtp.join('')
}
});
if (autoSubmit) {
onFullFill();
resetOtp();
}
}
// Move focus or handle auto-submit
if (newValue.length === 1 && isValidInput(newValue, inputType)) {
setFieldError("otp", undefined); // Clear any previous error
if (index < length - 1) {
// Move focus to the next field if not the last one
(_a = inputRefs.current[index + 1]) === null || _a === void 0 ? void 0 : _a.focus();
// todo: only do this on desktop but fix same character not update issue
// inputRefs.current[index + 1]?.select();
}
}
};
var handleKeyDown = function (e, index) {
var _a, _b, _c;
if (e.key === "Backspace") {
e.preventDefault();
if (localOtp[index] === '' && index > 0) {
var newOtp = __spreadArray([], localOtp, true);
newOtp[index - 1] = '';
setLocalOtp(newOtp);
onChange({
target: {
name: "otp",
value: newOtp.join('')
}
});
(_a = inputRefs.current[index - 1]) === null || _a === void 0 ? void 0 : _a.focus();
}
else {
var newOtp = __spreadArray([], localOtp, true);
newOtp[index] = '';
setLocalOtp(newOtp);
}
}
else if (e.key === "ArrowLeft" && index > 0) {
e.preventDefault();
(_b = inputRefs.current[index - 1]) === null || _b === void 0 ? void 0 : _b.focus();
// todo: only do this on desktop but fix same character not update issue
// inputRefs.current[index - 1]?.select();
}
else if (e.key === "ArrowRight" && index < length - 1) {
e.preventDefault();
(_c = inputRefs.current[index + 1]) === null || _c === void 0 ? void 0 : _c.focus();
}
};
var handleKeyPress = function (event) {
if (event.key === 'Enter') {
event.preventDefault();
var isFullFilled = localOtp.every(function (val) { return val !== ''; });
if (isFullFilled) {
onFullFill();
}
else {
setFieldError("otp", "Please fill in all OTP fields before submitting.");
}
}
else if (!isValidInput(event.key, inputType)) {
event.preventDefault();
setFieldError("otp", "Only ".concat(inputType, " characters are allowed."));
}
};
var getInputType = function (inputType) {
switch (inputType) {
case 'alphanumeric':
case 'alphabetic':
return 'text';
case 'numeric':
return 'tel';
default:
return 'text';
}
};
var isEvenLength = length % 2 === 0;
var midpointIndex = length % 2 === 0 ? (length / 2) - 1 : null;
return (React.createElement(Container, null, localOtp.map(function (data, index) { return (React.createElement(React.Fragment, null,
React.createElement(Input, { name: "otp", type: getInputType(inputType), key: index, maxLength: 1, value: data, ref: function (el) { return inputRefs.current[index] = el; }, onChange: function (e) { return handleChange(e, index); }, onKeyDown: function (e) { return handleKeyDown(e, index); }, onKeyPress: function (e) { return handleKeyPress(e); }, onBlur: onBlur, autoComplete: autoComplete, className: isEvenLength && index === midpointIndex ? 'extra-space' : '', autoCorrect: "off", autoCapitalize: "off", spellCheck: "false", textColor: textColor, backgroundColor: backgroundColor, highlightColor: highlightColor, borderColor: borderColor }),
isEvenLength && index === midpointIndex && React.createElement(Spacer, null, "-"))); })));
};
var templateObject_1, templateObject_2, templateObject_3;
export { OtpInput as default };
//# sourceMappingURL=index.esm.js.map