Skip to content

Commit 4d3bb6c

Browse files
committed
hack together async listbox virtualized example
1 parent 54fcbaa commit 4d3bb6c

File tree

2 files changed

+102
-13
lines changed

2 files changed

+102
-13
lines changed

packages/react-aria-components/src/Virtualizer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ function CollectionRoot({collection, persistedKeys, scrollRef, renderDropIndicat
107107
}
108108

109109
return (
110-
<div {...contentProps}>
110+
// TODO: temporarily hack styling so the load more sentinel is properly positioned
111+
// (aka we need the virtualizer content wrapper to take its full height/width if its is in a flex parent)
112+
<div {...contentProps} style={{...contentProps.style, flex: 'none'}}>
111113
<VirtualizerContext.Provider value={state}>
112114
{renderChildren(null, state.visibleViews, renderDropIndicator)}
113115
</VirtualizerContext.Provider>

packages/react-aria-components/stories/ListBox.stories.tsx

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212

1313
import {action} from '@storybook/addon-actions';
1414
import {Collection, DropIndicator, GridLayout, Header, ListBox, ListBoxItem, ListBoxProps, ListBoxSection, ListLayout, Separator, Text, useDragAndDrop, Virtualizer, WaterfallLayout} from 'react-aria-components';
15+
import {Key, useAsyncList, useListData} from 'react-stately';
16+
import {Layout, LayoutInfo, Rect, Size} from '@react-stately/virtualizer';
1517
import {MyListBoxItem} from './utils';
16-
import React from 'react';
17-
import {Size} from '@react-stately/virtualizer';
18+
import React, {useMemo} from 'react';
1819
import styles from '../example/index.css';
1920
import {UNSTABLE_ListBoxLoadingIndicator} from '../src/ListBox';
20-
import {useAsyncList, useListData} from 'react-stately';
2121

2222
export default {
2323
title: 'React Aria Components'
@@ -510,7 +510,63 @@ AsyncListBox.story = {
510510
}
511511
};
512512

513-
export const AsyncListBoxVirtualized = () => {
513+
class HorizontalLayout extends Layout {
514+
protected rowWidth: number;
515+
516+
constructor(options) {
517+
super();
518+
this.rowWidth = options.rowWidth ?? 100;
519+
}
520+
521+
// Determine which items are visible within the given rectangle.
522+
getVisibleLayoutInfos(rect: Rect): LayoutInfo[] {
523+
let virtualizer = this.virtualizer!;
524+
let keys = Array.from(virtualizer.collection.getKeys());
525+
let startIndex = Math.max(0, Math.floor(rect.x / 100));
526+
let endIndex = Math.min(keys.length - 1, Math.ceil(rect.maxX / 100));
527+
let layoutInfos = [] as LayoutInfo[];
528+
for (let i = startIndex; i <= endIndex; i++) {
529+
let layoutInfo = this.getLayoutInfo(keys[i]);
530+
if (layoutInfo) {
531+
layoutInfos.push(layoutInfo);
532+
}
533+
}
534+
535+
// Always add persisted keys (e.g. the focused item), even when out of view.
536+
for (let key of virtualizer.persistedKeys) {
537+
let item = virtualizer.collection.getItem(key);
538+
let layoutInfo = this.getLayoutInfo(key);
539+
if (item?.index && layoutInfo) {
540+
if (item?.index < startIndex) {
541+
layoutInfos.unshift(layoutInfo);
542+
} else if (item?.index > endIndex) {
543+
layoutInfos.push(layoutInfo);
544+
}
545+
}
546+
}
547+
548+
return layoutInfos;
549+
}
550+
551+
// Provide a LayoutInfo for a specific item.
552+
getLayoutInfo(key: Key): LayoutInfo | null {
553+
let node = this.virtualizer!.collection.getItem(key);
554+
if (!node) {
555+
return null;
556+
}
557+
558+
let rect = new Rect(node.index * this.rowWidth, 0, this.rowWidth, 100);
559+
return new LayoutInfo(node.type, node.key, rect);
560+
}
561+
562+
// Provide the total size of all items.
563+
getContentSize(): Size {
564+
let numItems = this.virtualizer!.collection.size;
565+
return new Size(numItems * this.rowWidth, 100);
566+
}
567+
}
568+
569+
export const AsyncListBoxVirtualized = (args) => {
514570
let list = useAsyncList<Character>({
515571
async load({signal, cursor, filterText}) {
516572
if (cursor) {
@@ -528,25 +584,56 @@ export const AsyncListBoxVirtualized = () => {
528584
}
529585
});
530586

587+
let layout = useMemo(() => {
588+
return args.orientation === 'horizontal' ? new HorizontalLayout({rowWidth: 100}) : new ListLayout({rowHeight: 50, padding: 4});
589+
}, [args.orientation]);
531590
return (
532591
<Virtualizer
533-
// TODO: loadMore doesn't quite work if we dont set a rowHeight, this is because when
534-
// Will also need to test against a case where there are sections being loaded and/or estimated height
535-
// layout={new ListLayout({estimatedRowHeight: 50})}
536-
// layout={ListLayout}
537-
layout={new ListLayout({rowHeight: 25})}>
592+
layout={layout}>
538593
<ListBox
539-
className={styles.menu}
540-
style={{height: 400}}
594+
{...args}
595+
style={{
596+
height: args.orientation === 'horizontal' ? 100 : 400,
597+
width: args.orientation === 'horizontal' ? 400 : 100,
598+
border: '1px solid gray',
599+
background: 'lightgray',
600+
overflow: 'auto',
601+
padding: 'unset',
602+
display: 'flex'
603+
}}
541604
aria-label="async virtualized listbox"
542605
isLoading={list.isLoading}
543606
onLoadMore={list.loadMore}
544607
renderEmptyState={() => list.isLoading ? 'Loading spinner' : 'No results found'}>
545608
<Collection items={list.items}>
546-
{item => <MyListBoxItem id={item.name}>{item.name}</MyListBoxItem>}
609+
{(item: Character) => (
610+
<MyListBoxItem
611+
style={{
612+
backgroundColor: 'lightgrey',
613+
border: '1px solid black',
614+
boxSizing: 'border-box',
615+
height: '100%',
616+
width: '100%'
617+
}}
618+
id={item.name}>
619+
{item.name}
620+
</MyListBoxItem>
621+
)}
547622
</Collection>
548623
{list.isLoading && list.items.length > 0 && <MyListBoxLoaderIndicator />}
549624
</ListBox>
550625
</Virtualizer>
551626
);
552627
};
628+
629+
AsyncListBoxVirtualized.story = {
630+
args: {
631+
orientation: 'horizontal'
632+
},
633+
argTypes: {
634+
orientation: {
635+
control: 'radio',
636+
options: ['horizontal', 'vertical']
637+
}
638+
}
639+
};

0 commit comments

Comments
 (0)