@heroui/listbox
Version:
A listbox displays a list of options and allows a user to select one or more of them.
328 lines (321 loc) • 10.6 kB
JavaScript
"use client";
import {
listbox_section_default
} from "./chunk-HFV4GF3Z.mjs";
import {
listbox_item_default
} from "./chunk-SDA7RI74.mjs";
// src/virtualized-listbox.tsx
import { useMemo as useMemo2, useRef as useRef2, useState } from "react";
import { mergeProps } from "@react-aria/utils";
import { useVirtualizer } from "@tanstack/react-virtual";
import { isEmpty } from "@heroui/shared-utils";
// ../scroll-shadow/src/use-scroll-shadow.ts
import { mapPropsVariants } from "@heroui/system";
import { scrollShadow } from "@heroui/theme";
import { useDOMRef } from "@heroui/react-utils";
// ../../hooks/use-data-scroll-overflow/src/index.ts
import { capitalize } from "@heroui/shared-utils";
import { useEffect, useRef } from "react";
function useDataScrollOverflow(props = {}) {
const {
domRef,
isEnabled = true,
overflowCheck = "vertical",
visibility = "auto",
offset = 0,
onVisibilityChange,
updateDeps = []
} = props;
const visibleRef = useRef(visibility);
useEffect(() => {
const el = domRef == null ? void 0 : domRef.current;
if (!el || !isEnabled) return;
const setAttributes = (direction, hasBefore, hasAfter, prefix, suffix) => {
if (visibility === "auto") {
const both = `${prefix}${capitalize(suffix)}Scroll`;
if (hasBefore && hasAfter) {
el.dataset[both] = "true";
el.removeAttribute(`data-${prefix}-scroll`);
el.removeAttribute(`data-${suffix}-scroll`);
} else {
el.dataset[`${prefix}Scroll`] = hasBefore.toString();
el.dataset[`${suffix}Scroll`] = hasAfter.toString();
el.removeAttribute(`data-${prefix}-${suffix}-scroll`);
}
} else {
const next = hasBefore && hasAfter ? "both" : hasBefore ? prefix : hasAfter ? suffix : "none";
if (next !== visibleRef.current) {
onVisibilityChange == null ? void 0 : onVisibilityChange(next);
visibleRef.current = next;
}
}
};
const checkOverflow = () => {
var _a, _b;
const directions = [
{ type: "vertical", prefix: "top", suffix: "bottom" },
{ type: "horizontal", prefix: "left", suffix: "right" }
];
const listbox = el.querySelector('ul[data-slot="list"]');
const scrollHeight = +((_a = listbox == null ? void 0 : listbox.getAttribute("data-virtual-scroll-height")) != null ? _a : el.scrollHeight);
const scrollTop = +((_b = listbox == null ? void 0 : listbox.getAttribute("data-virtual-scroll-top")) != null ? _b : el.scrollTop);
for (const { type, prefix, suffix } of directions) {
if (overflowCheck === type || overflowCheck === "both") {
const hasBefore = type === "vertical" ? scrollTop > offset : el.scrollLeft > offset;
const hasAfter = type === "vertical" ? scrollTop + el.clientHeight + offset < scrollHeight : el.scrollLeft + el.clientWidth + offset < el.scrollWidth;
setAttributes(type, hasBefore, hasAfter, prefix, suffix);
}
}
};
const clearOverflow = () => {
["top", "bottom", "top-bottom", "left", "right", "left-right"].forEach((attr) => {
el.removeAttribute(`data-${attr}-scroll`);
});
};
checkOverflow();
el.addEventListener("scroll", checkOverflow, true);
if (visibility !== "auto") {
clearOverflow();
if (visibility === "both") {
el.dataset.topBottomScroll = String(overflowCheck === "vertical");
el.dataset.leftRightScroll = String(overflowCheck === "horizontal");
} else {
el.dataset.topBottomScroll = "false";
el.dataset.leftRightScroll = "false";
["top", "bottom", "left", "right"].forEach((attr) => {
el.dataset[`${attr}Scroll`] = String(visibility === attr);
});
}
}
return () => {
el.removeEventListener("scroll", checkOverflow, true);
clearOverflow();
};
}, [...updateDeps, isEnabled, visibility, overflowCheck, onVisibilityChange, domRef]);
}
// ../scroll-shadow/src/use-scroll-shadow.ts
import { useMemo } from "react";
import { objectToDeps } from "@heroui/shared-utils";
function useScrollShadow(originalProps) {
var _a;
const [props, variantProps] = mapPropsVariants(originalProps, scrollShadow.variantKeys);
const {
ref,
as,
children,
className,
style,
size = 40,
offset = 0,
visibility = "auto",
isEnabled = true,
onVisibilityChange,
...otherProps
} = props;
const Component = as || "div";
const domRef = useDOMRef(ref);
useDataScrollOverflow({
domRef,
offset,
visibility,
isEnabled,
onVisibilityChange,
updateDeps: [children],
overflowCheck: (_a = originalProps.orientation) != null ? _a : "vertical"
});
const styles = useMemo(
() => scrollShadow({
...variantProps,
className
}),
[objectToDeps(variantProps), className]
);
const getBaseProps = (props2 = {}) => {
var _a2;
return {
ref: domRef,
className: styles,
"data-orientation": (_a2 = originalProps.orientation) != null ? _a2 : "vertical",
style: {
"--scroll-shadow-size": `${size}px`,
...style,
...props2.style
},
...otherProps,
...props2
};
};
return { Component, styles, domRef, children, getBaseProps };
}
// src/virtualized-listbox.tsx
import { filterDOMProps } from "@heroui/react-utils";
import { jsx, jsxs } from "react/jsx-runtime";
var getItemSizesForCollection = (collection, itemHeight) => {
const sizes = [];
for (const item of collection) {
if (item.type === "section") {
sizes.push(([...item.childNodes].length + 1) * itemHeight);
} else {
sizes.push(itemHeight);
}
}
return sizes;
};
var getScrollState = (element) => {
if (!element || element.scrollTop === void 0 || element.clientHeight === void 0 || element.scrollHeight === void 0) {
return {
isTop: false,
isBottom: false,
isMiddle: false
};
}
const isAtTop = element.scrollTop === 0;
const isAtBottom = Math.ceil(element.scrollTop + element.clientHeight) >= element.scrollHeight;
const isInMiddle = !isAtTop && !isAtBottom;
return {
isTop: isAtTop,
isBottom: isAtBottom,
isMiddle: isInMiddle
};
};
var VirtualizedListbox = (props) => {
var _a;
const {
Component,
state,
color,
variant,
itemClasses,
getBaseProps,
topContent,
bottomContent,
hideEmptyContent,
hideSelectedIcon,
shouldHighlightOnFocus,
disableAnimation,
getEmptyContentProps,
getListProps,
scrollShadowProps
} = props;
const { virtualization } = props;
if (!virtualization || !isEmpty(virtualization) && !virtualization.maxListboxHeight && !virtualization.itemHeight) {
throw new Error(
"You are using a virtualized listbox. VirtualizedListbox requires 'virtualization' props with 'maxListboxHeight' and 'itemHeight' properties. This error might have originated from autocomplete components that use VirtualizedListbox. Please provide these props to use the virtualized listbox."
);
}
const { maxListboxHeight, itemHeight } = virtualization;
const listHeight = Math.min(maxListboxHeight, itemHeight * state.collection.size);
const parentRef = useRef2(null);
const itemSizes = useMemo2(
() => getItemSizesForCollection([...state.collection], itemHeight),
[state.collection, itemHeight]
);
const rowVirtualizer = useVirtualizer({
count: [...state.collection].length,
getScrollElement: () => parentRef.current,
estimateSize: (i) => itemSizes[i]
});
const virtualItems = rowVirtualizer.getVirtualItems();
const virtualScrollHeight = rowVirtualizer.getTotalSize();
const { getBaseProps: getBasePropsScrollShadow } = useScrollShadow({ ...scrollShadowProps });
const renderRow = (virtualItem) => {
var _a2;
const item = [...state.collection][virtualItem.index];
if (!item) {
return null;
}
const itemProps = {
color,
item,
state,
variant,
disableAnimation,
hideSelectedIcon,
...item.props
};
const virtualizerStyle = {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`
};
if (item.type === "section") {
return /* @__PURE__ */ jsx(
listbox_section_default,
{
...itemProps,
itemClasses,
style: { ...virtualizerStyle, ...itemProps.style }
},
item.key
);
}
let listboxItem = /* @__PURE__ */ jsx(
listbox_item_default,
{
...itemProps,
classNames: mergeProps(itemClasses, (_a2 = item.props) == null ? void 0 : _a2.classNames),
shouldHighlightOnFocus,
style: { ...virtualizerStyle, ...itemProps.style }
},
item.key
);
if (item.wrapper) {
listboxItem = item.wrapper(listboxItem);
}
return listboxItem;
};
const [scrollState, setScrollState] = useState({
isTop: false,
isBottom: true,
isMiddle: false
});
const content = /* @__PURE__ */ jsxs(
Component,
{
...getListProps(),
"data-virtual-scroll-height": virtualScrollHeight,
"data-virtual-scroll-top": (_a = parentRef == null ? void 0 : parentRef.current) == null ? void 0 : _a.scrollTop,
children: [
!state.collection.size && !hideEmptyContent && /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("div", { ...getEmptyContentProps() }) }),
/* @__PURE__ */ jsx(
"div",
{
...filterDOMProps(getBasePropsScrollShadow()),
ref: parentRef,
style: {
height: maxListboxHeight,
overflow: "auto"
},
onScroll: (e) => {
setScrollState(getScrollState(e.target));
},
children: listHeight > 0 && itemHeight > 0 && /* @__PURE__ */ jsx(
"div",
{
style: {
height: `${virtualScrollHeight}px`,
width: "100%",
position: "relative"
},
children: virtualItems.map((virtualItem) => renderRow(virtualItem))
}
)
}
)
]
}
);
return /* @__PURE__ */ jsxs("div", { ...getBaseProps(), children: [
topContent,
content,
bottomContent
] });
};
var virtualized_listbox_default = VirtualizedListbox;
export {
virtualized_listbox_default
};