Skip to content

Commit f80c621

Browse files
authored
Revert "feat: Tree multiple level loading support (#8299)" (#8344)
This reverts commit 6bbacb8.
1 parent 41ce6bf commit f80c621

File tree

7 files changed

+65
-832
lines changed

7 files changed

+65
-832
lines changed

packages/@react-spectrum/autocomplete/test/SearchAutocomplete.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1844,7 +1844,7 @@ describe('SearchAutocomplete', function () {
18441844
expect(() => within(tray).getByText('No results')).toThrow();
18451845
});
18461846

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

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

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

packages/@react-stately/layout/src/ListLayout.ts

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -253,48 +253,41 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
253253

254254
protected buildCollection(y = this.padding): LayoutNode[] {
255255
let collection = this.virtualizer!.collection;
256-
let collectionNodes = [...collection];
257-
let loaderNodes = collectionNodes.filter(node => node.type === 'loader');
256+
let skipped = 0;
258257
let nodes: LayoutNode[] = [];
259-
260-
let isEmptyOrLoading = collection?.size === 0 || !collectionNodes.some(item => item.type !== 'loader');
258+
let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader');
261259
if (isEmptyOrLoading) {
262260
y = 0;
263261
}
264262

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

273272
let layoutNode = this.buildChild(node, this.padding, y, null);
274273
y = layoutNode.layoutInfo.rect.maxY + this.gap;
275274
nodes.push(layoutNode);
276-
if (node.type === 'loader') {
277-
let index = loaderNodes.indexOf(node);
278-
loaderNodes.splice(index, 1);
279-
}
275+
if (node.type === 'item' && y > this.requestedRect.maxY) {
276+
let itemsAfterRect = collection.size - (nodes.length + skipped);
277+
let lastNode = collection.getItem(collection.getLastKey()!);
278+
if (lastNode?.type === 'loader') {
279+
itemsAfterRect--;
280+
}
281+
282+
y += itemsAfterRect * rowHeight;
280283

281-
// Build each loader that exists in the collection that is outside the visible rect so that they are persisted
282-
// at the proper estimated location. If the node.type is "section" then we don't do this shortcut since we have to
283-
// build the sections to see how tall they are.
284-
if ((node.type === 'item' || node.type === 'loader') && y > this.requestedRect.maxY) {
285-
let lastProcessedIndex = collectionNodes.indexOf(node);
286-
for (let loaderNode of loaderNodes) {
287-
let loaderNodeIndex = collectionNodes.indexOf(loaderNode);
288-
// Subtract by an additional 1 since we've already added the current item's height to y
289-
y += (loaderNodeIndex - lastProcessedIndex - 1) * rowHeight;
290-
let loader = this.buildChild(loaderNode, this.padding, y, null);
284+
// Always add the loader sentinel if present. This assumes the loader is the last option/row
285+
// will need to refactor when handling multi section loading
286+
if (lastNode?.type === 'loader' && nodes.at(-1)?.layoutInfo.type !== 'loader') {
287+
let loader = this.buildChild(lastNode, this.padding, y, null);
291288
nodes.push(loader);
292289
y = loader.layoutInfo.rect.maxY;
293-
lastProcessedIndex = loaderNodeIndex;
294290
}
295-
296-
// 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
297-
y += (collectionNodes.length - lastProcessedIndex - 1) * rowHeight;
298291
break;
299292
}
300293
}

packages/@react-stately/tree/src/useTreeState.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,8 @@ export function useTreeState<T extends object>(props: TreeProps<T>): TreeState<T
6565

6666
// Reset focused key if that item is deleted from the collection.
6767
useEffect(() => {
68-
if (selectionState.focusedKey != null) {
69-
let focusedItem = tree.getItem(selectionState.focusedKey);
70-
// TODO: do we want to have the same logic as useListState/useGridState where it tries to find the nearest row?
71-
// We could possibly special case this loader case and have it try to find the item just before it/the parent
72-
if (!focusedItem || focusedItem.type === 'loader' && !focusedItem.props.isLoading) {
73-
selectionState.setFocusedKey(null);
74-
}
68+
if (selectionState.focusedKey != null && !tree.getItem(selectionState.focusedKey)) {
69+
selectionState.setFocusedKey(null);
7570
}
7671
// eslint-disable-next-line react-hooks/exhaustive-deps
7772
}, [tree, selectionState.focusedKey]);

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

Lines changed: 23 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {DisabledBehavior, DragPreviewRenderer, Expandable, forwardRefType, Hover
2020
import {DragAndDropContext, DropIndicatorContext, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop';
2121
import {DragAndDropHooks} from './useDragAndDrop';
2222
import {DraggableCollectionState, DroppableCollectionState, Collection as ICollection, Node, SelectionBehavior, TreeState, useTreeState} from 'react-stately';
23-
import {filterDOMProps, inertValue, LoadMoreSentinelProps, UNSTABLE_useLoadMoreSentinel, useObjectRef} from '@react-aria/utils';
23+
import {filterDOMProps, useObjectRef} from '@react-aria/utils';
2424
import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useContext, useEffect, useMemo, useRef, useState} from 'react';
2525
import {TreeDropTargetDelegate} from './TreeDropTargetDelegate';
2626
import {useControlledState} from '@react-stately/utils';
@@ -196,7 +196,7 @@ function TreeInner<T extends object>({props, collection, treeRef: ref}: TreeInne
196196
let hasDropHooks = !!dragAndDropHooks?.useDroppableCollectionState;
197197
let dragHooksProvided = useRef(hasDragHooks);
198198
let dropHooksProvided = useRef(hasDropHooks);
199-
199+
200200
useEffect(() => {
201201
if (dragHooksProvided.current !== hasDragHooks) {
202202
console.warn('Drag hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
@@ -701,87 +701,51 @@ export const TreeItem = /*#__PURE__*/ createBranchComponent('item', <T extends o
701701
);
702702
});
703703

704-
export interface UNSTABLE_TreeLoadingSentinelRenderProps extends Pick<TreeItemRenderProps, 'isFocused' | 'isFocusVisible'> {
704+
export interface UNSTABLE_TreeLoadingIndicatorRenderProps {
705705
/**
706706
* What level the tree item has within the tree.
707707
* @selector [data-level]
708708
*/
709709
level: number
710710
}
711711

712-
export interface TreeLoadingSentinelProps extends Omit<LoadMoreSentinelProps, 'collection'>, RenderProps<UNSTABLE_TreeLoadingSentinelRenderProps> {
713-
/**
714-
* The load more spinner to render when loading additional items.
715-
*/
716-
children?: ReactNode | ((values: UNSTABLE_TreeLoadingSentinelRenderProps & {defaultChildren: ReactNode | undefined}) => ReactNode),
717-
/**
718-
* Whether or not the loading spinner should be rendered or not.
719-
*/
720-
isLoading?: boolean
721-
}
712+
export interface TreeLoaderProps extends RenderProps<UNSTABLE_TreeLoadingIndicatorRenderProps>, StyleRenderProps<UNSTABLE_TreeLoadingIndicatorRenderProps> {}
722713

723-
export const UNSTABLE_TreeLoadingSentinel = createLeafComponent('loader', function TreeLoadingSentinel<T extends object>(props: TreeLoadingSentinelProps, ref: ForwardedRef<HTMLDivElement>, item: Node<T>) {
714+
export const UNSTABLE_TreeLoadingIndicator = createLeafComponent('loader', function TreeLoader<T extends object>(props: TreeLoaderProps, ref: ForwardedRef<HTMLDivElement>, item: Node<T>) {
724715
let state = useContext(TreeStateContext)!;
725-
let {isLoading, onLoadMore, scrollOffset, ...otherProps} = props;
726-
let sentinelRef = useRef(null);
727-
let memoedLoadMoreProps = useMemo(() => ({
728-
onLoadMore,
729-
// TODO: this collection will update anytime a row is expanded/collapsed becaused the flattenedRows will change.
730-
// This means onLoadMore will trigger but that might be ok cause the user should have logic to handle multiple loadMore calls
731-
collection: state?.collection,
732-
sentinelRef,
733-
scrollOffset
734-
}), [onLoadMore, scrollOffset, state?.collection]);
735-
UNSTABLE_useLoadMoreSentinel(memoedLoadMoreProps, sentinelRef);
736-
737-
ref = useObjectRef<HTMLDivElement>(ref);
738-
let {rowProps, gridCellProps, ...states} = useTreeItem({node: item}, state, ref);
716+
// This loader row is is non-interactable, but we want the same aria props calculated as a typical row
717+
// @ts-ignore
718+
let {rowProps} = useTreeItem({node: item}, state, ref);
739719
let level = rowProps['aria-level'] || 1;
740720

741721
let ariaProps = {
742-
role: 'row',
743722
'aria-level': rowProps['aria-level'],
744723
'aria-posinset': rowProps['aria-posinset'],
745-
'aria-setsize': rowProps['aria-setsize'],
746-
tabIndex: rowProps.tabIndex
724+
'aria-setsize': rowProps['aria-setsize']
747725
};
748726

749-
let {isFocusVisible, focusProps} = useFocusRing();
750-
751727
let renderProps = useRenderProps({
752-
...otherProps,
728+
...props,
753729
id: undefined,
754730
children: item.rendered,
755731
defaultClassName: 'react-aria-TreeLoader',
756732
values: {
757-
level,
758-
isFocused: states.isFocused,
759-
isFocusVisible
733+
level
760734
}
761735
});
762736

763737
return (
764738
<>
765-
{/* Alway render the sentinel. For now onus is on the user for styling when using flex + gap (this would introduce a gap even though it doesn't take room) */}
766-
{/* @ts-ignore - compatibility with React < 19 */}
767-
<div style={{position: 'relative', width: 0, height: 0}} inert={inertValue(true)} >
768-
<div data-testid="loadMoreSentinel" ref={sentinelRef} style={{position: 'absolute', height: 1, width: 1}} />
769-
</div>
770-
{isLoading && renderProps.children && (
771-
<div
772-
ref={ref}
773-
{...mergeProps(filterDOMProps(props as any), ariaProps, focusProps)}
774-
{...renderProps}
775-
data-key={rowProps['data-key']}
776-
data-collection={rowProps['data-collection']}
777-
data-focused={states.isFocused || undefined}
778-
data-focus-visible={isFocusVisible || undefined}
779-
data-level={level}>
780-
<div {...gridCellProps}>
781-
{renderProps.children}
782-
</div>
739+
<div
740+
role="row"
741+
ref={ref}
742+
{...mergeProps(filterDOMProps(props as any), ariaProps)}
743+
{...renderProps}
744+
data-level={level}>
745+
<div role="gridcell" aria-colindex={1}>
746+
{renderProps.children}
783747
</div>
784-
)}
748+
</div>
785749
</>
786750
);
787751
});
@@ -834,10 +798,9 @@ function flattenTree<T>(collection: TreeCollection<T>, opts: TreeGridCollectionO
834798
keyMap.set(node.key, node as CollectionNode<T>);
835799
}
836800

837-
// Grab the modified node from the key map so our flattened list and modified key map point to the same nodes
838-
let modifiedNode = keyMap.get(node.key) || node;
839-
if (modifiedNode.level === 0 || (modifiedNode.parentKey != null && expandedKeys.has(modifiedNode.parentKey) && flattenedRows.find(row => row.key === modifiedNode.parentKey))) {
840-
flattenedRows.push(modifiedNode);
801+
if (node.level === 0 || (parentKey != null && expandedKeys.has(parentKey) && flattenedRows.find(row => row.key === parentKey))) {
802+
// Grab the modified node from the key map so our flattened list and modified key map point to the same nodes
803+
flattenedRows.push(keyMap.get(node.key) || node);
841804
}
842805
} else if (node.type !== null) {
843806
keyMap.set(node.key, node as CollectionNode<T>);
@@ -951,5 +914,3 @@ function RootDropIndicator() {
951914
</div>
952915
);
953916
}
954-
955-

packages/react-aria-components/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export {ToggleButton, ToggleButtonContext} from './ToggleButton';
7575
export {ToggleButtonGroup, ToggleButtonGroupContext, ToggleGroupStateContext} from './ToggleButtonGroup';
7676
export {Toolbar, ToolbarContext} from './Toolbar';
7777
export {TooltipTrigger, Tooltip, TooltipTriggerStateContext, TooltipContext} from './Tooltip';
78-
export {UNSTABLE_TreeLoadingSentinel, Tree, TreeItem, TreeContext, TreeItemContent, TreeStateContext} from './Tree';
78+
export {UNSTABLE_TreeLoadingIndicator, Tree, TreeItem, TreeContext, TreeItemContent, TreeStateContext} from './Tree';
7979
export {useDragAndDrop} from './useDragAndDrop';
8080
export {DropIndicator, DropIndicatorContext, DragAndDropContext} from './DragAndDrop';
8181
export {Virtualizer} from './Virtualizer';

0 commit comments

Comments
 (0)