UNPKG

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