UNPKG

formik-otp-input

Version:

A React component for one-time-password.

259 lines (249 loc) 15 kB
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