Skip to content

Commit ac76c44

Browse files
committed
dynamic height samples (#87)
1 parent 6f6449f commit ac76c44

File tree

3 files changed

+217
-3
lines changed

3 files changed

+217
-3
lines changed

packages/core/src/features/selection/feature.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ export const selectionFeature: FeatureImplementation = {
4343
tree.setSelectedItems(selectedItems.filter((id) => id !== itemId));
4444
},
4545

46-
isSelected: ({ tree, item }) => {
46+
isSelected: ({ tree, itemId }) => {
4747
const { selectedItems } = tree.getState();
48-
return selectedItems.includes(item.getItemMeta().itemId);
48+
return selectedItems.includes(itemId);
4949
},
5050

5151
selectUpTo: ({ tree, item }, ctrl: boolean) => {

packages/docs/docs/recipes/2-virtualization.mdx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,25 @@ using trees with many items. Read this guide to learn more about [Proxy Item Ins
2828
You can use them by setting the `instanceBuilder` tree config option to [`buildProxiedInstance`](/api/core/function/buildProxiedInstance),
2929
a symbol that you can import from `@headless-tree/core`.
3030

31-
:::
31+
:::
32+
33+
If you need to need a ref to the virtualized DOM items, keep in mind that `treeItem.getProps()` also
34+
returns a ref that needs to be assigned to the DOM element. Also keep in mind that the ref function
35+
of a DOM element is called both on mount and unmount, and calling `treeItem.getProps()` will fail
36+
if the item is already unloaded in the tree. Calling `treeItem.getProps()` outside the ref, like the following,
37+
will work:
38+
39+
```ts jsx
40+
const props = item.getProps();
41+
return (
42+
<button
43+
{...props}
44+
key={virtualItem.key}
45+
data-index={virtualItem.index}
46+
ref={(r) => {
47+
virtualizer.measureElement(r);
48+
props.ref(r);
49+
}}
50+
/>
51+
);
52+
```
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import type { Meta } from "@storybook/react";
2+
import React, {
3+
forwardRef,
4+
useImperativeHandle,
5+
useRef,
6+
useState,
7+
} from "react";
8+
import {
9+
TreeInstance,
10+
TreeState,
11+
buildProxiedInstance,
12+
buildStaticInstance,
13+
dragAndDropFeature,
14+
hotkeysCoreFeature,
15+
keyboardDragAndDropFeature,
16+
selectionFeature,
17+
syncDataLoaderFeature,
18+
} from "@headless-tree/core";
19+
import { useTree } from "@headless-tree/react";
20+
import { Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
21+
import cx from "classnames";
22+
import { PropsOfArgtype } from "../argtypes";
23+
24+
const meta = {
25+
title: "React/Scalability/Basic Virtualization With Dynamic Height",
26+
tags: ["homepage"],
27+
argTypes: {
28+
itemsPerLevel: {
29+
type: "number",
30+
},
31+
openLevels: {
32+
type: "number",
33+
},
34+
useProxyInstances: {
35+
type: "boolean",
36+
},
37+
},
38+
args: {
39+
itemsPerLevel: 10,
40+
openLevels: 4,
41+
useProxyInstances: true,
42+
},
43+
} satisfies Meta;
44+
45+
export default meta;
46+
47+
// story-start
48+
const heights = Array.from({ length: 100 }, (_, i) => Math.random() * 30 + 20);
49+
50+
const getInitiallyExpandedItemIds = (
51+
itemsPerLevel: number,
52+
openLevels: number,
53+
prefix = "folder",
54+
): string[] => {
55+
if (openLevels === 0) {
56+
return [];
57+
}
58+
59+
const expandedItems: string[] = [];
60+
61+
for (let i = 0; i < itemsPerLevel; i++) {
62+
expandedItems.push(`${prefix}-${i}`);
63+
}
64+
65+
return [
66+
...expandedItems,
67+
...expandedItems.flatMap((itemId) =>
68+
getInitiallyExpandedItemIds(itemsPerLevel, openLevels - 1, itemId),
69+
),
70+
];
71+
};
72+
73+
const Inner = forwardRef<
74+
Virtualizer<HTMLDivElement, Element>,
75+
{ tree: TreeInstance<string> }
76+
>(({ tree }, ref) => {
77+
const parentRef = useRef<HTMLDivElement | null>(null);
78+
79+
const virtualizer = useVirtualizer({
80+
count: tree.getItems().length,
81+
getScrollElement: () => parentRef.current,
82+
estimateSize: () => 25,
83+
});
84+
85+
useImperativeHandle(ref, () => virtualizer);
86+
87+
return (
88+
<div
89+
ref={parentRef}
90+
style={{
91+
height: `400px`,
92+
overflow: "auto",
93+
}}
94+
>
95+
<div
96+
{...tree.getContainerProps()}
97+
className="tree"
98+
style={{
99+
height: `${virtualizer.getTotalSize()}px`,
100+
width: "100%",
101+
position: "relative",
102+
}}
103+
>
104+
{virtualizer.getVirtualItems().map((virtualItem) => {
105+
const item = tree.getItems()[virtualItem.index];
106+
const props = item.getProps();
107+
return (
108+
<button
109+
{...props}
110+
key={virtualItem.key}
111+
data-index={virtualItem.index}
112+
ref={(r) => {
113+
virtualizer.measureElement(r);
114+
props.ref(r);
115+
// do not call item.getProps() in here, as this would also
116+
// be called on unmount on items that then don't exist anymore
117+
}}
118+
style={{
119+
position: "absolute",
120+
top: 0,
121+
left: 0,
122+
width: "100%",
123+
transform: `translateY(${virtualItem.start}px)`,
124+
paddingLeft: `${item.getItemMeta().level * 20}px`,
125+
}}
126+
>
127+
<div
128+
className={cx("treeitem", {
129+
focused: item.isFocused(),
130+
expanded: item.isExpanded(),
131+
selected: item.isSelected(),
132+
folder: item.isFolder(),
133+
drop: item.isDragTarget(),
134+
})}
135+
style={{
136+
height: `${heights[item.getItemMeta().index % heights.length]}px`,
137+
}}
138+
>
139+
{item.getItemName()}
140+
</div>
141+
</button>
142+
);
143+
})}
144+
<div style={tree.getDragLineStyle()} className="dragline" />
145+
</div>
146+
</div>
147+
);
148+
});
149+
150+
export const BasicVirtualizationWithDynamicHeight = ({
151+
itemsPerLevel,
152+
openLevels,
153+
useProxyInstances,
154+
}: PropsOfArgtype<typeof meta>) => {
155+
const virtualizer = useRef<Virtualizer<HTMLDivElement, Element> | null>(null);
156+
const [state, setState] = useState<Partial<TreeState<string>>>(() => ({
157+
expandedItems: getInitiallyExpandedItemIds(itemsPerLevel, openLevels),
158+
}));
159+
const tree = useTree<string>({
160+
instanceBuilder: useProxyInstances
161+
? buildProxiedInstance
162+
: buildStaticInstance,
163+
state,
164+
setState,
165+
rootItemId: "folder",
166+
getItemName: (item) => item.getItemData(),
167+
isItemFolder: (item) => !item.getItemData().endsWith("item"),
168+
scrollToItem: (item) => {
169+
virtualizer.current?.scrollToIndex(item.getItemMeta().index);
170+
},
171+
canReorder: true,
172+
indent: 20,
173+
dataLoader: {
174+
getItem: (itemId) => itemId,
175+
getChildren: (itemId) => {
176+
const items: string[] = [];
177+
for (let i = 0; i < itemsPerLevel; i++) {
178+
items.push(`${itemId}-${i}`);
179+
}
180+
return items;
181+
},
182+
},
183+
features: [
184+
syncDataLoaderFeature,
185+
selectionFeature,
186+
hotkeysCoreFeature,
187+
dragAndDropFeature,
188+
keyboardDragAndDropFeature,
189+
],
190+
});
191+
192+
return <Inner tree={tree} ref={virtualizer} />;
193+
};

0 commit comments

Comments
 (0)