Skip to content

Commit

Permalink
fix: add custom function to calculate rowHeight for dataset with sect…
Browse files Browse the repository at this point in the history
…ions
  • Loading branch information
vinroger committed Dec 30, 2024
1 parent 4f0ef58 commit 30b1832
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 18 deletions.
62 changes: 45 additions & 17 deletions packages/components/listbox/src/virtualized-listbox.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -13,6 +14,21 @@ interface Props extends UseListboxReturn {
virtualization?: VirtualizationProps;
}

const getItemSizesForCollection = (collection: Node<object>[], 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,
Expand Down Expand Up @@ -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,
}: {
Expand All @@ -64,6 +84,10 @@ const VirtualizedListbox = (props: Props) => {
}) => {
const item = [...state.collection][index];

if (!item) {
return null;
}

const itemProps = {
color,
item,
Expand Down Expand Up @@ -111,6 +135,7 @@ const VirtualizedListbox = (props: Props) => {
)}
<div
ref={parentRef}
className="scrollbar-hide"
style={{
height: maxListboxHeight,
overflow: "auto",
Expand All @@ -124,19 +149,22 @@ 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 (
<ListBoxRow
key={virtualItem.index}
index={virtualItem.index}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
/>
);
})}
</div>
)}
</div>
Expand Down
4 changes: 3 additions & 1 deletion packages/components/select/src/use-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ export type UseSelectProps<T> = 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;
/**
Expand Down Expand Up @@ -208,7 +210,7 @@ export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
onSelectionChange,
placeholder,
isVirtualized,
itemHeight = 32,
itemHeight = 36,
maxListboxHeight = 256,
children,
disallowEmptySelection = false,
Expand Down
79 changes: 79 additions & 0 deletions packages/components/select/stories/select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}) => (
<Select
disallowEmptySelection
className="max-w-xs"
color="secondary"
defaultSelectedKeys={["jinx"]}
isVirtualized={isVirtualized}
label={`Avatar Decoration ${isVirtualized ? "(Virtualized)" : "(Non-virtualized)"}`}
selectedKeys={["jinx"]}
selectionMode="single"
variant="bordered"
>
{Object.keys(AVATAR_DECORATIONS).map((key) => (
<SelectSection
key={key}
classNames={{
heading: "uppercase text-secondary",
}}
title={key}
>
{AVATAR_DECORATIONS[key].map((item) => (
<SelectItem key={item} className="capitalize" color="secondary" variant="bordered">
{item.replace(/-/g, " ")}
</SelectItem>
))}
</SelectSection>
))}
</Select>
);

return (
<div className="flex gap-4 w-full">
<SelectComponent isVirtualized={false} />
<SelectComponent isVirtualized={true} />
</div>
);
},
};

export const ValidationBehaviorAria = {
render: ValidationBehaviorAriaTemplate,
args: {
Expand Down

0 comments on commit 30b1832

Please sign in to comment.