diff --git a/packages/components/listbox/src/virtualized-listbox.tsx b/packages/components/listbox/src/virtualized-listbox.tsx index 0a36766204..48c9499356 100644 --- a/packages/components/listbox/src/virtualized-listbox.tsx +++ b/packages/components/listbox/src/virtualized-listbox.tsx @@ -1,7 +1,8 @@ -import {useRef} from "react"; +import {useMemo, useRef} from "react"; import {mergeProps} from "@react-aria/utils"; import {useVirtualizer} from "@tanstack/react-virtual"; import {isEmpty} from "@nextui-org/shared-utils"; +import {Node} from "@react-types/shared"; import ListboxItem from "./listbox-item"; import ListboxSection from "./listbox-section"; @@ -13,6 +14,21 @@ interface Props extends UseListboxReturn { virtualization?: VirtualizationProps; } +const getItemSizesForCollection = (collection: Node[], itemHeight: number) => { + const sizes: number[] = []; + + for (const item of collection) { + if (item.type === "section") { + /* +1 for the section header */ + sizes.push(([...item.childNodes].length + 1) * itemHeight); + } else { + sizes.push(itemHeight); + } + } + + return sizes; +}; + const VirtualizedListbox = (props: Props) => { const { Component, @@ -46,16 +62,20 @@ const VirtualizedListbox = (props: Props) => { const listHeight = Math.min(maxListboxHeight, itemHeight * state.collection.size); const parentRef = useRef(null); + const itemSizes = useMemo( + () => getItemSizesForCollection([...state.collection], itemHeight), + [state.collection, itemHeight], + ); const rowVirtualizer = useVirtualizer({ - count: state.collection.size, + count: [...state.collection].length, getScrollElement: () => parentRef.current, - estimateSize: () => itemHeight, + estimateSize: (i) => itemSizes[i], }); const virtualItems = rowVirtualizer.getVirtualItems(); - const renderRow = ({ + const ListBoxRow = ({ index, style: virtualizerStyle, }: { @@ -64,6 +84,10 @@ const VirtualizedListbox = (props: Props) => { }) => { const item = [...state.collection][index]; + if (!item) { + return null; + } + const itemProps = { color, item, @@ -111,6 +135,7 @@ const VirtualizedListbox = (props: Props) => { )}
{ position: "relative", }} > - {virtualItems.map((virtualItem) => - renderRow({ - index: virtualItem.index, - style: { - position: "absolute", - top: 0, - left: 0, - width: "100%", - height: `${virtualItem.size}px`, - transform: `translateY(${virtualItem.start}px)`, - }, - }), - )} + {virtualItems.map((virtualItem) => { + return ( + + ); + })}
)} diff --git a/packages/components/select/src/use-select.ts b/packages/components/select/src/use-select.ts index e563cd1f7d..09aa8fd993 100644 --- a/packages/components/select/src/use-select.ts +++ b/packages/components/select/src/use-select.ts @@ -161,7 +161,9 @@ export type UseSelectProps = Omit< SelectVariantProps & { /** * The height of each item in the listbox. + * For dataset with sections, the itemHeight must be the height of each item (including padding, border, margin). * This is required for virtualized listboxes to calculate the height of each item. + * @default 36 */ itemHeight?: number; /** @@ -208,7 +210,7 @@ export function useSelect(originalProps: UseSelectProps) { onSelectionChange, placeholder, isVirtualized, - itemHeight = 32, + itemHeight = 36, maxListboxHeight = 256, children, disallowEmptySelection = false, diff --git a/packages/components/select/stories/select.stories.tsx b/packages/components/select/stories/select.stories.tsx index 34136d2bb3..6fac92d091 100644 --- a/packages/components/select/stories/select.stories.tsx +++ b/packages/components/select/stories/select.stories.tsx @@ -1391,6 +1391,85 @@ export const CustomItemHeight = { }, }; +const AVATAR_DECORATIONS: {[key: string]: string[]} = { + arcane: ["jinx", "atlas-gauntlets", "flame-chompers", "fishbones", "hexcore", "shimmer"], + anime: ["cat-ears", "heart-bloom", "in-love", "in-tears", "soul-leaving-body", "starry-eyed"], + "lofi-vibes": ["chromawave", "cozy-cat", "cozy-headphones", "doodling", "rainy-mood"], + valorant: [ + "a-hint-of-clove", + "blade-storm", + "cypher", + "frag-out", + "omen-cowl", + "reyna-leer", + "vct-supernova", + "viper", + "yoru", + "carnalito", + "a-hint-of-clove", + "blade-storm", + "cypher", + "frag-out", + "omen-cowl", + "reyna-leer", + "vct-supernova", + "viper", + "yoru", + "carnalito", + ], + spongebob: [ + "flower-clouds", + "gary-the-snail", + "imagination", + "musclebob", + "sandy-cheeks", + "spongebob", + ], + arcade: ["clyde-invaders", "hot-shot", "joystick", "mallow-jump", "pipedream", "snake"], + "street-fighter": ["akuma", "cammy", "chun-li", "guile", "juri", "ken", "m.bison", "ryu"], +}; + +export const NonVirtualizedVsVirtualizedWithSections = { + render: () => { + const SelectComponent = ({isVirtualized}: {isVirtualized: boolean}) => ( + + ); + + return ( +
+ + +
+ ); + }, +}; + export const ValidationBehaviorAria = { render: ValidationBehaviorAriaTemplate, args: {