UNPKG

@razorpay/blade

Version:

The Design System that powers Razorpay

383 lines (376 loc) 18 kB
import _defineProperty from '@babel/runtime/helpers/defineProperty'; import _slicedToArray from '@babel/runtime/helpers/slicedToArray'; import _objectWithoutProperties from '@babel/runtime/helpers/objectWithoutProperties'; import React__default, { useState, useImperativeHandle, useEffect } from 'react'; import '../BaseInput/index.js'; import { BaseInput, getHintType } from '../BaseInput/BaseInput.js'; import isEmpty from '../../../utils/lodashButBetter/isEmpty.js'; import '../../Form/index.js'; import { useFormId } from '../../Form/useFormId.js'; import '../../Box/BaseBox/index.js'; import '../../Box/styledProps/index.js'; import '../../../utils/index.js'; import '../../../utils/metaAttribute/index.js'; import '../../../utils/makeSize/index.js'; import '../../../utils/makeAnalyticsAttribute/index.js'; import { jsx, jsxs } from 'react/jsx-runtime'; import { getPlatformType } from '../../../utils/getPlatformType/getPlatformType.js'; import { BaseBox } from '../../Box/BaseBox/BaseBox.web.js'; import { makeAnalyticsAttribute } from '../../../utils/makeAnalyticsAttribute/makeAnalyticsAttribute.js'; import { metaAttribute } from '../../../utils/metaAttribute/metaAttribute.web.js'; import { MetaConstants } from '../../../utils/metaAttribute/metaConstants.js'; import { getStyledProps } from '../../Box/styledProps/getStyledProps.js'; import { FormLabel } from '../../Form/FormLabel.js'; import { makeSize } from '../../../utils/makeSize/makeSize.js'; import { FormHint } from '../../Form/FormHint.js'; var _excluded = ["autoFocus", "errorText", "helpText", "isDisabled", "keyboardReturnKeyType", "keyboardType", "label", "accessibilityLabel", "labelPosition", "name", "onChange", "onFocus", "onBlur", "onOTPFilled", "otpLength", "placeholder", "successText", "validationState", "value", "isMasked", "autoCompleteSuggestionType", "testID", "size"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } var isReactNative = getPlatformType() === 'react-native'; /** * Converts a string value of otp to array if passed otherwise returns an array of 6 empty strings */ var otpToArray = function otpToArray(code) { var _code$split; return (_code$split = code === null || code === void 0 ? void 0 : code.split('')) !== null && _code$split !== void 0 ? _code$split : Array(6).fill(''); }; /** * OTPInput component can be used for accepting OTPs sent to users for authentication/verification purposes. * * ## Usage * * ```tsx * <OTPInput * label="Enter OTP" * name="otpInput" * onChange={({ name, value }): void => console.log({ name, value })} * onOTPFilled={({ name, value }): void => console.log({ name, value })} * /> * ``` */ var _OTPInput = function _OTPInput(_ref, incomingRef) { var autoFocus = _ref.autoFocus, errorText = _ref.errorText, helpText = _ref.helpText, isDisabled = _ref.isDisabled, keyboardReturnKeyType = _ref.keyboardReturnKeyType, _ref$keyboardType = _ref.keyboardType, keyboardType = _ref$keyboardType === void 0 ? 'decimal' : _ref$keyboardType, label = _ref.label, accessibilityLabel = _ref.accessibilityLabel, labelPosition = _ref.labelPosition, name = _ref.name, onChange = _ref.onChange, _onFocus = _ref.onFocus, _onBlur = _ref.onBlur, onOTPFilled = _ref.onOTPFilled, _ref$otpLength = _ref.otpLength, otpLength = _ref$otpLength === void 0 ? 6 : _ref$otpLength, placeholder = _ref.placeholder, successText = _ref.successText, validationState = _ref.validationState, inputValue = _ref.value, isMasked = _ref.isMasked, _ref$autoCompleteSugg = _ref.autoCompleteSuggestionType, autoCompleteSuggestionType = _ref$autoCompleteSugg === void 0 ? 'oneTimeCode' : _ref$autoCompleteSugg, testID = _ref.testID, _ref$size = _ref.size, size = _ref$size === void 0 ? 'medium' : _ref$size, rest = _objectWithoutProperties(_ref, _excluded); var inputRefs = []; var _useState = useState(otpToArray(inputValue)), _useState2 = _slicedToArray(_useState, 2), otpValue = _useState2[0], setOtpValue = _useState2[1]; var _useState3 = useState([]), _useState4 = _slicedToArray(_useState3, 2), inputType = _useState4[0], setInputType = _useState4[1]; var isLabelLeftPositioned = labelPosition === 'left'; var _useFormId = useFormId('otp'), inputId = _useFormId.inputId, helpTextId = _useFormId.helpTextId, errorTextId = _useFormId.errorTextId, successTextId = _useFormId.successTextId; useImperativeHandle(incomingRef, function () { return inputRefs.map(function (ref) { return ref.current; }); }, [inputRefs]); useEffect(function () { // Effect for calling `onOTPFilled` callback if (inputValue && inputValue.length >= otpLength) { // callback for when the OTPInput is controlled and inputValue reaches the same or greater length as the otpLength onOTPFilled === null || onOTPFilled === void 0 ? void 0 : onOTPFilled({ value: inputValue.slice(0, otpLength), name: name }); } else if (!inputValue && otpValue.join('').length >= otpLength) { // callback for when the OTPInput is uncontrolled and otpValue stored in state reaches the same or greater length as the otpLength onOTPFilled === null || onOTPFilled === void 0 ? void 0 : onOTPFilled({ value: otpValue.slice(0, otpLength).join(''), name: name }); } }, [otpValue, otpLength, name, inputValue, onOTPFilled]); useEffect(function () { /* We want to disable the password managers for OTPInput when isMasked is set. The issue with only setting autocomplete='off' is that its not an enforcement but a suggestion to the browser to follow. This workaround unsets type on first render and sets it to `password` only once a value is entered by the user. */ otpValue.forEach(function (otp, index) { // Set inputType as 'password' only when a value is entered when isMasked is set if (!isEmpty(otp) && !inputType[index] && isMasked) { var newInputType = Array.from(inputType); newInputType[index] = 'password'; setInputType(newInputType); } // Cleanup the inputType array whenever the value is empty but inputType[index] is set if (isEmpty(otp) && inputType[index]) { var _newInputType = Array.from(inputType); _newInputType[index] = undefined; setInputType(_newInputType); } }); }, [otpValue, inputType, isMasked]); /** * Changes the value of the otp at a given index and updates the otpValue stored in state * * @param {{ value: string; index: number }} { value, index } * @returns {string} updated otpValue */ var setOtpValueByIndex = function setOtpValueByIndex(_ref2) { var value = _ref2.value, index = _ref2.index; var newOtpValue = Array.from(otpValue); newOtpValue[index] = value; setOtpValue(newOtpValue); return newOtpValue.join(''); }; /** * Sets focus to the desired otp input by index * * @param {number} index the index of the otp input to be focused */ var focusOnOtpByIndex = function focusOnOtpByIndex(index) { var _inputRefs$index, _inputRefs$index$curr; (_inputRefs$index = inputRefs[index]) === null || _inputRefs$index === void 0 ? void 0 : (_inputRefs$index$curr = _inputRefs$index.current) === null || _inputRefs$index$curr === void 0 ? void 0 : _inputRefs$index$curr.focus(); if (!isReactNative) { var _inputRefs$index2, _inputRefs$index2$cur; // React Native doesn't support imperatively selecting the value of input (_inputRefs$index2 = inputRefs[index]) === null || _inputRefs$index2 === void 0 ? void 0 : (_inputRefs$index2$cur = _inputRefs$index2.current) === null || _inputRefs$index2$cur === void 0 ? void 0 : _inputRefs$index2$cur.select(); } }; var handleOnChange = function handleOnChange(_ref3) { var value = _ref3.value, currentOtpIndex = _ref3.currentOtpIndex; if (value && value === ' ') { // React native doesn't support `event.preventDefault()` hence have to add this check to ensure that empty space is not allowed return; } if (inputValue && inputValue.length > 0) { // When OTPInput is controlled, set the otpValue as the consumer passed `inputValue` and append the value on current index based on user's input. // User's input will not reflect on the otp but will trigger `onChange` callback with the user's input appended so that the consumer can take appropriate action. var newOtpValue = Array.from(inputValue); newOtpValue[currentOtpIndex] = value !== null && value !== void 0 ? value : ''; setOtpValue(newOtpValue); onChange === null || onChange === void 0 ? void 0 : onChange({ name: name, value: newOtpValue.join('') }); } else if (value && value.trim().length > 1) { // When the entered value is more that 1 character (when value is pasted), set the otpValue to the newly received value. // Could have used `onPaste` for web to achieve this but 1. React Native doesn't support onPaste and 2. Safari's autofill on web doesn't trigger onPaste setOtpValue(Array.from(value)); onChange === null || onChange === void 0 ? void 0 : onChange({ name: name, value: value.trim().slice(0, otpLength) }); } else if (otpValue[currentOtpIndex] !== (value === null || value === void 0 ? void 0 : value.trim())) { var _value$trim; // Set the value at the current index to the entered value // only as long as its not the same as the already existing value (this prevents `onChange` being triggered unnecessarily) var newValue = setOtpValueByIndex({ value: (_value$trim = value === null || value === void 0 ? void 0 : value.trim()) !== null && _value$trim !== void 0 ? _value$trim : '', index: currentOtpIndex }); onChange === null || onChange === void 0 ? void 0 : onChange({ name: name, value: newValue }); } }; var handleOnInput = function handleOnInput(_ref4) { var value = _ref4.value, currentOtpIndex = _ref4.currentOtpIndex; // Moves focus to next input whenever a value is entered in the current input if (value && value.trim().length === 1) { focusOnOtpByIndex(++currentOtpIndex); } }; var handleOnKeyDown = function handleOnKeyDown(_ref5) { var key = _ref5.key, code = _ref5.code, event = _ref5.event, currentOtpIndex = _ref5.currentOtpIndex; if (key === 'Backspace' || code === 'Backspace' || code === 'Delete' || key === 'Delete') { var _event$preventDefault; (_event$preventDefault = event.preventDefault) === null || _event$preventDefault === void 0 ? void 0 : _event$preventDefault.call(event); if (otpValue[currentOtpIndex]) { // Clear the value at the current index if value exists handleOnChange({ value: '', currentOtpIndex: currentOtpIndex }); } else { // Move focus to the previous input if the current input is empty // and clear the value at the new active (previous) index focusOnOtpByIndex(--currentOtpIndex); handleOnChange({ value: '', currentOtpIndex: currentOtpIndex }); } } else if (key === 'ArrowLeft' || code === 'ArrowLeft') { var _event$preventDefault2; (_event$preventDefault2 = event.preventDefault) === null || _event$preventDefault2 === void 0 ? void 0 : _event$preventDefault2.call(event); focusOnOtpByIndex(--currentOtpIndex); } else if (key === 'ArrowRight' || code === 'ArrowRight') { var _event$preventDefault3; (_event$preventDefault3 = event.preventDefault) === null || _event$preventDefault3 === void 0 ? void 0 : _event$preventDefault3.call(event); focusOnOtpByIndex(++currentOtpIndex); } else if (key === ' ' || code === 'Space') { var _event$preventDefault4; (_event$preventDefault4 = event.preventDefault) === null || _event$preventDefault4 === void 0 ? void 0 : _event$preventDefault4.call(event); } }; var getHiddenInput = function getHiddenInput() { if (!isReactNative) { var _ref6; return /*#__PURE__*/jsx("input", { hidden: true, id: inputId, name: name, value: (_ref6 = inputValue !== null && inputValue !== void 0 ? inputValue : otpValue.join('')) !== null && _ref6 !== void 0 ? _ref6 : '', readOnly: true }); } return null; }; var getOTPInputFields = function getOTPInputFields() { var inputs = []; var _loop = function _loop(index) { var _otpValue$index, _Array$from$index; var currentValue = inputValue ? otpToArray(inputValue)[index] || '' : otpValue[index] || ''; var ref = /*#__PURE__*/React__default.createRef(); // if an inputValue is passed (controlled) and isMasked is set, inputType will always be password var currentInputType; if (isMasked) { // if inputValue is passed (controlled component) then the inputType will always be password currentInputType = inputValue ? 'password' : inputType[index]; } inputRefs.push(ref); inputs.push( /*#__PURE__*/jsx(BaseBox, { flex: 1, marginLeft: index == 0 ? 'spacing.0' : 'spacing.3', children: /*#__PURE__*/jsx(BaseInput // eslint-disable-next-line jsx-a11y/no-autofocus , _objectSpread({ autoFocus: autoFocus && index === 0, accessibilityLabel: "".concat(index === 0 ? label || accessibilityLabel : '', " character ").concat(index + 1), label: label, hideLabelText: true, id: "".concat(inputId, "-").concat(index), textAlign: "center", ref: ref, name: name, value: currentValue, maxCharacters: ((_otpValue$index = otpValue[index]) === null || _otpValue$index === void 0 ? void 0 : _otpValue$index.length) > 0 ? 1 : undefined, onChange: function onChange(formEvent) { return handleOnChange(_objectSpread(_objectSpread({}, formEvent), {}, { currentOtpIndex: index })); }, onFocus: function onFocus(formEvent) { return _onFocus === null || _onFocus === void 0 ? void 0 : _onFocus(_objectSpread(_objectSpread({}, formEvent), {}, { inputIndex: index })); }, onBlur: function onBlur(formEvent) { return _onBlur === null || _onBlur === void 0 ? void 0 : _onBlur(_objectSpread(_objectSpread({}, formEvent), {}, { inputIndex: index })); }, onInput: function onInput(formEvent) { return handleOnInput(_objectSpread(_objectSpread({}, formEvent), {}, { currentOtpIndex: index })); }, onKeyDown: function onKeyDown(keyboardEvent) { return handleOnKeyDown(_objectSpread(_objectSpread({}, keyboardEvent), {}, { currentOtpIndex: index })); }, isDisabled: isDisabled, placeholder: (_Array$from$index = Array.from(placeholder !== null && placeholder !== void 0 ? placeholder : '')[index]) !== null && _Array$from$index !== void 0 ? _Array$from$index : '', isRequired: true, autoCompleteSuggestionType: autoCompleteSuggestionType, keyboardType: keyboardType, keyboardReturnKeyType: keyboardReturnKeyType, validationState: validationState, successText: successText, errorText: errorText, helpText: helpText, hideFormHint: true, type: currentInputType, size: size, valueComponentType: "heading" }, makeAnalyticsAttribute(rest))) }, "".concat(inputId, "-").concat(index))); }; for (var index = 0; index < otpLength; index++) { _loop(index); } return inputs; }; return /*#__PURE__*/jsxs(BaseBox, _objectSpread(_objectSpread(_objectSpread({}, metaAttribute({ name: MetaConstants.OTPInput, testID: testID })), getStyledProps(rest)), {}, { children: [/*#__PURE__*/jsxs(BaseBox, { display: "flex", flexDirection: isLabelLeftPositioned ? 'row' : 'column', alignItems: isLabelLeftPositioned ? 'center' : undefined, position: "relative", children: [Boolean(label) && /*#__PURE__*/jsx(FormLabel, { as: "label", position: labelPosition, htmlFor: inputId, size: size, children: label }), /*#__PURE__*/jsxs(BaseBox, { display: "flex", flexDirection: "row", children: [getHiddenInput(), getOTPInputFields()] })] }), /*#__PURE__*/jsx(BaseBox, { marginLeft: makeSize(isLabelLeftPositioned ? 136 : 0), children: /*#__PURE__*/jsx(FormHint, { type: getHintType({ validationState: validationState, hasHelpText: Boolean(helpText) }), helpText: helpText, errorText: errorText, successText: successText, helpTextId: helpTextId, errorTextId: errorTextId, successTextId: successTextId, size: size }) })] })); }; var OTPInput = /*#__PURE__*/React__default.forwardRef(_OTPInput); export { OTPInput }; //# sourceMappingURL=OTPInput.js.map