Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
390b6b0
Update tree and listlayout to handle multi loaders
LFDanLu May 23, 2025
50c6f14
adapting other stories to new loader api and adding useAsync example …
LFDanLu May 23, 2025
f99e4bd
add tests
LFDanLu May 23, 2025
76bc6dd
fix story for correctness, should only need to provide a dependecy at…
LFDanLu May 23, 2025
7123f7e
Merge branch 'main' of github.com:adobe/react-spectrum into multi_loa…
LFDanLu May 27, 2025
1b4b7b5
restoring focus back to the tree if the user was focused on the loader
LFDanLu May 27, 2025
5dbf204
fixing estimated loader position if sections exist
LFDanLu Jun 2, 2025
63a9fd8
Merge branch 'main' of github.com:adobe/react-spectrum into multi_loa…
LFDanLu Jun 4, 2025
ddffc91
skip test for now
LFDanLu Jun 4, 2025
f24361d
Merge branch 'main' of github.com:adobe/react-spectrum into multi_loa…
LFDanLu Jun 5, 2025
3c8058f
Merge branch 'main' of github.com:adobe/react-spectrum into multi_loa…
LFDanLu Jun 5, 2025
6ca4b80
Merge branch 'main' of github.com:adobe/react-spectrum into multi_loa…
LFDanLu Jun 16, 2025
8749e85
revert loader keyboard focus specific logic
LFDanLu Jun 16, 2025
493c680
pulling over relevant code from focus_loading_spinners
LFDanLu Jun 16, 2025
1d26003
modify tree to return item count when querying size
LFDanLu Jun 16, 2025
c719c18
update TableCollection to return just the number of rows as size
LFDanLu Jun 17, 2025
fbfae78
update other collection components to leverage item only count
LFDanLu Jun 17, 2025
19ea161
clean up
LFDanLu Jun 17, 2025
6095683
properly prevent picker from opening where there arent items
LFDanLu Jun 17, 2025
74db6cb
fix lint
LFDanLu Jun 17, 2025
b6bb68b
Merge branch 'main' of github.com:adobe/react-spectrum into multi_loa…
LFDanLu Jun 23, 2025
da608f3
review comments
LFDanLu Jun 23, 2025
81de58a
review comments
LFDanLu Jun 23, 2025
6907e1e
making the async stories not load forever
LFDanLu Jun 24, 2025
7217b93
Merge branch 'main' into multi_loader_support
LFDanLu Jul 7, 2025
eb1aa3c
Merge branch 'main' of github.com:adobe/react-spectrum into multi_loa…
LFDanLu Jul 8, 2025
4eab8de
docs: Async load more documentation for RAC (#8431)
LFDanLu Jul 8, 2025
88b725a
review comments
LFDanLu Jul 10, 2025
463115d
Merge branch 'main' of github.com:adobe/react-spectrum into multi_loa…
LFDanLu Jul 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion packages/@react-aria/collections/src/BaseCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
private firstKey: Key | null = null;
private lastKey: Key | null = null;
private frozen = false;
private itemCount: number = 0;

get size(): number {
return this.keyMap.size;
return this.itemCount;
}

getKeys(): IterableIterator<Key> {
Expand Down Expand Up @@ -184,6 +185,7 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
collection.keyMap = new Map(this.keyMap);
collection.firstKey = this.firstKey;
collection.lastKey = this.lastKey;
collection.itemCount = this.itemCount;
return collection;
}

Expand All @@ -192,6 +194,10 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
throw new Error('Cannot add a node to a frozen collection');
}

if (node.type === 'item' && this.keyMap.get(node.key) == null) {
this.itemCount++;
}

this.keyMap.set(node.key, node);
}

Expand All @@ -200,6 +206,11 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
throw new Error('Cannot remove a node to a frozen collection');
}

let node = this.keyMap.get(key);
if (node != null && node.type === 'item') {
this.itemCount--;
}

this.keyMap.delete(key);
}

Expand Down
1 change: 0 additions & 1 deletion packages/@react-aria/gridlist/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"@react-aria/interactions": "^3.25.3",
"@react-aria/selection": "^3.24.3",
"@react-aria/utils": "^3.29.1",
"@react-stately/collections": "^3.12.5",
"@react-stately/list": "^3.12.3",
"@react-stately/tree": "^3.9.0",
"@react-types/shared": "^3.30.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/@react-aria/gridlist/src/useGridListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import {chain, getScrollParent, mergeProps, scrollIntoViewport, useSlotId, useSyntheticLinkProps} from '@react-aria/utils';
import {DOMAttributes, FocusableElement, Key, RefObject, Node as RSNode} from '@react-types/shared';
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
import {getLastItem} from '@react-stately/collections';
import {getRowId, listMap} from './utils';
import {HTMLAttributes, KeyboardEvent as ReactKeyboardEvent, useRef} from 'react';
import {isFocusVisible} from '@react-aria/interactions';
Expand Down Expand Up @@ -104,13 +103,14 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
if (node.level > 0 && node?.parentKey != null) {
let parent = state.collection.getItem(node.parentKey);
if (parent) {
// siblings must exist because our original node exists, same with lastItem
// siblings must exist because our original node exists
let siblings = state.collection.getChildren?.(parent.key)!;
setSize = getLastItem(siblings)!.index + 1;
setSize = [...siblings].filter(row => row.type === 'item').length;
}
} else {
setSize = ([...state.collection].filter(row => row.level === 0).at(-1)?.index ?? 0) + 1;
setSize = [...state.collection].filter(row => row.level === 0 && row.type === 'item').length;
}

treeGridRowProps = {
'aria-expanded': isExpanded,
'aria-level': node.level + 1,
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export {useEffectEvent} from './useEffectEvent';
export {useDeepMemo} from './useDeepMemo';
export {useFormReset} from './useFormReset';
export {useLoadMore} from './useLoadMore';
export {UNSTABLE_useLoadMoreSentinel} from './useLoadMoreSentinel';
export {useLoadMoreSentinel} from './useLoadMoreSentinel';
export {inertValue} from './inertValue';
export {CLEAR_FOCUS_EVENT, FOCUS_EVENT} from './constants';
export {isCtrlKeyPressed} from './keyboard';
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/utils/src/useLoadMoreSentinel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface LoadMoreSentinelProps extends Omit<AsyncLoadable, 'isLoading'>
scrollOffset?: number
}

export function UNSTABLE_useLoadMoreSentinel(props: LoadMoreSentinelProps, ref: RefObject<HTMLElement | null>): void {
export function useLoadMoreSentinel(props: LoadMoreSentinelProps, ref: RefObject<HTMLElement | null>): void {
let {collection, onLoadMore, scrollOffset = 1} = props;

let sentinelObserver = useRef<IntersectionObserver>(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1844,7 +1844,7 @@ describe('SearchAutocomplete', function () {
expect(() => within(tray).getByText('No results')).toThrow();
});

it.skip('user can select options by pressing them', async function () {
it('user can select options by pressing them', async function () {
let {getByRole, getByText, getByTestId} = renderSearchAutocomplete();
let button = getByRole('button');

Expand Down Expand Up @@ -1892,7 +1892,7 @@ describe('SearchAutocomplete', function () {
expect(items[1]).toHaveAttribute('aria-selected', 'true');
});

it.skip('user can select options by focusing them and hitting enter', async function () {
it('user can select options by focusing them and hitting enter', async function () {
let {getByRole, getByText, getByTestId} = renderSearchAutocomplete();
let button = getByRole('button');

Expand Down
4 changes: 2 additions & 2 deletions packages/@react-spectrum/s2/src/CardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import {
ContextValue,
GridLayout,
GridListItem,
GridListLoadMoreItem,
GridListProps,
Size,
UNSTABLE_GridListLoadingSentinel,
Virtualizer,
WaterfallLayout
} from 'react-aria-components';
Expand Down Expand Up @@ -246,7 +246,7 @@ export const CardView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca

let renderer;
let cardLoadingSentinel = (
<UNSTABLE_GridListLoadingSentinel
<GridListLoadMoreItem
onLoadMore={onLoadMore} />
);

Expand Down
6 changes: 3 additions & 3 deletions packages/@react-spectrum/s2/src/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import {
ListBox,
ListBoxItem,
ListBoxItemProps,
ListBoxLoadMoreItem,
ListBoxProps,
ListLayout,
ListStateContext,
Provider,
SectionProps,
UNSTABLE_ListBoxLoadingSentinel,
Virtualizer
} from 'react-aria-components';
import {AsyncLoadable, HelpTextProps, LoadingState, SpectrumLabelableProps} from '@react-types/shared';
Expand Down Expand Up @@ -542,7 +542,7 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps<any

let renderer;
let listBoxLoadingCircle = (
<UNSTABLE_ListBoxLoadingSentinel
<ListBoxLoadMoreItem
// Only show the spinner in the list when loading more
isLoading={loadingState === 'loadingMore'}
onLoadMore={onLoadMore}
Expand All @@ -553,7 +553,7 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps<any
styles={progressCircleStyles({size})}
// Same loading string as table
aria-label={stringFormatter.format('table.loadingMore')} />
</UNSTABLE_ListBoxLoadingSentinel>
</ListBoxLoadMoreItem>
);

if (typeof children === 'function') {
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-spectrum/s2/src/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ import {
ListBox,
ListBoxItem,
ListBoxItemProps,
ListBoxLoadMoreItem,
ListBoxProps,
ListLayout,
Provider,
SectionProps,
SelectValue,
UNSTABLE_ListBoxLoadingSentinel,
Virtualizer
} from 'react-aria-components';
import {AsyncLoadable, FocusableRef, FocusableRefValue, HelpTextProps, LoadingState, PressEvent, RefObject, SpectrumLabelableProps} from '@react-types/shared';
Expand Down Expand Up @@ -307,12 +307,12 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick
let spinnerId = useSlotId([showButtonSpinner]);

let listBoxLoadingCircle = (
<UNSTABLE_ListBoxLoadingSentinel
<ListBoxLoadMoreItem
className={loadingWrapperStyles}
isLoading={loadingState === 'loadingMore'}
onLoadMore={onLoadMore}>
<PickerProgressCircle size={size} aria-label={stringFormatter.format('table.loadingMore')} />
</UNSTABLE_ListBoxLoadingSentinel>
</ListBoxLoadMoreItem>
);

if (typeof children === 'function' && items) {
Expand Down
17 changes: 8 additions & 9 deletions packages/@react-spectrum/s2/src/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ import {
RowRenderProps,
TableBodyRenderProps,
TableLayout,
TableLoadMoreItem,
TableRenderProps,
UNSTABLE_TableLoadingSentinel,
useSlottedContext,
useTableOptions,
Virtualizer
Expand Down Expand Up @@ -196,11 +196,10 @@ export class S2TableLayout<T> extends TableLayout<T> {
if (!header) {
return [];
}
let {children, layoutInfo} = body;
let {layoutInfo} = body;
// TableLayout's buildCollection always sets the body width to the max width between the header width, but
// we want the body to be sticky and only as wide as the table so it is always in view if loading/empty
// TODO: we may want to adjust RAC layouts to do something simlar? Current users of RAC table will probably run into something similar
let isEmptyOrLoading = children?.length === 0 || (children?.length === 1 && children[0].layoutInfo.type === 'loader');
let isEmptyOrLoading = this.virtualizer?.collection.size === 0;
if (isEmptyOrLoading) {
layoutInfo.rect.width = this.virtualizer!.visibleRect.width - 80;
}
Expand All @@ -219,19 +218,19 @@ export class S2TableLayout<T> extends TableLayout<T> {
// If performing first load or empty, the body will be sticky so we don't want to apply sticky to the loader, otherwise it will
// affect the positioning of the empty state renderer
let collection = this.virtualizer!.collection;
let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader');
let isEmptyOrLoading = collection?.size === 0;
layoutInfo.isSticky = !isEmptyOrLoading;
return layoutNode;
}

// y is the height of the headers
protected buildBody(y: number): LayoutNode {
let layoutNode = super.buildBody(y);
let {children, layoutInfo} = layoutNode;
let {layoutInfo} = layoutNode;
// Needs overflow for sticky loader
layoutInfo.allowOverflow = true;
// If loading or empty, we'll want the body to be sticky and centered
let isEmptyOrLoading = children?.length === 0 || (children?.length === 1 && children[0].layoutInfo.type === 'loader');
let isEmptyOrLoading = this.virtualizer?.collection.size === 0;
if (isEmptyOrLoading) {
layoutInfo.rect = new Rect(40, 40, this.virtualizer!.visibleRect.width - 80, this.virtualizer!.visibleRect.height - 80);
layoutInfo.isSticky = true;
Expand Down Expand Up @@ -390,13 +389,13 @@ export const TableBody = /*#__PURE__*/ (forwardRef as forwardRefType)(function T
// This is because we don't distinguish between loadingMore and loading in the layout, resulting in a different rect being used to build the body. Perhaps can be considered as a user error
// if they pass loadingMore without having any other items in the table. Alternatively, could update the layout so it knows the current loading state.
let loadMoreSpinner = (
<UNSTABLE_TableLoadingSentinel isLoading={loadingState === 'loadingMore'} onLoadMore={onLoadMore} className={style({height: 'full', width: 'full'})}>
<TableLoadMoreItem isLoading={loadingState === 'loadingMore'} onLoadMore={onLoadMore} className={style({height: 'full', width: 'full'})}>
<div className={centeredWrapper}>
<ProgressCircle
isIndeterminate
aria-label={stringFormatter.format('table.loadingMore')} />
</div>
</UNSTABLE_TableLoadingSentinel>
</TableLoadMoreItem>
);

// If the user is rendering their rows in dynamic fashion, wrap their render function in Collection so we can inject
Expand Down
11 changes: 10 additions & 1 deletion packages/@react-spectrum/s2/test/Picker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,20 @@ describe('Picker', () => {
let options = selectTester.options();
for (let [index, option] of options.entries()) {
expect(option).toHaveAttribute('aria-posinset', `${index + 1}`);
expect(option).toHaveAttribute('aria-setsize', `${items.length}`);
}

tree.rerender(<DynamicPicker items={items} loadingState="loadingMore" />);
options = selectTester.options();
for (let [index, option] of options.entries()) {
expect(option).toHaveAttribute('aria-posinset', `${index + 1}`);
if (index === options.length - 1) {
// The last row is the loader here which shouldn't have posinset
expect(option).not.toHaveAttribute('aria-posinset');
expect(option).not.toHaveAttribute('aria-setsize');
} else {
expect(option).toHaveAttribute('aria-posinset', `${index + 1}`);
expect(option).toHaveAttribute('aria-setsize', `${items.length}`);
}
}

let newItems = [...items, {name: 'Chocolate Mint'}, {name: 'Chocolate Chip Cookie Dough'}];
Expand All @@ -117,6 +125,7 @@ describe('Picker', () => {
options = selectTester.options();
for (let [index, option] of options.entries()) {
expect(option).toHaveAttribute('aria-posinset', `${index + 1}`);
expect(option).toHaveAttribute('aria-setsize', `${newItems.length}`);
}
});

Expand Down
4 changes: 2 additions & 2 deletions packages/@react-spectrum/tree/test/TreeView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1440,8 +1440,8 @@ describe('Tree', () => {

let row = treeTester.rows[0];
expect(row).toHaveAttribute('aria-level', '1');
expect(row).toHaveAttribute('aria-posinset', '1');
expect(row).toHaveAttribute('aria-setsize', '1');
expect(row).not.toHaveAttribute('aria-posinset');
expect(row).not.toHaveAttribute('aria-setsize');
let gridCell = treeTester.cells({element: row})[0];
expect(gridCell).toHaveTextContent('No resultsNo results found.');

Expand Down
2 changes: 1 addition & 1 deletion packages/@react-stately/layout/src/GridLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
let collection = this.virtualizer!.collection;
// Make sure to set rows to 0 if we performing a first time load or are rendering the empty state so that Virtualizer
// won't try to render its body
let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader');
let isEmptyOrLoading = collection?.size === 0;
let rows = isEmptyOrLoading ? 0 : Math.ceil(collection.size / numColumns);
let iterator = collection[Symbol.iterator]();
let y = rows > 0 ? minSpace.height : 0;
Expand Down
38 changes: 22 additions & 16 deletions packages/@react-stately/layout/src/ListLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,41 +253,47 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte

protected buildCollection(y: number = this.padding): LayoutNode[] {
let collection = this.virtualizer!.collection;
let skipped = 0;
let collectionNodes = [...collection];
let loaderNodes = collectionNodes.filter(node => node.type === 'loader');
let nodes: LayoutNode[] = [];
let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader');
let isEmptyOrLoading = collection?.size === 0;
if (isEmptyOrLoading) {
y = 0;
}

for (let node of collection) {
for (let node of collectionNodes) {
let rowHeight = (this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT) + this.gap;
// Skip rows before the valid rectangle unless they are already cached.
if (node.type === 'item' && y + rowHeight < this.requestedRect.y && !this.isValid(node, y)) {
y += rowHeight;
skipped++;
continue;
}

let layoutNode = this.buildChild(node, this.padding, y, null);
y = layoutNode.layoutInfo.rect.maxY + this.gap;
nodes.push(layoutNode);
if (node.type === 'item' && y > this.requestedRect.maxY) {
let itemsAfterRect = collection.size - (nodes.length + skipped);
let lastNode = collection.getItem(collection.getLastKey()!);
if (lastNode?.type === 'loader') {
itemsAfterRect--;
}

y += itemsAfterRect * rowHeight;
if (node.type === 'loader') {
let index = loaderNodes.indexOf(node);
loaderNodes.splice(index, 1);
}

// Always add the loader sentinel if present. This assumes the loader is the last option/row
// will need to refactor when handling multi section loading
if (lastNode?.type === 'loader' && nodes.at(-1)?.layoutInfo.type !== 'loader') {
let loader = this.buildChild(lastNode, this.padding, y, null);
// Build each loader that exists in the collection that is outside the visible rect so that they are persisted
// at the proper estimated location. If the node.type is "section" then we don't do this shortcut since we have to
// build the sections to see how tall they are.
if ((node.type === 'item' || node.type === 'loader') && y > this.requestedRect.maxY) {
let lastProcessedIndex = collectionNodes.indexOf(node);
for (let loaderNode of loaderNodes) {
let loaderNodeIndex = collectionNodes.indexOf(loaderNode);
// Subtract by an additional 1 since we've already added the current item's height to y
y += (loaderNodeIndex - lastProcessedIndex - 1) * rowHeight;
let loader = this.buildChild(loaderNode, this.padding, y, null);
nodes.push(loader);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this potentially reorder the loaders? This would put them all at the end, even if they were somewhere else within the collection originally.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we only hit this point if we past the virtualizer's requested rect though right? At that point we only need/want to build and persist the loaders, and the order of these remaining loaders should be correct provided that iterating over the collection provides the nodes in their proper order

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my mental model for this flow is essentially:

  1. Iterate through the collection from the top.
  2. From y=0 to y=requestedRect.y, we build every loader and estimate the y increase for items
  3. From y=requestedRect.y to y=requestedRect.maxY, we build all loaders and items. Note that the loaders are removed from loaderNodes if they are built in step 2 or 3
  4. From y=requestedRect.maxY onwards, build the remaining loaders, increasing y with an estimated rowHeight * number of items between each loader and its next loader

so theoretically the order of the loaders should be preserved right?

y = loader.layoutInfo.rect.maxY;
lastProcessedIndex = loaderNodeIndex;
}

// Account for the rest of the items after the last loader spinner, subtract by 1 since we've processed the current node's height already
y += (collectionNodes.length - lastProcessedIndex - 1) * rowHeight;
break;
}
}
Expand Down
5 changes: 1 addition & 4 deletions packages/@react-stately/layout/src/TableLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,6 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
if (y > this.requestedRect.maxY) {
let rowsAfterRect = collection.size - (children.length + skipped);
let lastNode = getLastItem(childNodes);
if (lastNode?.type === 'loader') {
rowsAfterRect--;
}

// Estimate the remaining height for rows that we don't need to layout right now.
y += rowsAfterRect * rowHeight;
Expand All @@ -299,7 +296,7 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
}

// Make sure that the table body gets a height if empty or performing initial load
let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader');
let isEmptyOrLoading = collection?.size === 0;
if (isEmptyOrLoading) {
y = this.virtualizer!.visibleRect.maxY;
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-stately/layout/src/WaterfallLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export class WaterfallLayout<T extends object, O extends WaterfallLayoutOptions
}

// Reset all columns to the maximum for the next section. If loading, set to 0 so virtualizer doesn't render its body since there aren't items to render
let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader');
let isEmptyOrLoading = collection?.size === 0;
let maxHeight = isEmptyOrLoading ? 0 : Math.max(...columnHeights);
this.contentSize = new Size(this.virtualizer!.visibleRect.width, maxHeight);
this.layoutInfos = newLayoutInfos;
Expand Down
Loading