@codeworker.br/govbr-tw-react
Version:
Biblioteca de componentes React usando Tailwind CSS que implementa o Padrão Digital de Governo.
261 lines (260 loc) • 10.5 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
import { cn } from "../../libs/utils";
import BASE_CLASSNAMES from "../../config/baseClassNames";
import inputVariants from "../Input/variants";
const DEFAULT_DIGITS = 6;
const PATTERN_CHAR = "x";
const SLOT_WIDTH_BY_DENSITY = {
lowest: "w-16",
low: "w-14",
default: "w-12",
high: "w-10",
};
const sanitize = (input) => input.replace(/\D/g, "");
const toValueArray = (value, length) => {
const sanitized = sanitize(value !== null && value !== void 0 ? value : "");
const chars = sanitized.slice(0, length).split("");
return Array.from({ length }, (_, index) => { var _a; return (_a = chars[index]) !== null && _a !== void 0 ? _a : ""; });
};
const parsePattern = (pattern) => {
const tokens = [];
let slotIndex = 0;
let separatorBuffer = "";
const pushSeparator = () => {
if (!separatorBuffer) {
return;
}
tokens.push({
type: "separator",
value: separatorBuffer,
key: `separator-${slotIndex}-${tokens.length}`,
});
separatorBuffer = "";
};
for (const rawChar of pattern) {
const char = rawChar.toLowerCase();
if (char === PATTERN_CHAR) {
pushSeparator();
tokens.push({ type: "slot", index: slotIndex });
slotIndex += 1;
}
else {
separatorBuffer += rawChar;
}
}
pushSeparator();
return { tokens, slotCount: slotIndex };
};
const focusInput = (ref, index) => {
const instance = ref.current[index];
if (instance) {
instance.focus();
instance.select();
}
};
const OtpInput = (_a) => {
var _b;
var { value, defaultValue, onChange, onComplete, digits, pattern, name, disabled, readOnly, autoFocus, placeholder, variant = "default", density = "default", className, slotClassName, separatorClassName, inputClassName } = _a, rest = __rest(_a, ["value", "defaultValue", "onChange", "onComplete", "digits", "pattern", "name", "disabled", "readOnly", "autoFocus", "placeholder", "variant", "density", "className", "slotClassName", "separatorClassName", "inputClassName"]);
const patternInfo = useMemo(() => {
if (!pattern) {
return null;
}
return parsePattern(pattern);
}, [pattern]);
const slotCount = useMemo(() => {
if (patternInfo) {
return patternInfo.slotCount;
}
if (digits && digits > 0) {
return digits;
}
return DEFAULT_DIGITS;
}, [patternInfo, digits]);
useEffect(() => {
if (!patternInfo || digits === undefined) {
return;
}
if (digits === patternInfo.slotCount) {
return;
}
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.warn(`[OtpInput] The provided digits prop (${digits}) does not match the number of slots inferred from the pattern (${patternInfo.slotCount}). Using the pattern derived value.`);
}
}, [digits, patternInfo]);
const tokens = useMemo(() => {
if (patternInfo) {
return patternInfo.tokens;
}
return Array.from({ length: slotCount }, (_, index) => ({
type: "slot",
index,
}));
}, [patternInfo, slotCount]);
const slotWidthClass = (_b = SLOT_WIDTH_BY_DENSITY[(density !== null && density !== void 0 ? density : "default")]) !== null && _b !== void 0 ? _b : SLOT_WIDTH_BY_DENSITY["default"];
const inputRefs = useRef([]);
const isControlled = value !== undefined;
const [internalValues, setInternalValues] = useState(() => toValueArray(defaultValue, slotCount));
useEffect(() => {
inputRefs.current = Array.from({ length: slotCount }, (_, index) => { var _a; return (_a = inputRefs.current[index]) !== null && _a !== void 0 ? _a : null; });
}, [slotCount]);
useEffect(() => {
if (isControlled) {
return;
}
setInternalValues((previous) => {
const next = toValueArray(previous.join(""), slotCount);
return next;
});
}, [isControlled, slotCount]);
useEffect(() => {
if (isControlled || defaultValue === undefined) {
return;
}
setInternalValues(toValueArray(defaultValue, slotCount));
}, [defaultValue, isControlled, slotCount]);
const valueArray = useMemo(() => {
if (isControlled) {
return toValueArray(value, slotCount);
}
return internalValues;
}, [isControlled, value, slotCount, internalValues]);
const emitValue = useCallback((next) => {
if (!isControlled) {
setInternalValues(next);
}
const nextValue = next.join("");
onChange === null || onChange === void 0 ? void 0 : onChange(nextValue);
if (next.length === slotCount && next.every((char) => char)) {
onComplete === null || onComplete === void 0 ? void 0 : onComplete(nextValue);
}
}, [isControlled, onChange, onComplete, slotCount]);
const setValueAtIndex = useCallback((index, nextChars) => {
const next = valueArray.slice();
let cursor = index;
for (const char of nextChars) {
if (cursor >= slotCount) {
break;
}
next[cursor] = char;
cursor += 1;
}
emitValue(next);
if (cursor <= slotCount - 1) {
focusInput(inputRefs, cursor);
}
else {
focusInput(inputRefs, slotCount - 1);
}
}, [emitValue, slotCount, valueArray]);
const clearValueAtIndex = useCallback((index) => {
const next = valueArray.slice();
next[index] = "";
emitValue(next);
}, [emitValue, valueArray]);
const handleSlotChange = useCallback((slotIndex) => (event) => {
if (disabled || readOnly) {
return;
}
const nextChars = sanitize(event.target.value).split("");
if (nextChars.length === 0) {
clearValueAtIndex(slotIndex);
return;
}
setValueAtIndex(slotIndex, nextChars);
}, [clearValueAtIndex, disabled, readOnly, setValueAtIndex]);
const handleSlotFocus = useCallback((event) => {
event.target.select();
}, []);
const focusPrevious = useCallback((current) => {
const previous = current - 1;
if (previous >= 0) {
focusInput(inputRefs, previous);
return previous;
}
return current;
}, []);
const focusNext = useCallback((current) => {
const next = current + 1;
if (next < slotCount) {
focusInput(inputRefs, next);
return next;
}
return current;
}, [slotCount]);
const handleKeyDown = useCallback((index) => (event) => {
if (disabled || readOnly) {
return;
}
if (event.key === "Backspace") {
event.preventDefault();
if (valueArray[index]) {
clearValueAtIndex(index);
}
else {
const previous = focusPrevious(index);
if (valueArray[previous]) {
clearValueAtIndex(previous);
}
}
}
else if (event.key === "ArrowLeft") {
event.preventDefault();
focusPrevious(index);
}
else if (event.key === "ArrowRight") {
event.preventDefault();
focusNext(index);
}
else if (event.key === " " || event.key === "Spacebar") {
event.preventDefault();
}
}, [
clearValueAtIndex,
disabled,
focusNext,
focusPrevious,
readOnly,
valueArray,
]);
const handlePaste = useCallback((index) => (event) => {
var _a, _b;
if (disabled || readOnly) {
return;
}
event.preventDefault();
const pasted = sanitize((_b = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData("text")) !== null && _b !== void 0 ? _b : "");
if (!pasted) {
return;
}
setValueAtIndex(index, pasted.split(""));
}, [disabled, readOnly, setValueAtIndex]);
useEffect(() => {
if (!autoFocus || disabled) {
return;
}
focusInput(inputRefs, 0);
}, [autoFocus, disabled, slotCount]);
return (_jsxs("div", Object.assign({ className: cn("inline-flex items-center gap-2", BASE_CLASSNAMES.otpInput.root, className) }, rest, { children: [name ? _jsx("input", { type: "hidden", name: name, value: valueArray.join("") }) : null, tokens.map((token) => {
if (token.type === "separator") {
return (_jsx("span", { className: cn("text-sm font-medium text-govbr-gray-80", BASE_CLASSNAMES.otpInput.separator, separatorClassName), children: token.value }, token.key));
}
const index = token.index;
return (_jsx("input", { ref: (element) => {
inputRefs.current[index] = element;
}, type: "text", inputMode: "numeric", pattern: "[0-9]*", autoComplete: "one-time-code", className: cn(inputVariants({ variant, density, iconPosition: "none" }), slotWidthClass, "flex-shrink-0 px-0 text-center", BASE_CLASSNAMES.input.root, BASE_CLASSNAMES.otpInput.slot, slotClassName, inputClassName), value: valueArray[index], onChange: handleSlotChange(index), onKeyDown: handleKeyDown(index), onPaste: handlePaste(index), onFocus: handleSlotFocus, placeholder: placeholder, disabled: disabled, readOnly: readOnly, maxLength: 1, "data-filled": valueArray[index] ? "true" : "false", "aria-label": `Codigo OTP digito ${index + 1}` }, `slot-${index}`));
})] })));
};
export default OtpInput;