Skip to content

Commit a0e9f70

Browse files
committed
Revert "feat: Tree multiple level loading support (#8299)"
This reverts commit 6bbacb8.
1 parent 21d1950 commit a0e9f70

File tree

7 files changed

+70
-835
lines changed

7 files changed

+70
-835
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 & 60 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} from 'react';
2525
import {useControlledState} from '@react-stately/utils';
2626

@@ -699,87 +699,51 @@ export const TreeItem = /*#__PURE__*/ createBranchComponent('item', <T extends o
699699
);
700700
});
701701

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

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

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

739719
let ariaProps = {
740-
role: 'row',
741720
'aria-level': rowProps['aria-level'],
742721
'aria-posinset': rowProps['aria-posinset'],
743-
'aria-setsize': rowProps['aria-setsize'],
744-
tabIndex: rowProps.tabIndex
722+
'aria-setsize': rowProps['aria-setsize']
745723
};
746724

747-
let {isFocusVisible, focusProps} = useFocusRing();
748-
749725
let renderProps = useRenderProps({
750-
...otherProps,
726+
...props,
751727
id: undefined,
752728
children: item.rendered,
753729
defaultClassName: 'react-aria-TreeLoader',
754730
values: {
755-
level,
756-
isFocused: states.isFocused,
757-
isFocusVisible
731+
level
758732
}
759733
});
760734

761735
return (
762736
<>
763-
{/* 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) */}
764-
{/* @ts-ignore - compatibility with React < 19 */}
765-
<div style={{position: 'relative', width: 0, height: 0}} inert={inertValue(true)} >
766-
<div data-testid="loadMoreSentinel" ref={sentinelRef} style={{position: 'absolute', height: 1, width: 1}} />
767-
</div>
768-
{isLoading && renderProps.children && (
769-
<div
770-
ref={ref}
771-
{...mergeProps(filterDOMProps(props as any), ariaProps, focusProps)}
772-
{...renderProps}
773-
data-key={rowProps['data-key']}
774-
data-collection={rowProps['data-collection']}
775-
data-focused={states.isFocused || undefined}
776-
data-focus-visible={isFocusVisible || undefined}
777-
data-level={level}>
778-
<div {...gridCellProps}>
779-
{renderProps.children}
780-
</div>
737+
<div
738+
role="row"
739+
ref={ref}
740+
{...mergeProps(filterDOMProps(props as any), ariaProps)}
741+
{...renderProps}
742+
data-level={level}>
743+
<div role="gridcell" aria-colindex={1}>
744+
{renderProps.children}
781745
</div>
782-
)}
746+
</div>
783747
</>
784748
);
785749
});
@@ -832,10 +796,9 @@ function flattenTree<T>(collection: TreeCollection<T>, opts: TreeGridCollectionO
832796
keyMap.set(node.key, node as CollectionNode<T>);
833797
}
834798

835-
// Grab the modified node from the key map so our flattened list and modified key map point to the same nodes
836-
let modifiedNode = keyMap.get(node.key) || node;
837-
if (modifiedNode.level === 0 || (modifiedNode.parentKey != null && expandedKeys.has(modifiedNode.parentKey) && flattenedRows.find(row => row.key === modifiedNode.parentKey))) {
838-
flattenedRows.push(modifiedNode);
799+
if (node.level === 0 || (parentKey != null && expandedKeys.has(parentKey) && flattenedRows.find(row => row.key === parentKey))) {
800+
// Grab the modified node from the key map so our flattened list and modified key map point to the same nodes
801+
flattenedRows.push(keyMap.get(node.key) || node);
839802
}
840803
} else if (node.type !== null) {
841804
keyMap.set(node.key, node as CollectionNode<T>);
@@ -873,7 +836,7 @@ function TreeDropIndicatorWrapper(props: DropIndicatorProps, ref: ForwardedRef<H
873836
let level = dropState && props.target.type === 'item' ? (dropState.collection.getItem(props.target.key)?.level || 0) + 1 : 1;
874837

875838
return (
876-
<TreeDropIndicatorForwardRef
839+
<TreeDropIndicatorForwardRef
877840
{...props}
878841
dropIndicatorProps={dropIndicatorProps}
879842
isDropTarget={isDropTarget}

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)