Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: nested collection support #7379

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
9 changes: 8 additions & 1 deletion packages/@react-aria/collections/src/CollectionBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {BaseCollection} from './BaseCollection';
import {BaseNode, Document, ElementNode} from './Document';
import {CachedChildrenOptions, useCachedChildren} from './useCachedChildren';
import {createPortal} from 'react-dom';
import {forwardRefType, Node} from '@react-types/shared';
import {forwardRefType, Key, Node} from '@react-types/shared';
import {Hidden} from './Hidden';
import React, {createContext, ForwardedRef, forwardRef, JSX, ReactElement, ReactNode, useCallback, useContext, useMemo, useRef, useState} from 'react';
import {useIsSSR} from '@react-aria/ssr';
Expand All @@ -25,6 +25,7 @@ const ShallowRenderContext = createContext(false);
const CollectionDocumentContext = createContext<Document<any, BaseCollection<any>> | null>(null);

export interface CollectionBuilderProps<C extends BaseCollection<object>> {
id?: Key,
content: ReactNode,
children: (collection: C) => ReactNode,
createCollection?: () => C
Expand All @@ -51,6 +52,12 @@ export function CollectionBuilder<C extends BaseCollection<object>>(props: Colle
// This is fine. CollectionDocumentContext never changes after mounting.
// eslint-disable-next-line react-hooks/rules-of-hooks
let {collection, document} = useCollectionDocument(props.createCollection);

if (props.id) {
// @ts-expect-error
document.key = props.id;
}

return (
<>
<Hidden>
Expand Down
5 changes: 3 additions & 2 deletions packages/@react-aria/collections/src/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import {BaseCollection, CollectionNode, Mutable} from './BaseCollection';
import {ForwardedRef, ReactElement} from 'react';
import {Node} from '@react-types/shared';
import {Key, Node} from '@react-types/shared';

// This Collection implementation is perhaps a little unusual. It works by rendering the React tree into a
// Portal to a fake DOM implementation. This gives us efficient access to the tree of rendered objects, and
Expand Down Expand Up @@ -226,7 +226,7 @@ export class ElementNode<T> extends BaseNode<T> {

constructor(type: string, ownerDocument: Document<T, any>) {
super(ownerDocument);
this.node = new CollectionNode(type, `react-aria-${++ownerDocument.nodeId}`);
this.node = new CollectionNode(type, `${ownerDocument.key}-${++ownerDocument.nodeId}`);
// Start a transaction so that no updates are emitted from the collection
// until the props for this node are set. We don't know the real id for the
// node until then, so we need to avoid emitting collections in an inconsistent state.
Expand Down Expand Up @@ -304,6 +304,7 @@ export class ElementNode<T> extends BaseNode<T> {
* which is lazily copied on write during updates.
*/
export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extends BaseNode<T> {
readonly key: Key = 'react-aria';
nodeType = 11; // DOCUMENT_FRAGMENT_NODE
ownerDocument = this;
dirtyNodes: Set<BaseNode<T>> = new Set();
Expand Down
9 changes: 8 additions & 1 deletion packages/@react-aria/gridlist/src/useGridList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export interface AriaGridListProps<T> extends GridListProps<T>, DOMProps, AriaLa
export interface AriaGridListOptions<T> extends Omit<AriaGridListProps<T>, 'children'> {
/** Whether the list uses virtual scrolling. */
isVirtualized?: boolean,
/**
* Whether typeahead navigation is disabled.
* @default false
*/
disallowTypeAhead?: boolean,
/**
* An optional keyboard delegate implementation for type to select,
* to override the default.
Expand Down Expand Up @@ -95,6 +100,7 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
keyboardDelegate,
layoutDelegate,
onAction,
disallowTypeAhead,
linkBehavior = 'action',
keyboardNavigationBehavior = 'arrow'
} = props;
Expand All @@ -113,7 +119,8 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
isVirtualized,
selectOnFocus: state.selectionManager.selectionBehavior === 'replace',
shouldFocusWrap: props.shouldFocusWrap,
linkBehavior
linkBehavior,
disallowTypeAhead
});

let id = useId(props.id);
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/gridlist/src/useGridListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
onFocus,
// 'aria-label': [(node.textValue || undefined), rowAnnouncement].filter(Boolean).join(', '),
'aria-label': node.textValue || undefined,
'aria-selected': state.selectionManager.canSelectItem(node.key) ? state.selectionManager.isSelected(node.key) : undefined,
'aria-disabled': state.selectionManager.isDisabled(node.key) || undefined,
'aria-selected': itemStates.allowsSelection ? itemStates.isSelected : undefined,
'aria-disabled': itemStates.isDisabled || undefined,
'aria-labelledby': descriptionId && node.textValue ? `${getRowId(state, node.key)} ${descriptionId}` : undefined,
id: getRowId(state, node.key)
});
Expand Down
13 changes: 9 additions & 4 deletions packages/@react-aria/gridlist/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@ export function getRowId<T>(state: ListState<T>, key: Key) {
throw new Error('Unknown list');
}

return `${id}-${normalizeKey(key)}`;
return `${id}-${normalizeKey(state, key)}`;
}

export function normalizeKey(key: Key): string {
export function normalizeKey<T>(state: ListState<T>, key: Key): Key {
let {id} = listMap.get(state);
if (!id) {
throw new Error('Unknown list');
}

if (typeof key === 'string') {
return key.replace(/\s*/g, '');
return key.replace(/\s*/g, '').replace(id, '');
}

return '' + key;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure, why were we casting here?

return key;
}
11 changes: 9 additions & 2 deletions packages/react-aria-components/src/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, ScrollableProps, Slot
import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop';
import {DragAndDropHooks} from './useDragAndDrop';
import {DraggableCollectionState, DroppableCollectionState, Collection as ICollection, ListState, Node, SelectionBehavior, useListState} from 'react-stately';
import {filterDOMProps, useObjectRef} from '@react-aria/utils';
import {filterDOMProps, useId, useObjectRef} from '@react-aria/utils';
import {forwardRefType, HoverEvents, Key, LinkDOMProps, RefObject} from '@react-types/shared';
import {ListStateContext} from './ListBox';
import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
Expand Down Expand Up @@ -57,6 +57,11 @@ export interface GridListRenderProps {
}

export interface GridListProps<T> extends Omit<AriaGridListProps<T>, 'children'>, CollectionProps<T>, StyleRenderProps<GridListRenderProps>, SlotProps, ScrollableProps<HTMLDivElement> {
/**
* Whether typeahead navigation is disabled.
* @default false
*/
disallowTypeAhead?: boolean,
/** How multiple selection should behave in the collection. */
selectionBehavior?: SelectionBehavior,
/** The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the GridList. */
Expand All @@ -77,8 +82,10 @@ function GridList<T extends object>(props: GridListProps<T>, ref: ForwardedRef<H
// Render the portal first so that we have the collection by the time we render the DOM in SSR.
[props, ref] = useContextProps(props, ref, GridListContext);

props.id = useId(props.id);

return (
<CollectionBuilder content={<Collection {...props} />}>
<CollectionBuilder id={props.id} content={<Collection {...props} />}>
{collection => <GridListInner props={props} collection={collection} gridListRef={ref} />}
</CollectionBuilder>
);
Expand Down
9 changes: 8 additions & 1 deletion packages/react-aria-components/stories/GridList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,14 @@ export function TagGroupInsideGridList() {
</TagGroup>
</MyGridListItem>
<MyGridListItem>1,2 <Button>Actions</Button></MyGridListItem>
<MyGridListItem>1,3 <Button>Actions</Button></MyGridListItem>
<MyGridListItem>1,3
<TagGroup aria-label="Tag group">
<TagList style={{display: 'flex', gap: 10}}>
<Tag key="1">Tag 1</Tag>
<Tag key="2">Tag 2</Tag>
<Tag key="3">Tag 3</Tag>
</TagList>
</TagGroup></MyGridListItem>
</GridList>
);
}