@heroui/listbox
Version:
A listbox displays a list of options and allows a user to select one or more of them.
687 lines (675 loc) • 24.5 kB
JavaScript
"use client";
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/virtualized-listbox.tsx
var virtualized_listbox_exports = {};
__export(virtualized_listbox_exports, {
default: () => virtualized_listbox_default
});
module.exports = __toCommonJS(virtualized_listbox_exports);
var import_react6 = require("react");
var import_react_virtual = require("@tanstack/react-virtual");
var import_shared_utils5 = require("@heroui/shared-utils");
// ../scroll-shadow/src/use-scroll-shadow.ts
var import_system = require("@heroui/system");
var import_theme = require("@heroui/theme");
var import_react_utils = require("@heroui/react-utils");
// ../../hooks/use-data-scroll-overflow/src/index.ts
var import_shared_utils = require("@heroui/shared-utils");
var import_react = require("react");
function useDataScrollOverflow(props = {}) {
const {
domRef,
isEnabled = true,
overflowCheck = "vertical",
visibility = "auto",
offset = 0,
onVisibilityChange,
updateDeps = []
} = props;
const visibleRef = (0, import_react.useRef)(visibility);
(0, import_react.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}${(0, import_shared_utils.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
var import_react2 = require("react");
var import_shared_utils2 = require("@heroui/shared-utils");
function useScrollShadow(originalProps) {
var _a;
const [props, variantProps] = (0, import_system.mapPropsVariants)(originalProps, import_theme.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 = (0, import_react_utils.useDOMRef)(ref);
useDataScrollOverflow({
domRef,
offset,
visibility,
isEnabled,
onVisibilityChange,
updateDeps: [children],
overflowCheck: (_a = originalProps.orientation) != null ? _a : "vertical"
});
const styles = (0, import_react2.useMemo)(
() => (0, import_theme.scrollShadow)({
...variantProps,
className
}),
[(0, import_shared_utils2.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
var import_react_utils3 = require("@heroui/react-utils");
// src/listbox-item.tsx
var import_react4 = require("react");
// src/use-listbox-item.ts
var import_react3 = require("react");
var import_theme2 = require("@heroui/theme");
var import_system2 = require("@heroui/system");
var import_focus = require("@react-aria/focus");
var import_react_utils2 = require("@heroui/react-utils");
var import_shared_utils3 = require("@heroui/shared-utils");
var import_listbox = require("@react-aria/listbox");
var import_interactions = require("@react-aria/interactions");
var import_use_is_mobile = require("@heroui/use-is-mobile");
function useListboxItem(originalProps) {
var _a, _b;
const globalContext = (0, import_system2.useProviderContext)();
const [props, variantProps] = (0, import_system2.mapPropsVariants)(originalProps, import_theme2.listboxItem.variantKeys);
const {
as,
item,
state,
description,
startContent,
endContent,
isVirtualized,
selectedIcon,
className,
classNames,
autoFocus,
onPress,
onPressUp,
onPressStart,
onPressEnd,
onPressChange,
onClick,
shouldHighlightOnFocus,
hideSelectedIcon = false,
isReadOnly = false,
...otherProps
} = props;
const disableAnimation = (_b = (_a = originalProps.disableAnimation) != null ? _a : globalContext == null ? void 0 : globalContext.disableAnimation) != null ? _b : false;
const domRef = (0, import_react3.useRef)(null);
const Component = as || (originalProps.href ? "a" : "li");
const shouldFilterDOMProps = typeof Component === "string";
const { rendered, key } = item;
const isDisabled = state.disabledKeys.has(key) || originalProps.isDisabled;
const isSelectable = state.selectionManager.selectionMode !== "none";
const isMobile = (0, import_use_is_mobile.useIsMobile)();
const { pressProps, isPressed } = (0, import_interactions.usePress)({
ref: domRef,
isDisabled,
onClick,
onPress,
onPressUp,
onPressStart,
onPressEnd,
onPressChange
});
const { isHovered, hoverProps } = (0, import_interactions.useHover)({
isDisabled
});
const { isFocusVisible, focusProps } = (0, import_focus.useFocusRing)({
autoFocus
});
const { isFocused, isSelected, optionProps, labelProps, descriptionProps } = (0, import_listbox.useOption)(
{
key,
isDisabled,
"aria-label": props["aria-label"],
isVirtualized
},
state,
domRef
);
let itemProps = optionProps;
const slots = (0, import_react3.useMemo)(
() => (0, import_theme2.listboxItem)({
...variantProps,
isDisabled,
disableAnimation,
hasTitleTextChild: typeof rendered === "string",
hasDescriptionTextChild: typeof description === "string"
}),
[(0, import_shared_utils3.objectToDeps)(variantProps), isDisabled, disableAnimation, rendered, description]
);
const baseStyles = (0, import_theme2.cn)(classNames == null ? void 0 : classNames.base, className);
if (isReadOnly) {
itemProps = (0, import_shared_utils3.removeEvents)(itemProps);
}
const isHighlighted = shouldHighlightOnFocus && isFocused || (isMobile ? isHovered || isPressed : isHovered || isFocused && !isFocusVisible);
const handleFocusCapture = (e) => {
const target = e.target;
const isBlockBubbled = target.closest("[data-slot='startContent']") || target.closest("[data-slot='endContent']");
if (isBlockBubbled) {
e.stopPropagation();
}
};
const getItemProps = (props2 = {}) => ({
ref: domRef,
onFocusCapture: handleFocusCapture,
...(0, import_shared_utils3.mergeProps)(
itemProps,
isReadOnly ? {} : (0, import_shared_utils3.mergeProps)(focusProps, pressProps),
hoverProps,
(0, import_react_utils2.filterDOMProps)(otherProps, {
enabled: shouldFilterDOMProps
}),
props2
),
"data-selectable": (0, import_shared_utils3.dataAttr)(isSelectable),
"data-focus": (0, import_shared_utils3.dataAttr)(isFocused),
"data-hover": (0, import_shared_utils3.dataAttr)(isHighlighted),
"data-disabled": (0, import_shared_utils3.dataAttr)(isDisabled),
"data-selected": (0, import_shared_utils3.dataAttr)(isSelected),
"data-pressed": (0, import_shared_utils3.dataAttr)(isPressed),
"data-focus-visible": (0, import_shared_utils3.dataAttr)(isFocusVisible),
className: slots.base({ class: (0, import_theme2.cn)(baseStyles, props2.className) })
});
const getLabelProps = (props2 = {}) => ({
...(0, import_shared_utils3.mergeProps)(labelProps, props2),
"data-label": (0, import_shared_utils3.dataAttr)(true),
className: slots.title({ class: classNames == null ? void 0 : classNames.title })
});
const getDescriptionProps = (props2 = {}) => ({
...(0, import_shared_utils3.mergeProps)(descriptionProps, props2),
className: slots.description({ class: classNames == null ? void 0 : classNames.description })
});
const getWrapperProps = (props2 = {}) => ({
...(0, import_shared_utils3.mergeProps)(props2),
className: slots.wrapper({ class: classNames == null ? void 0 : classNames.wrapper })
});
const getSelectedIconProps = (0, import_react3.useCallback)(
(props2 = {}) => {
return {
"aria-hidden": (0, import_shared_utils3.dataAttr)(true),
"data-disabled": (0, import_shared_utils3.dataAttr)(isDisabled),
className: slots.selectedIcon({ class: classNames == null ? void 0 : classNames.selectedIcon }),
...props2
};
},
[isDisabled, slots, classNames]
);
return {
Component,
domRef,
slots,
classNames,
isSelectable,
isSelected,
isDisabled,
rendered,
description,
startContent,
endContent,
selectedIcon,
hideSelectedIcon,
disableAnimation,
getItemProps,
getLabelProps,
getWrapperProps,
getDescriptionProps,
getSelectedIconProps
};
}
// src/listbox-selected-icon.tsx
var import_jsx_runtime = require("react/jsx-runtime");
function ListboxSelectedIcon(props) {
const { isSelected, disableAnimation, ...otherProps } = props;
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"svg",
{
"aria-hidden": "true",
"data-selected": isSelected,
role: "presentation",
viewBox: "0 0 17 18",
...otherProps,
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"polyline",
{
fill: "none",
points: "1 9 7 14 15 4",
stroke: "currentColor",
strokeDasharray: 22,
strokeDashoffset: isSelected ? 44 : 66,
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 1.5,
style: !disableAnimation ? {
transition: "stroke-dashoffset 200ms ease"
} : {}
}
)
}
);
}
// src/listbox-item.tsx
var import_jsx_runtime2 = require("react/jsx-runtime");
var ListboxItem = (props) => {
const {
Component,
rendered,
description,
isSelectable,
isSelected,
isDisabled,
selectedIcon,
startContent,
endContent,
hideSelectedIcon,
disableAnimation,
getItemProps,
getLabelProps,
getWrapperProps,
getDescriptionProps,
getSelectedIconProps
} = useListboxItem(props);
const selectedContent = (0, import_react4.useMemo)(() => {
const defaultIcon = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ListboxSelectedIcon, { disableAnimation, isSelected });
if (typeof selectedIcon === "function") {
return selectedIcon({ icon: defaultIcon, isSelected, isDisabled });
}
if (selectedIcon) return selectedIcon;
return defaultIcon;
}, [selectedIcon, isSelected, isDisabled, disableAnimation]);
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Component, { ...getItemProps(), children: [
startContent && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "data-slot": "startContent", children: startContent }),
description ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ...getWrapperProps(), children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { ...getLabelProps(), children: rendered }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { ...getDescriptionProps(), children: description })
] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { ...getLabelProps(), children: rendered }),
isSelectable && !hideSelectedIcon && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { ...getSelectedIconProps(), children: selectedContent }),
endContent && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "data-slot": "endContent", children: endContent })
] });
};
ListboxItem.displayName = "HeroUI.ListboxItem";
var listbox_item_default = ListboxItem;
// src/listbox-section.tsx
var import_theme3 = require("@heroui/theme");
var import_react5 = require("react");
var import_system3 = require("@heroui/system");
var import_shared_utils4 = require("@heroui/shared-utils");
var import_divider = require("@heroui/divider");
var import_listbox2 = require("@react-aria/listbox");
var import_jsx_runtime3 = require("react/jsx-runtime");
var ListboxSection = (0, import_system3.forwardRef)(
({
item,
state,
as,
variant,
color,
disableAnimation,
className,
classNames,
hideSelectedIcon,
showDivider = false,
dividerProps = {},
itemClasses,
// removed title from props to avoid browsers showing a tooltip on hover
// the title props is already inside the rendered prop
// eslint-disable-next-line @typescript-eslint/no-unused-vars
title,
// removed items from props to avoid show in html element
// eslint-disable-next-line @typescript-eslint/no-unused-vars
items,
...otherProps
}, _) => {
const Component = as || "li";
const slots = (0, import_react5.useMemo)(() => (0, import_theme3.listboxSection)(), []);
const baseStyles = (0, import_theme3.cn)(classNames == null ? void 0 : classNames.base, className);
const dividerStyles = (0, import_theme3.cn)(classNames == null ? void 0 : classNames.divider, dividerProps == null ? void 0 : dividerProps.className);
const { itemProps, headingProps, groupProps } = (0, import_listbox2.useListBoxSection)({
heading: item.rendered,
"aria-label": item["aria-label"]
});
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
Component,
{
"data-slot": "base",
...(0, import_shared_utils4.mergeProps)(itemProps, otherProps),
className: slots.base({ class: baseStyles }),
children: [
item.rendered && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"span",
{
...headingProps,
className: slots.heading({ class: classNames == null ? void 0 : classNames.heading }),
"data-slot": "heading",
children: item.rendered
}
),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"ul",
{
...groupProps,
className: slots.group({ class: classNames == null ? void 0 : classNames.group }),
"data-has-title": !!item.rendered,
"data-slot": "group",
children: [
[...item.childNodes].map((node) => {
const { key: nodeKey, props: nodeProps } = node;
let listboxItem2 = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
listbox_item_default,
{
classNames: itemClasses,
color,
disableAnimation,
hideSelectedIcon,
item: node,
state,
variant,
...nodeProps
},
nodeKey
);
if (node.wrapper) {
listboxItem2 = node.wrapper(listboxItem2);
}
return listboxItem2;
}),
showDivider && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
import_divider.Divider,
{
as: "li",
className: slots.divider({
class: dividerStyles
}),
...dividerProps
}
)
]
}
)
]
},
item.key
);
}
);
ListboxSection.displayName = "HeroUI.ListboxSection";
var listbox_section_default = ListboxSection;
// src/virtualized-listbox.tsx
var import_jsx_runtime4 = require("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 || !(0, import_shared_utils5.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 = (0, import_react6.useRef)(null);
const itemSizes = (0, import_react6.useMemo)(
() => getItemSizesForCollection([...state.collection], itemHeight),
[state.collection, itemHeight]
);
const rowVirtualizer = (0, import_react_virtual.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__ */ (0, import_jsx_runtime4.jsx)(
listbox_section_default,
{
...itemProps,
itemClasses,
style: { ...virtualizerStyle, ...itemProps.style }
},
item.key
);
}
let listboxItem2 = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
listbox_item_default,
{
...itemProps,
classNames: (0, import_shared_utils5.mergeProps)(itemClasses, (_a2 = item.props) == null ? void 0 : _a2.classNames),
shouldHighlightOnFocus,
style: { ...virtualizerStyle, ...itemProps.style }
},
item.key
);
if (item.wrapper) {
listboxItem2 = item.wrapper(listboxItem2);
}
return listboxItem2;
};
const [scrollState, setScrollState] = (0, import_react6.useState)({
isTop: false,
isBottom: true,
isMiddle: false
});
const content = /* @__PURE__ */ (0, import_jsx_runtime4.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__ */ (0, import_jsx_runtime4.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ...getEmptyContentProps() }) }),
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"div",
{
...(0, import_react_utils3.filterDOMProps)(getBasePropsScrollShadow()),
ref: parentRef,
style: {
height: maxListboxHeight,
overflow: "auto"
},
onScroll: (e) => {
setScrollState(getScrollState(e.target));
},
children: listHeight > 0 && itemHeight > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"div",
{
style: {
height: `${virtualScrollHeight}px`,
width: "100%",
position: "relative"
},
children: virtualItems.map((virtualItem) => renderRow(virtualItem))
}
)
}
)
]
}
);
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { ...getBaseProps(), children: [
topContent,
content,
bottomContent
] });
};
var virtualized_listbox_default = VirtualizedListbox;