@gravity-ui/uikit
Version:
Gravity UI base styling and components
203 lines (202 loc) • 9.5 kB
JavaScript
'use client';
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PinInput = void 0;
const tslib_1 = require("tslib");
const jsx_runtime_1 = require("react/jsx-runtime");
const React = tslib_1.__importStar(require("react"));
const constants_1 = require("../../constants.js");
const hooks_1 = require("../../hooks/index.js");
const private_1 = require("../../hooks/private/index.js");
const controls_1 = require("../controls/index.js");
const OuterAdditionalContent_1 = require("../controls/common/OuterAdditionalContent/OuterAdditionalContent.js");
const theme_1 = require("../theme/index.js");
const cn_1 = require("../utils/cn.js");
const filterDOMProps_1 = require("../utils/filterDOMProps.js");
const i18n_1 = tslib_1.__importDefault(require("./i18n/index.js"));
require("./PinInput.css");
const b = (0, cn_1.block)('pin-input');
const NUMERIC_REGEXP = /[0-9]+/;
const ALPHANUMERIC_REGEXP = /[0-9a-z]+/i;
const validate = (type, newValue) => {
if (type === 'numeric') {
return NUMERIC_REGEXP.test(newValue);
}
else {
return ALPHANUMERIC_REGEXP.test(newValue);
}
};
exports.PinInput = React.forwardRef((props, ref) => {
const { value, defaultValue, onUpdate, onUpdateComplete, onFocus, onBlur, length = 4, size = 'm', type = 'numeric', id: idProp, name, form, placeholder, disabled, autoFocus, otp, mask, responsive, note, validationState, errorMessage, apiRef, className, style, qa, ...restProps } = props;
const refs = React.useRef({});
const [activeIndex, setActiveIndex] = React.useState(0);
const [focusedIndex, setFocusedIndex] = React.useState(-1);
const updateCallback = React.useCallback((newValue) => {
if (onUpdate) {
onUpdate(newValue);
}
if (onUpdateComplete && newValue.every((v) => Boolean(v))) {
onUpdateComplete(newValue);
}
}, [onUpdate, onUpdateComplete]);
const [values, setValues] = (0, hooks_1.useControlledState)(value, defaultValue ?? Array.from({ length }, () => ''), updateCallback);
const direction = (0, theme_1.useDirection)();
const errorMessageId = (0, hooks_1.useUniqId)();
const noteId = (0, hooks_1.useUniqId)();
const isErrorMsgVisible = validationState === 'invalid' && errorMessage;
const ariaDescribedBy = [
props?.['aria-describedby'],
note ? noteId : undefined,
isErrorMsgVisible ? errorMessageId : undefined,
]
.filter(Boolean)
.join(' ');
const handleRef = (index, inputRef) => {
refs.current[index] = inputRef;
};
const focus = (index) => {
setActiveIndex(index);
refs.current[index]?.focus();
};
const focusPrev = (index) => {
if (index > 0) {
focus(index - 1);
}
};
const focusNext = (index) => {
if (index < length - 1) {
focus(index + 1);
}
};
const setValuesAtIndex = (index, nextValue) => {
// Normalize array size to length prop
const newValues = Array.from({ length }, (__, i) => values[i] ?? '');
if (nextValue.length > 0) {
// Fill the subsequent inputs as well as the target input
for (let k = 0; k < nextValue.length && index + k < newValues.length; k++) {
newValues[index + k] = nextValue[k];
}
}
else {
newValues[index] = '';
}
// If values are the same then do not update
if (newValues.every((__, i) => newValues[i] === values[i])) {
return;
}
setValues(newValues);
};
const handleInputChange = (i, event) => {
let nextValue = event.currentTarget.value;
const currentValue = values[i];
if (currentValue) {
// Remove the current value from the new value
if (currentValue === nextValue[0]) {
nextValue = nextValue.slice(1);
}
else if (currentValue === nextValue[nextValue.length - 1]) {
nextValue = nextValue.slice(0, -1);
}
}
if (!validate(type, nextValue)) {
return;
}
// If value's length greater than 1, then it's a paste so inserting at the start
if (nextValue.length > 1) {
setValuesAtIndex(0, nextValue);
focusNext(nextValue.length - 1);
}
else {
setValuesAtIndex(i, nextValue);
focusNext(i);
}
};
const handleInputKeyDown = (i, event) => {
switch (event.code) {
case constants_1.KeyCode.BACKSPACE:
event.preventDefault();
if (event.currentTarget.value) {
setValuesAtIndex(i, '');
}
else if (i > 0) {
setValuesAtIndex(i - 1, '');
focusPrev(i);
}
break;
case constants_1.KeyCode.ARROW_LEFT:
case constants_1.KeyCode.ARROW_UP:
event.preventDefault();
if (direction === 'rtl' && event.code === constants_1.KeyCode.ARROW_LEFT) {
focusNext(i);
}
else {
focusPrev(i);
}
break;
case constants_1.KeyCode.ARROW_RIGHT:
case constants_1.KeyCode.ARROW_DOWN:
event.preventDefault();
if (direction === 'rtl' && event.code === constants_1.KeyCode.ARROW_RIGHT) {
focusPrev(i);
}
else {
focusNext(i);
}
break;
}
};
const handleFocus = (index) => {
setFocusedIndex(index);
setActiveIndex(index);
};
const handleBlur = () => {
setFocusedIndex(-1);
};
React.useEffect(() => {
if (autoFocus) {
focus(0);
}
// We only care about autofocus on initial render
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useImperativeHandle(apiRef, () => ({
focus: () => {
refs.current[activeIndex]?.focus();
},
}), [activeIndex]);
const formInputRef = (0, private_1.useFormResetHandler)({ initialValue: values, onReset: setValues });
const { focusWithinProps } = (0, hooks_1.useFocusWithin)({
onFocusWithin: onFocus,
onBlurWithin: onBlur,
});
let id = (0, hooks_1.useUniqId)();
if (idProp) {
id = idProp;
}
return ((0, jsx_runtime_1.jsxs)("div", { ref: ref, ...(0, filterDOMProps_1.filterDOMProps)(restProps, { labelable: true }), ...focusWithinProps, className: b({ size, responsive }, className), style: style, "data-qa": qa, role: "group", id: id, "aria-describedby": ariaDescribedBy, children: [(0, jsx_runtime_1.jsxs)("div", { className: b('items'), children: [Array.from({ length }).map((__, i) => {
const inputId = `${id}-${i}`;
const ariaLabelledBy = props['aria-labelledby'] || props['aria-label']
? [inputId, props['aria-labelledby'] || id].join(' ')
: undefined;
return ((0, jsx_runtime_1.jsx)("div", { className: b('item'), children: (0, jsx_runtime_1.jsx)(controls_1.TextInput
// Only pick first symbol while keeping input always controlled
, {
// Only pick first symbol while keeping input always controlled
value: values[i]?.[0] ?? '', tabIndex: activeIndex === i ? 0 : -1, type: mask ? 'password' : 'text', size: size, id: inputId, disabled: disabled, placeholder: focusedIndex === i ? undefined : placeholder, autoComplete: otp ? 'one-time-code' : 'off', validationState: validationState, controlProps: {
inputMode: type === 'numeric' ? 'numeric' : 'text',
pattern: type === 'numeric' ? '[0-9]*' : '[0-9a-zA-Z]*',
className: b('control'),
autoCapitalize: 'none',
'aria-label': (0, i18n_1.default)('label_one-of', {
number: i + 1,
count: length,
}),
'aria-labelledby': ariaLabelledBy,
'aria-describedby': ariaDescribedBy,
'aria-details': props['aria-details'],
'aria-invalid': validationState === 'invalid' ? true : undefined,
}, controlRef: handleRef.bind(null, i), onChange: handleInputChange.bind(null, i), onKeyDown: handleInputKeyDown.bind(null, i), onFocus: handleFocus.bind(null, i), onBlur: handleBlur }) }, i));
}), name ? ((0, jsx_runtime_1.jsx)("input", { ref: formInputRef, type: "hidden", name: name, form: form, value: values.join(''), disabled: disabled })) : null] }), (0, jsx_runtime_1.jsx)(OuterAdditionalContent_1.OuterAdditionalContent, { note: note, errorMessage: isErrorMsgVisible ? errorMessage : null, noteId: noteId, errorMessageId: errorMessageId })] }));
});
exports.PinInput.displayName = 'PinInput';
//# sourceMappingURL=PinInput.js.map