@razorpay/blade
Version:
The Design System that powers Razorpay
383 lines (376 loc) • 18 kB
JavaScript
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