UNPKG

@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
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;