UNPKG

@gravity-ui/uikit

Version:

Gravity UI base styling and components

203 lines (202 loc) 9.5 kB
'use client'; "use strict"; 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