Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions packages/@react-aria/collections/src/useCachedChildren.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ export function useCachedChildren<T extends object>(props: CachedChildrenOptions
rendered = children(item);
// @ts-ignore
let key = rendered.props.id ?? item.key ?? item.id;

if (key == null) {
throw new Error('Could not determine key for item');
}

if (idScope != null) {
key = idScope + ':' + key;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/@react-aria/dnd/src/DropTargetKeyboardNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ function nextDropTarget(
nextKey = keyboardDelegate.getKeyBelow?.(target.key);
}
let nextCollectionKey = collection.getKeyAfter(target.key);
let nextCollectionNode = nextCollectionKey && collection.getItem(nextCollectionKey);
while (nextCollectionNode && nextCollectionNode.type === 'content') {
nextCollectionKey = nextCollectionKey ? collection.getKeyAfter(nextCollectionKey) : null;
nextCollectionNode = nextCollectionKey && collection.getItem(nextCollectionKey);
}

// If the keyboard delegate did not move to the next key in the collection,
// jump to that key with the same drop position. Otherwise, try the other
Expand Down
20 changes: 17 additions & 3 deletions packages/@react-aria/gridlist/src/useGridListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import {chain, getActiveElement, getEventTarget, getScrollParent, isFocusWithin, mergeProps, nodeContains, scrollIntoViewport, useSlotId, useSyntheticLinkProps} from '@react-aria/utils';
import {DOMAttributes, FocusableElement, Key, RefObject, Node as RSNode} from '@react-types/shared';
import {Collection, DOMAttributes, FocusableElement, Key, RefObject, Node as RSNode} from '@react-types/shared';
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
import {getRowId, listMap} from './utils';
import {HTMLAttributes, KeyboardEvent as ReactKeyboardEvent, useRef} from 'react';
Expand Down Expand Up @@ -100,11 +100,11 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt

let isExpanded = hasChildRows ? state.expandedKeys.has(node.key) : undefined;
let setSize = 1;
if (node.level > 0 && node?.parentKey != null) {
if (node.level >= 0 && node?.parentKey != null) {
let parent = state.collection.getItem(node.parentKey);
if (parent) {
// siblings must exist because our original node exists
let siblings = state.collection.getChildren?.(parent.key)!;
let siblings = getDirectChildren(parent, state.collection);
setSize = [...siblings].filter(row => row.type === 'item').length;
}
} else {
Expand Down Expand Up @@ -326,3 +326,17 @@ function last(walker: TreeWalker) {
} while (last);
return next;
}

function getDirectChildren<T>(parent: RSNode<T>, collection: Collection<RSNode<T>>) {
// We can't assume that we can use firstChildKey because if a person builds a tree using hooks, they would not have access to that property (using type Node vs CollectionNode)
// Instead, get all children and start at the first node (rather than just using firstChildKey) and only look at its siblings
let children = collection.getChildren?.(parent.key);
let childArray = children ? Array.from(children) : [];
let node = childArray.length > 0 ? childArray[0] : null;
let siblings: RSNode<T>[] = [];
while (node) {
siblings.push(node);
node = node.nextKey != null ? collection.getItem(node.nextKey) : null;
}
return siblings;
}
56 changes: 54 additions & 2 deletions packages/@react-aria/selection/src/ListKeyboardDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ interface ListKeyboardDelegateOptions<T> {
direction?: Direction,
disabledKeys?: Set<Key>,
disabledBehavior?: DisabledBehavior,
layoutDelegate?: LayoutDelegate
layoutDelegate?: LayoutDelegate,
expandedKeys?: Set<Key>
}

export class ListKeyboardDelegate<T> implements KeyboardDelegate {
Expand All @@ -36,8 +37,9 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
private orientation?: Orientation;
private direction?: Direction;
private layoutDelegate: LayoutDelegate;
private expandedKeys?: Set<Key>;

constructor(collection: Collection<Node<T>>, disabledKeys: Set<Key>, ref: RefObject<HTMLElement | null>, collator?: Intl.Collator);
constructor(collection: Collection<Node<T>>, disabledKeys: Set<Key>, ref: RefObject<HTMLElement | null>, collator?: Intl.Collator, expandedKeys?: Set<Key>);
constructor(options: ListKeyboardDelegateOptions<T>);
constructor(...args: any[]) {
if (args.length === 1) {
Expand All @@ -51,6 +53,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
this.direction = opts.direction;
this.layout = opts.layout || 'stack';
this.layoutDelegate = opts.layoutDelegate || new DOMLayoutDelegate(opts.ref);
this.expandedKeys = opts.expandedKeys;
} else {
this.collection = args[0];
this.disabledKeys = args[1];
Expand All @@ -60,6 +63,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
this.orientation = 'vertical';
this.disabledBehavior = 'all';
this.layoutDelegate = new DOMLayoutDelegate(this.ref);
this.expandedKeys = args[4];
}

// If this is a vertical stack, remove the left/right methods completely
Expand Down Expand Up @@ -88,6 +92,50 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
return null;
}

// Returns the first key that's visible starting from and inclusive of the provided key
private findNextVisible(key: Key | null): Key | null {
let node = key ? this.collection.getItem(key) : null;
if (!node) {
return null;
}

// If the node's parent is expanded, then we can assume that this is a visible node
if (node.parentKey && this.expandedKeys?.has(node.parentKey)) {
return node.key;
}

// If the node's parent is not expanded, find the top-most non-expanded node since it's possible for them to be nested
let parentNode = node.parentKey ? this.collection.getItem(node.parentKey) : null;
// if the the parent node is not a section, and the parent node is not included in expanded keys
while (parentNode && parentNode.type !== 'section' && node && node.parentKey && this.expandedKeys && !this.expandedKeys.has(parentNode.key)) {
node = this.collection.getItem(node.parentKey);
parentNode = node && node.parentKey ? this.collection.getItem(node.parentKey) : null;
}

return node?.key ?? null;
}

// Returns the first key that's visible and non-disabled starting from and inclusive of the provided key
private findNextNonDisabledVisible(key: Key | null, getNext: (key: Key) => Key | null) {
let nextKey = key;
while (nextKey !== null) {
let visibleKey = this.findNextVisible(nextKey);
// If visibleKey is null, that means there are no visibleKeys (don't feel like this is a real use case though, I would assume that there is always one visible node)
if (visibleKey == null) {
return null;
}

let node = this.collection.getItem(visibleKey);
if (node?.type === 'item' && !this.isDisabled(node)) {
return visibleKey;
}

nextKey = getNext(visibleKey);
}

return null;
}

getNextKey(key: Key): Key | null {
let nextKey: Key | null = key;
nextKey = this.collection.getKeyAfter(nextKey);
Expand Down Expand Up @@ -201,6 +249,10 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {

getLastKey(): Key | null {
let key = this.collection.getLastKey();
// we only need to check for visible keys if items can be expanded/collapsed
if (this.expandedKeys) {
return this.findNextNonDisabledVisible(key, key => this.collection.getKeyBefore(key));
}
return this.findNextNonDisabled(key, key => this.collection.getKeyBefore(key));
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@react-stately/data/src/useTreeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ function moveItems<T extends object>(
// decrement the index if the child being removed is in the target parent and before the target index
// the root node is special, it is null, and will not have a key, however, a parentKey can still point to it
if ((child.parentKey === toParent
|| child.parentKey === toParent?.key)
|| child.parentKey === toParent?.key)
&& keyArray.includes(child.key)
&& (toParent?.children ? toParent.children.indexOf(child) : items.indexOf(child)) < originalToIndex) {
toIndex--;
Expand Down
19 changes: 18 additions & 1 deletion packages/@react-stately/layout/src/ListLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,9 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte

protected buildCollection(y: number = this.padding): LayoutNode[] {
let collection = this.virtualizer!.collection;
let collectionNodes = [...collection];
// filter out content nodes since we don't want them to affect the height
// Tree specific for now, if we add content nodes to other collection items, we might need to reconsider this
let collectionNodes = toArray(collection, (node) => node.type !== 'content');
let loaderNodes = collectionNodes.filter(node => node.type === 'loader');
let nodes: LayoutNode[] = [];
let isEmptyOrLoading = collection?.size === 0;
Expand Down Expand Up @@ -370,6 +372,11 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
let skipped = 0;
let children: LayoutNode[] = [];
for (let child of getChildNodes(node, collection)) {
// skip if it is a content node, Tree specific for now, if we add content nodes to other collection items, we might need to reconsider this
if (child.type === 'content') {
continue;
}

let rowHeight = (this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT) + this.gap;

// Skip rows before the valid rectangle unless they are already cached.
Expand Down Expand Up @@ -617,3 +624,13 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
return new LayoutInfo('dropIndicator', target.key + ':' + target.dropPosition, rect);
}
}

function toArray<T>(collection: Collection<Node<T>>, predicate: (node: Node<T>) => boolean): Node<T>[] {
const result: Node<T>[] = [];
for (const node of collection) {
if (predicate(node)) {
result.push(node);
}
}
return result;
}
6 changes: 6 additions & 0 deletions packages/react-aria-components/src/Collection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ function useCollectionRender(
items: parent ? collection.getChildren!(parent.key) : collection,
dependencies: [renderDropIndicator],
children(node) {
// Return a empty fragment since we don't want to render the content twice
// If we don't skip the content node here, we end up rendering them twice in a Tree since we also render the content node in TreeItem
if (node.type === 'content') {
return <></>;
}

let rendered = node.render!(node);
if (!renderDropIndicator || node.type !== 'item') {
return rendered;
Expand Down
2 changes: 1 addition & 1 deletion packages/react-aria-components/src/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ export const GridListSection = /*#__PURE__*/ createBranchComponent(SectionNode,
export interface GridListHeaderProps extends DOMRenderProps<'div', undefined>, DOMProps, GlobalDOMAttributes<HTMLElement> {}

export const GridListHeaderContext = createContext<ContextValue<GridListHeaderProps, HTMLDivElement>>({});
const GridListHeaderInnerContext = createContext<HTMLAttributes<HTMLElement> | null>(null);
export const GridListHeaderInnerContext = createContext<HTMLAttributes<HTMLElement> | null>(null);

export const GridListHeader = /*#__PURE__*/ createLeafComponent(HeaderNode, function Header(props: GridListHeaderProps, ref: ForwardedRef<HTMLDivElement>) {
[props, ref] = useContextProps(props, ref, GridListHeaderContext);
Expand Down
Loading
Loading