analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
484 lines (464 loc) • 20.3 kB
JavaScript
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
var _chunkLGPTKTMPjs = require('./chunk-LGPTKTMP.js');
var _chunk34ST3MKOjs = require('./chunk-34ST3MKO.js');
var _chunkANT66KVKjs = require('./chunk-ANT66KVK.js');
var _chunkTN3AYOMVjs = require('./chunk-TN3AYOMV.js');
// src/components/SearchSelect/SearchSelect.tsx
var _react = require('react');
var _reactdom = require('react-dom');
var _CaretDown = require('@phosphor-icons/react/dist/csr/CaretDown');
var _Check = require('@phosphor-icons/react/dist/csr/Check');
var _MagnifyingGlass = require('@phosphor-icons/react/dist/csr/MagnifyingGlass');
var _SpinnerGap = require('@phosphor-icons/react/dist/csr/SpinnerGap');
var _jsxruntime = require('react/jsx-runtime');
var SIZE_CLASSES = {
small: "text-sm h-8",
medium: "text-md h-9",
large: "text-lg h-10"
};
var PADDING_CLASSES = {
small: "px-3 py-1",
medium: "px-3 py-2",
large: "px-4 py-3"
};
var VARIANT_CLASSES = {
outlined: "border-2 rounded-lg focus:border-primary-950",
underlined: "border-b-2 focus:border-primary-950",
rounded: "border-2 rounded-full focus:border-primary-950"
};
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = _react.useState.call(void 0, value);
_react.useEffect.call(void 0, () => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
var SearchSelect = _react.forwardRef.call(void 0,
({
value,
onValueChange,
options,
placeholder = "Selecione...",
searchPlaceholder = "Buscar...",
label,
helperText,
errorMessage,
disabled = false,
loading = false,
size = "small",
variant = "outlined",
className,
emptyText = "Nenhum resultado encontrado",
loadingText = "Carregando...",
onSearch,
searchDebounce = 300,
pagination,
onLoadMore,
loadingMore = false,
id,
filterLocally = true
}, ref) => {
const [open, setOpen] = _react.useState.call(void 0, false);
const [searchQuery, setSearchQuery] = _react.useState.call(void 0, "");
const [highlightedIndex, setHighlightedIndex] = _react.useState.call(void 0, -1);
const triggerRef = _react.useRef.call(void 0, null);
const contentRef = _react.useRef.call(void 0, null);
const searchInputRef = _react.useRef.call(void 0, null);
const listRef = _react.useRef.call(void 0, null);
const isFetchingMoreRef = _react.useRef.call(void 0, false);
const generatedId = _react.useId.call(void 0, );
const selectId = _nullishCoalesce(id, () => ( `search-select-${generatedId}`));
const listboxId = `${selectId}-listbox`;
const helperId = `${selectId}-helper`;
const errorId = `${selectId}-error`;
const debouncedSearch = useDebounce(searchQuery, searchDebounce);
_react.useEffect.call(void 0, () => {
if (onSearch && debouncedSearch !== void 0) {
onSearch(debouncedSearch);
}
}, [debouncedSearch, onSearch]);
const filteredOptions = _react.useMemo.call(void 0, () => {
if (!filterLocally || !searchQuery) {
return options;
}
const query = searchQuery.toLowerCase();
return options.filter((opt) => opt.label.toLowerCase().includes(query));
}, [options, searchQuery, filterLocally]);
_react.useEffect.call(void 0, () => {
if (highlightedIndex >= filteredOptions.length) {
setHighlightedIndex(Math.max(filteredOptions.length - 1, -1));
}
}, [filteredOptions.length, highlightedIndex]);
const selectedLabel = _react.useMemo.call(void 0, () => {
const selected = options.find((opt) => opt.value === value);
return _optionalChain([selected, 'optionalAccess', _ => _.label]);
}, [options, value]);
const [triggerRect, setTriggerRect] = _react.useState.call(void 0, null);
const updateTriggerRect = _react.useCallback.call(void 0, () => {
if (triggerRef.current) {
setTriggerRect(triggerRef.current.getBoundingClientRect());
}
}, []);
const handleToggle = _react.useCallback.call(void 0, () => {
if (disabled || loading) return;
const newOpen = !open;
if (newOpen) {
updateTriggerRect();
setSearchQuery("");
setHighlightedIndex(-1);
}
setOpen(newOpen);
}, [disabled, loading, open, updateTriggerRect]);
const handleSelect = _react.useCallback.call(void 0,
(optionValue) => {
_optionalChain([onValueChange, 'optionalCall', _2 => _2(optionValue)]);
setOpen(false);
setSearchQuery("");
requestAnimationFrame(() => {
_optionalChain([triggerRef, 'access', _3 => _3.current, 'optionalAccess', _4 => _4.focus, 'call', _5 => _5()]);
});
},
[onValueChange]
);
_react.useEffect.call(void 0, () => {
if (open && searchInputRef.current) {
setTimeout(() => {
_optionalChain([searchInputRef, 'access', _6 => _6.current, 'optionalAccess', _7 => _7.focus, 'call', _8 => _8()]);
}, 0);
}
}, [open]);
_react.useEffect.call(void 0, () => {
if (!open) return;
const handleClickOutside = (event) => {
const target = event.target;
const isInsideTrigger = _optionalChain([triggerRef, 'access', _9 => _9.current, 'optionalAccess', _10 => _10.contains, 'call', _11 => _11(target)]);
const isInsideContent = _optionalChain([contentRef, 'access', _12 => _12.current, 'optionalAccess', _13 => _13.contains, 'call', _14 => _14(target)]);
if (!isInsideTrigger && !isInsideContent) {
setOpen(false);
setSearchQuery("");
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [open]);
_react.useEffect.call(void 0, () => {
if (!open) return;
const handleUpdate = () => updateTriggerRect();
window.addEventListener("scroll", handleUpdate, true);
window.addEventListener("resize", handleUpdate);
return () => {
window.removeEventListener("scroll", handleUpdate, true);
window.removeEventListener("resize", handleUpdate);
};
}, [open, updateTriggerRect]);
const findNextEnabledIndex = _react.useCallback.call(void 0,
(currentIndex, direction) => {
const len = filteredOptions.length;
if (len === 0) return -1;
let startIndex;
if (currentIndex < 0) {
startIndex = direction === "next" ? -1 : len;
} else {
startIndex = currentIndex;
}
for (let i = 0; i < len; i++) {
if (direction === "next") {
startIndex = (startIndex + 1) % len;
} else {
startIndex = (startIndex - 1 + len) % len;
}
if (!filteredOptions[startIndex].disabled) {
return startIndex;
}
}
return -1;
},
[filteredOptions]
);
const handleKeyDown = _react.useCallback.call(void 0,
(e) => {
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setHighlightedIndex((prev) => findNextEnabledIndex(prev, "next"));
break;
case "ArrowUp":
e.preventDefault();
setHighlightedIndex((prev) => findNextEnabledIndex(prev, "prev"));
break;
case "Enter":
e.preventDefault();
if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length && !filteredOptions[highlightedIndex].disabled) {
handleSelect(filteredOptions[highlightedIndex].value);
}
break;
case "Escape":
e.preventDefault();
setOpen(false);
setSearchQuery("");
_optionalChain([triggerRef, 'access', _15 => _15.current, 'optionalAccess', _16 => _16.focus, 'call', _17 => _17()]);
break;
}
},
[filteredOptions, highlightedIndex, handleSelect, findNextEnabledIndex]
);
_react.useEffect.call(void 0, () => {
if (!loadingMore) {
isFetchingMoreRef.current = false;
}
}, [loadingMore]);
const handleScroll = _react.useCallback.call(void 0, () => {
if (!listRef.current || !_optionalChain([pagination, 'optionalAccess', _18 => _18.hasNext]) || loadingMore || isFetchingMoreRef.current || !onLoadMore)
return;
const { scrollTop, scrollHeight, clientHeight } = listRef.current;
const scrollThreshold = 50;
if (scrollHeight - scrollTop - clientHeight < scrollThreshold) {
isFetchingMoreRef.current = true;
const result = onLoadMore();
if (result && typeof result.then === "function") {
result.finally(() => {
isFetchingMoreRef.current = false;
});
}
}
}, [_optionalChain([pagination, 'optionalAccess', _19 => _19.hasNext]), loadingMore, onLoadMore]);
_react.useEffect.call(void 0, () => {
if (highlightedIndex >= 0 && listRef.current) {
const items = listRef.current.querySelectorAll("[data-option]");
const item = items[highlightedIndex];
if (item) {
item.scrollIntoView({ block: "nearest" });
}
}
}, [highlightedIndex]);
const setRefs = _react.useCallback.call(void 0,
(element) => {
triggerRef.current = element;
if (typeof ref === "function") {
ref(element);
} else if (ref) {
ref.current = element;
}
},
[ref]
);
const sizeClasses = SIZE_CLASSES[size];
const paddingClasses = PADDING_CLASSES[size];
const variantClasses = VARIANT_CLASSES[variant];
const invalid = !!errorMessage;
const getDropdownStyles = _react.useCallback.call(void 0, () => {
if (!triggerRect) return {};
const gap = 4;
const dropdownMaxHeight = 300;
const viewportHeight = window.innerHeight;
const spaceBelow = viewportHeight - triggerRect.bottom - gap;
const spaceAbove = triggerRect.top - gap;
const openAbove = spaceBelow < dropdownMaxHeight && spaceAbove > spaceBelow;
const styles = {
position: "fixed",
left: triggerRect.left,
width: triggerRect.width,
zIndex: 9999,
maxHeight: openAbove ? Math.min(spaceAbove, dropdownMaxHeight) : Math.min(spaceBelow, dropdownMaxHeight)
};
if (openAbove) {
styles.bottom = viewportHeight - triggerRect.top + gap;
} else {
styles.top = triggerRect.bottom + gap;
}
return styles;
}, [triggerRect]);
const renderOptionsContent = () => {
if (loading) {
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center justify-center gap-2 p-4 text-text-500", children: [
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _SpinnerGap.SpinnerGapIcon, { size: 18, className: "animate-spin" }),
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkANT66KVKjs.Text_default, { size: "sm", className: "text-text-500", children: loadingText })
] });
}
if (filteredOptions.length === 0) {
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkANT66KVKjs.Text_default, { size: "sm", className: "p-4 text-center text-text-500", children: emptyText });
}
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
filteredOptions.map((option, index) => {
const isSelected = option.value === value;
const isHighlighted = index === highlightedIndex;
const optionId = `${listboxId}-option-${index}`;
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
"div",
{
id: optionId,
"data-option": true,
role: "option",
"aria-selected": isSelected,
"aria-disabled": option.disabled,
tabIndex: -1,
onClick: () => {
if (!option.disabled) {
handleSelect(option.value);
}
},
onKeyDown: (e) => {
if (option.disabled) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSelect(option.value);
}
},
className: _chunkTN3AYOMVjs.cn.call(void 0,
"relative flex items-center gap-2 px-3 py-2.5 cursor-pointer transition-colors text-sm",
isSelected && "bg-primary-50",
isHighlighted && !isSelected && "bg-background-50",
option.disabled && "opacity-50 cursor-not-allowed pointer-events-none",
!option.disabled && !isSelected && !isHighlighted && "hover:bg-background-50"
),
children: [
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkANT66KVKjs.Text_default, { size: "sm", className: "flex-1 text-text-700", children: option.label }),
isSelected && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
_Check.CheckIcon,
{
size: 16,
className: "text-primary-700",
weight: "bold"
}
)
]
},
option.value
);
}),
loadingMore && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center justify-center gap-2 p-3 text-text-500 border-t border-border-100", children: [
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _SpinnerGap.SpinnerGapIcon, { size: 16, className: "animate-spin" }),
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkANT66KVKjs.Text_default, { size: "xs", className: "text-text-500", children: "Carregando mais..." })
] }),
pagination && !loadingMore && pagination.total > 0 && !(filterLocally && searchQuery) && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
_chunkANT66KVKjs.Text_default,
{
size: "xs",
className: "px-3 py-2 text-text-400 border-t border-border-100 text-center",
children: [
options.length,
" de ",
pagination.total,
" itens"
]
}
)
] });
};
const dropdownContent = open && triggerRect && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
"div",
{
ref: contentRef,
"data-search-select-id": selectId,
style: getDropdownStyles(),
className: "bg-secondary rounded-md border border-border-100 shadow-lg overflow-hidden flex flex-col",
children: [
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "p-2 border-b border-border-100", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
_chunkLGPTKTMPjs.Input_default,
{
ref: searchInputRef,
type: "text",
role: "combobox",
"aria-autocomplete": "list",
"aria-controls": listboxId,
"aria-activedescendant": highlightedIndex >= 0 ? `${listboxId}-option-${highlightedIndex}` : void 0,
value: searchQuery,
onChange: (e) => setSearchQuery(e.target.value),
onKeyDown: handleKeyDown,
placeholder: searchPlaceholder,
size: "small",
variant: "outlined",
iconRight: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _MagnifyingGlass.MagnifyingGlassIcon, { size: 16 }),
containerClassName: "w-full"
}
) }),
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
"div",
{
ref: listRef,
id: listboxId,
role: "listbox",
"aria-label": label || "Options",
onScroll: handleScroll,
className: "flex-1 overflow-y-auto",
children: renderOptionsContent()
}
)
]
}
);
const getDescribedById = () => {
if (errorMessage) return errorId;
if (helperText) return helperId;
return void 0;
};
const describedById = getDescribedById();
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: _chunkTN3AYOMVjs.cn.call(void 0, "w-full", className), children: [
label && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
"label",
{
htmlFor: selectId,
className: _chunkTN3AYOMVjs.cn.call(void 0,
"block font-bold text-text-900 mb-1.5",
size === "small" && "text-sm",
size === "medium" && "text-md",
size === "large" && "text-lg"
),
children: label
}
),
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
_chunk34ST3MKOjs.Button_default,
{
ref: setRefs,
id: selectId,
variant: "raw",
onClick: handleToggle,
disabled: disabled || loading,
"aria-expanded": open,
"aria-haspopup": "listbox",
"aria-controls": open ? listboxId : void 0,
"aria-invalid": !!errorMessage,
"aria-describedby": describedById,
className: _chunkTN3AYOMVjs.cn.call(void 0,
"flex w-full items-center justify-between border-border-300 bg-background",
sizeClasses,
paddingClasses,
variantClasses,
invalid && "border-indicator-error",
disabled || loading ? "cursor-not-allowed opacity-50 text-text-400" : "cursor-pointer hover:bg-background-50 text-text-700"
),
iconRight: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
_CaretDown.CaretDownIcon,
{
className: _chunkTN3AYOMVjs.cn.call(void 0,
"h-4 w-4 opacity-50 transition-transform shrink-0",
open && "rotate-180"
)
}
),
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: _chunkTN3AYOMVjs.cn.call(void 0, "truncate", !selectedLabel && "text-text-500"), children: loading ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _SpinnerGap.SpinnerGapIcon, { size: 14, className: "animate-spin" }),
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkANT66KVKjs.Text_default, { size: "sm", className: "text-text-500", children: loadingText })
] }) : selectedLabel || placeholder })
}
),
(helperText || errorMessage) && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mt-1.5", children: [
helperText && !errorMessage && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkANT66KVKjs.Text_default, { id: helperId, size: "sm", className: "text-text-500", children: helperText }),
errorMessage && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkANT66KVKjs.Text_default, { id: errorId, size: "sm", className: "text-indicator-error", children: errorMessage })
] }),
typeof document !== "undefined" && _reactdom.createPortal.call(void 0, dropdownContent, document.body)
] });
}
);
SearchSelect.displayName = "SearchSelect";
var SearchSelect_default = SearchSelect;
exports.SearchSelect = SearchSelect; exports.SearchSelect_default = SearchSelect_default;
//# sourceMappingURL=chunk-C5FRUHXC.js.map