@codeworker.br/govbr-tw-react
Version:
Biblioteca de componentes React usando Tailwind CSS que implementa o Padrão Digital de Governo.
296 lines (295 loc) • 14.4 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
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 React, { useCallback, useEffect, useMemo, useRef, useState, } from "react";
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import BASE_CLASSNAMES from "../../config/baseClassNames";
import { cn } from "../../libs/utils";
import Input from "../Input";
import { searchBoxMessageVariants, searchBoxOptionVariants, searchBoxPanelVariants, searchBoxRootVariants, } from "./variants";
const DEFAULT_MIN_CHARS = 2;
const DEFAULT_DEBOUNCE = 400;
const resolveKey = (item, key, fallbackKey) => {
if (typeof key === "function") {
const result = key(item);
return result !== null && result !== void 0 ? result : "";
}
const path = (key !== null && key !== void 0 ? key : fallbackKey).split(".");
let cursor = item;
for (const part of path) {
if (cursor !== null &&
typeof cursor === "object" &&
part in cursor) {
cursor = cursor[part];
}
else {
return "";
}
}
if (typeof cursor === "number") {
return cursor.toString();
}
return typeof cursor === "string" ? cursor : "";
};
const defaultRenderOption = (item, state) => (_jsx("div", { className: cn("flex flex-col text-left", state.isActive ? "text-govbr-blue-warm-vivid-70" : "text-govbr-gray-90"), children: _jsx("span", { className: "text-sm font-medium", children: state.label }) }));
const SearchBox = React.forwardRef((_a, ref) => {
var { value, defaultValue = "", minChars = DEFAULT_MIN_CHARS, debounce = DEFAULT_DEBOUNCE, data, fetchUrl, fetchOptions, queryParam = "q", labelKey, valueKey, transformResponse, renderOption = defaultRenderOption, renderSuggestions, onSearchChange, onOptionSelect, getOptionKey, emptyMessage = "Nenhum resultado encontrado", loadingMessage = "Carregando...", errorMessage = "Não foi possível carregar os resultados", density = "default", variant = "default", className, onChange, onBlur, onFocus, onKeyDown } = _a, inputProps = __rest(_a, ["value", "defaultValue", "minChars", "debounce", "data", "fetchUrl", "fetchOptions", "queryParam", "labelKey", "valueKey", "transformResponse", "renderOption", "renderSuggestions", "onSearchChange", "onOptionSelect", "getOptionKey", "emptyMessage", "loadingMessage", "errorMessage", "density", "variant", "className", "onChange", "onBlur", "onFocus", "onKeyDown"]);
const isControlled = value !== undefined;
const [internalValue, setInternalValue] = useState(defaultValue);
const inputValue = isControlled ? value !== null && value !== void 0 ? value : "" : internalValue;
const [query, setQuery] = useState(inputValue !== null && inputValue !== void 0 ? inputValue : "");
const [suggestions, setSuggestions] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [open, setOpen] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const containerRef = useRef(null);
const fetchIdRef = useRef(0);
useEffect(() => {
setQuery(inputValue !== null && inputValue !== void 0 ? inputValue : "");
}, [inputValue]);
const getLabel = useCallback((item) => resolveKey(item, labelKey, "label"), [labelKey]);
const getValueFromItem = useCallback((item) => valueKey ? resolveKey(item, valueKey, "value") : getLabel(item), [valueKey, getLabel]);
const closePanel = useCallback(() => {
setOpen(false);
setHighlightedIndex(-1);
}, []);
const selectItem = useCallback((item) => {
const nextValue = getValueFromItem(item);
if (!isControlled) {
setInternalValue(nextValue);
}
setQuery(nextValue);
onOptionSelect === null || onOptionSelect === void 0 ? void 0 : onOptionSelect(item);
closePanel();
}, [closePanel, getValueFromItem, isControlled, onOptionSelect]);
const handleClickOutside = useCallback((event) => {
if (!containerRef.current) {
return;
}
if (!containerRef.current.contains(event.target)) {
closePanel();
}
}, [closePanel]);
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [handleClickOutside]);
useEffect(() => {
if (query.length < minChars) {
setSuggestions([]);
setOpen(false);
setLoading(false);
setError(null);
return;
}
const currentFetchId = ++fetchIdRef.current;
setLoading(true);
setError(null);
let controller = null;
const timeoutId = window.setTimeout(() => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
try {
if (fetchUrl) {
controller = new AbortController();
const method = ((_a = fetchOptions === null || fetchOptions === void 0 ? void 0 : fetchOptions.method) !== null && _a !== void 0 ? _a : "GET").toUpperCase();
const isPost = method === "POST";
let requestUrl = fetchUrl;
let requestInit = Object.assign(Object.assign({}, fetchOptions), { method, signal: controller.signal });
if (isPost) {
if (!requestInit.body) {
requestInit = Object.assign(Object.assign({}, requestInit), { headers: Object.assign({ "Content-Type": "application/json" }, ((_b = requestInit.headers) !== null && _b !== void 0 ? _b : {})), body: JSON.stringify({ [queryParam]: query }) });
}
}
else {
const params = new URLSearchParams([[queryParam, query]]);
requestUrl = `${fetchUrl}${fetchUrl.includes("?") ? "&" : "?"}${params.toString()}`;
}
const response = yield fetch(requestUrl, requestInit);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = yield response.json();
const mapped = transformResponse
? transformResponse(result)
: Array.isArray(result)
? result
: Array.isArray(result === null || result === void 0 ? void 0 : result.data)
? result.data
: [];
if (fetchIdRef.current === currentFetchId) {
setSuggestions(mapped);
setOpen(true);
setHighlightedIndex(mapped.length > 0 ? 0 : -1);
}
}
else if (Array.isArray(data)) {
const lowered = query.toLowerCase();
const filtered = data.filter((item) => getLabel(item).toLowerCase().includes(lowered));
if (fetchIdRef.current === currentFetchId) {
setSuggestions(filtered);
setOpen(true);
setHighlightedIndex(filtered.length > 0 ? 0 : -1);
}
}
else {
setSuggestions([]);
setOpen(false);
setHighlightedIndex(-1);
}
}
catch (cause) {
if (fetchIdRef.current === currentFetchId) {
setError(cause instanceof Error ? cause.message : String(cause));
setSuggestions([]);
setOpen(true);
setHighlightedIndex(-1);
}
}
finally {
if (fetchIdRef.current === currentFetchId) {
setLoading(false);
}
}
}), debounce);
return () => {
window.clearTimeout(timeoutId);
controller === null || controller === void 0 ? void 0 : controller.abort();
};
}, [
data,
debounce,
fetchOptions,
fetchUrl,
getLabel,
minChars,
query,
queryParam,
transformResponse,
]);
const handleInputChange = (event) => {
const inputText = event.target.value;
if (!isControlled) {
setInternalValue(inputText);
}
setQuery(inputText);
onSearchChange === null || onSearchChange === void 0 ? void 0 : onSearchChange(inputText);
closePanel();
onChange === null || onChange === void 0 ? void 0 : onChange(event);
};
const handleFocus = (event) => {
if (query.length >= minChars && suggestions.length > 0) {
setOpen(true);
}
onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
};
const handleBlur = (event) => {
onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
};
const handleKeyDown = (event) => {
if (!open) {
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
return;
}
if (suggestions.length === 0) {
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
setHighlightedIndex((prev) => prev + 1 >= suggestions.length ? 0 : prev + 1);
}
else if (event.key === "ArrowUp") {
event.preventDefault();
setHighlightedIndex((prev) => prev - 1 < 0 ? suggestions.length - 1 : prev - 1);
}
else if (event.key === "Enter") {
event.preventDefault();
if (highlightedIndex >= 0 && highlightedIndex < suggestions.length) {
selectItem(suggestions[highlightedIndex]);
}
}
else if (event.key === "Escape") {
event.preventDefault();
closePanel();
}
else {
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
}
};
const suggestionsContent = useMemo(() => {
if (!open) {
return null;
}
if (renderSuggestions) {
return renderSuggestions({
items: suggestions,
loading,
error,
query,
select: selectItem,
close: closePanel,
getLabel,
});
}
return (_jsx("div", { className: cn(searchBoxPanelVariants({ variant }), BASE_CLASSNAMES.searchBox.panel), children: loading ? (_jsx("div", { className: cn(searchBoxMessageVariants({ state: "loading", variant })), children: loadingMessage })) : error ? (_jsx("div", { className: cn(searchBoxMessageVariants({ state: "error", variant })), children: errorMessage })) : suggestions.length === 0 ? (_jsx("div", { className: cn(searchBoxMessageVariants({ state: "empty", variant })), children: emptyMessage })) : (suggestions.map((item, index) => {
const label = getLabel(item);
const optionKey = getOptionKey
? getOptionKey(item, index)
: `${label}-${index}`;
const isActive = highlightedIndex === index;
return (_jsx("button", { type: "button", className: cn(searchBoxOptionVariants({
density,
variant,
active: isActive,
}), BASE_CLASSNAMES.searchBox.option), onMouseDown: (event) => {
event.preventDefault();
selectItem(item);
}, onMouseEnter: () => setHighlightedIndex(index), children: renderOption(item, {
index,
isActive,
label,
}) }, optionKey));
})) }));
}, [
closePanel,
emptyMessage,
error,
errorMessage,
getLabel,
getOptionKey,
highlightedIndex,
loading,
loadingMessage,
open,
query,
renderOption,
renderSuggestions,
selectItem,
suggestions,
]);
return (_jsxs("div", { ref: containerRef, className: cn(searchBoxRootVariants({ density, variant }), BASE_CLASSNAMES.searchBox.root), children: [_jsx(Input, Object.assign({ ref: ref, value: inputValue, onChange: handleInputChange, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, density: density, variant: variant, iconPosition: "left", className: className }, inputProps, { children: _jsx(FontAwesomeIcon, { icon: faMagnifyingGlass, className: cn("text-govbr-gray-50", BASE_CLASSNAMES.searchBox.icon) }) })), suggestionsContent] }));
});
SearchBox.displayName = "SearchBox";
export { SearchBox };