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: (DNM) Test build for nested collection support #7616

Open
wants to merge 17 commits 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
3 changes: 2 additions & 1 deletion packages/@react-aria/collections/src/CollectionBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {BaseNode, Document, ElementNode} from './Document';
import {CachedChildrenOptions, useCachedChildren} from './useCachedChildren';
import {createPortal} from 'react-dom';
import {forwardRefType, Node} from '@react-types/shared';
import {getNodeKey} from './utils';
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 Down Expand Up @@ -199,7 +200,7 @@ const CollectionContext = createContext<CachedChildrenOptions<unknown> | null>(n
export function Collection<T extends object>(props: CollectionProps<T>): JSX.Element {
let ctx = useContext(CollectionContext)!;
let dependencies = (ctx?.dependencies || []).concat(props.dependencies);
let idScope = props.idScope || ctx?.idScope;
let idScope = props.idScope && ctx?.idScope ? getNodeKey(props.idScope, ctx.idScope) : (props.idScope || ctx?.idScope);
let children = useCollectionChildren({
...props,
idScope,
Expand Down
1 change: 1 addition & 0 deletions packages/@react-aria/collections/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {CollectionBuilder, Collection, createLeafComponent, createBranchComponen
export {createHideableComponent, useIsHidden} from './Hidden';
export {useCachedChildren} from './useCachedChildren';
export {BaseCollection, CollectionNode} from './BaseCollection';
export {getNodeKey} from './utils';

export type {CollectionBuilderProps, CollectionProps} from './CollectionBuilder';
export type {CachedChildrenOptions} from './useCachedChildren';
3 changes: 2 additions & 1 deletion packages/@react-aria/collections/src/useCachedChildren.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import {cloneElement, ReactElement, ReactNode, useMemo} from 'react';
import {getNodeKey} from './utils';
import {Key} from '@react-types/shared';

export interface CachedChildrenOptions<T> {
Expand Down Expand Up @@ -51,7 +52,7 @@ export function useCachedChildren<T extends object>(props: CachedChildrenOptions
}

if (idScope) {
key = idScope + ':' + key;
key = getNodeKey(key, idScope);
}
// Note: only works if wrapped Item passes through id...
rendered = cloneElement(
Expand Down
5 changes: 5 additions & 0 deletions packages/@react-aria/collections/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {Key} from '@react-types/shared';

export function getNodeKey(key: Key, idScope?: Key) {
return idScope ? `${idScope}:${key}` : `${key}`;
}
1 change: 1 addition & 0 deletions packages/@react-aria/dnd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"dependencies": {
"@internationalized/string": "^3.2.5",
"@react-aria/collections": "3.0.0-alpha.7",
"@react-aria/i18n": "^3.12.5",
"@react-aria/interactions": "^3.23.0",
"@react-aria/live-announcer": "^3.4.1",
Expand Down
6 changes: 4 additions & 2 deletions packages/@react-aria/dnd/src/ListDropTargetDelegate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Direction, DropTarget, DropTargetDelegate, Node, Orientation, RefObject} from '@react-types/shared';
import {getNodeKey} from '@react-aria/collections';

interface ListDropTargetDelegateOptions {
/**
Expand Down Expand Up @@ -87,7 +88,8 @@ export class ListDropTargetDelegate implements DropTargetDelegate {
let isSecondaryRTL = this.layout === 'grid' && this.orientation === 'vertical' && this.direction === 'rtl';
let isFlowRTL = this.layout === 'stack' ? isPrimaryRTL : isSecondaryRTL;

let elements = this.ref.current.querySelectorAll('[data-key]');
let idScope = this.ref.current?.dataset['scope'];
let elements = this.ref.current.querySelectorAll(idScope ? `[data-key^="${CSS.escape(idScope)}"]` : '[data-key]');
let elementMap = new Map<string, HTMLElement>();
for (let item of elements) {
if (item instanceof HTMLElement && item.dataset.key != null) {
Expand All @@ -105,7 +107,7 @@ export class ListDropTargetDelegate implements DropTargetDelegate {
while (low < high) {
let mid = Math.floor((low + high) / 2);
let item = items[mid];
let element = elementMap.get(String(item.key));
let element = elementMap.get(getNodeKey(item.key, idScope));
if (!element) {
break;
}
Expand Down
1 change: 1 addition & 0 deletions packages/@react-aria/grid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"url": "https://github.com/adobe/react-spectrum"
},
"dependencies": {
"@react-aria/collections": "3.0.0-alpha.7",
"@react-aria/focus": "^3.19.1",
"@react-aria/i18n": "^3.12.5",
"@react-aria/interactions": "^3.23.0",
Expand Down
16 changes: 12 additions & 4 deletions packages/@react-aria/grid/src/useGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ import {useSelectableCollection} from '@react-aria/selection';
export interface GridProps extends DOMProps, AriaLabelingProps {
/** Whether the grid 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 @@ -66,6 +71,7 @@ export interface GridAria {
export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<T>>, ref: RefObject<HTMLElement | null>): GridAria {
let {
isVirtualized,
disallowTypeAhead,
keyboardDelegate,
focusMode,
scrollRef,
Expand Down Expand Up @@ -94,17 +100,19 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
focusMode
}), [keyboardDelegate, state.collection, state.disabledKeys, disabledBehavior, ref, direction, collator, focusMode]);

let id = useId(props.id);
gridMap.set(state, {id, keyboardDelegate: delegate, actions: {onRowAction, onCellAction}});

let {collectionProps} = useSelectableCollection({
ref,
idScope: id,
selectionManager: manager,
keyboardDelegate: delegate,
isVirtualized,
scrollRef
scrollRef,
disallowTypeAhead
});

let id = useId(props.id);
gridMap.set(state, {keyboardDelegate: delegate, actions: {onRowAction, onCellAction}});

let descriptionProps = useHighlightSelectionDescription({
selectionManager: manager,
hasItemActions: !!(onRowAction || onCellAction)
Expand Down
6 changes: 4 additions & 2 deletions packages/@react-aria/grid/src/useGridCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import {DOMAttributes, FocusableElement, Key, RefObject} from '@react-types/shared';
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
import {getNodeKey} from '@react-aria/collections';
import {getScrollParent, mergeProps, scrollIntoViewport} from '@react-aria/utils';
import {GridCollection, GridNode} from '@react-types/grid';
import {gridMap} from './utils';
Expand Down Expand Up @@ -60,7 +61,7 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
} = props;

let {direction} = useLocale();
let {keyboardDelegate, actions: {onCellAction}} = gridMap.get(state)!;
let {id, keyboardDelegate, actions: {onCellAction}} = gridMap.get(state)!;

// We need to track the key of the item at the time it was last focused so that we force
// focus to go to the item when the DOM node is reused for a different item in a virtualizer.
Expand Down Expand Up @@ -251,7 +252,8 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
let gridCellProps: DOMAttributes = mergeProps(itemProps, {
role: 'gridcell',
onKeyDownCapture,
onFocus
onFocus,
'data-key': getNodeKey(node.key, id)
});

if (isVirtualized) {
Expand Down
11 changes: 6 additions & 5 deletions packages/@react-aria/grid/src/useGridRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
* governing permissions and limitations under the License.
*/

import {chain} from '@react-aria/utils';
import {chain, mergeProps} from '@react-aria/utils';
import {DOMAttributes, FocusableElement, RefObject} from '@react-types/shared';
import {getNodeKey} from '@react-aria/collections';
import {GridCollection, GridNode} from '@react-types/grid';
import {gridMap} from './utils';
import {GridState} from '@react-stately/grid';
Expand Down Expand Up @@ -52,7 +53,7 @@ export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T
onAction
} = props;

let {actions} = gridMap.get(state)!;
let {id, actions} = gridMap.get(state)!;
let onRowAction = actions.onRowAction ? () => actions.onRowAction?.(node.key) : onAction;
let {itemProps, ...states} = useSelectableItem({
selectionManager: state.selectionManager,
Expand All @@ -66,12 +67,12 @@ export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T

let isSelected = state.selectionManager.isSelected(node.key);

let rowProps: DOMAttributes = {
let rowProps: DOMAttributes = mergeProps(itemProps, {
role: 'row',
'aria-selected': state.selectionManager.selectionMode !== 'none' ? isSelected : undefined,
'aria-disabled': states.isDisabled || undefined,
...itemProps
};
'data-key': getNodeKey(node.key, id)
});

if (isVirtualized) {
rowProps['aria-rowindex'] = node.index + 1; // aria-rowindex is 1 based
Expand Down
1 change: 1 addition & 0 deletions packages/@react-aria/grid/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {GridState} from '@react-stately/grid';
import type {Key, KeyboardDelegate} from '@react-types/shared';

interface GridMapShared {
id: string,
keyboardDelegate: KeyboardDelegate,
actions: {
onRowAction?: (key: Key) => void,
Expand Down
1 change: 1 addition & 0 deletions packages/@react-aria/gridlist/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"url": "https://github.com/adobe/react-spectrum"
},
"dependencies": {
"@react-aria/collections": "3.0.0-alpha.7",
"@react-aria/focus": "^3.19.1",
"@react-aria/grid": "^3.11.1",
"@react-aria/i18n": "^3.12.5",
Expand Down
15 changes: 12 additions & 3 deletions 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 @@ -103,21 +109,24 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
console.warn('An aria-label or aria-labelledby prop is required for accessibility.');
}

let id = useId(props.id);
listMap.set(state, {id, onAction, linkBehavior, keyboardNavigationBehavior});

let {listProps} = useSelectableList({
selectionManager: state.selectionManager,
collection: state.collection,
disabledKeys: state.disabledKeys,
ref,
idScope: id,
keyboardDelegate,
layoutDelegate,
isVirtualized,
selectOnFocus: state.selectionManager.selectionBehavior === 'replace',
shouldFocusWrap: props.shouldFocusWrap,
linkBehavior
linkBehavior,
disallowTypeAhead
});

let id = useId(props.id);
listMap.set(state, {id, onAction, linkBehavior, keyboardNavigationBehavior});

let descriptionProps = useHighlightSelectionDescription({
selectionManager: state.selectionManager,
Expand Down
10 changes: 6 additions & 4 deletions packages/@react-aria/gridlist/src/useGridListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {chain, getScrollParent, mergeProps, scrollIntoViewport, useSlotId, useSy
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 {getNodeKey} from '@react-aria/collections';
import {getRowId, listMap} from './utils';
import {HTMLAttributes, KeyboardEvent as ReactKeyboardEvent, useRef} from 'react';
import {isFocusVisible} from '@react-aria/interactions';
Expand Down Expand Up @@ -67,7 +68,7 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt

// let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/gridlist');
let {direction} = useLocale();
let {onAction, linkBehavior, keyboardNavigationBehavior} = listMap.get(state)!;
let {id, onAction, linkBehavior, keyboardNavigationBehavior} = listMap.get(state)!;
let descriptionId = useSlotId();

// We need to track the key of the item at the time it was last focused so that we force
Expand Down Expand Up @@ -271,10 +272,11 @@ 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)
id: getRowId(state, node.key),
'data-key': getNodeKey(node.key, id)
});

if (isVirtualized) {
Expand Down
1 change: 1 addition & 0 deletions packages/@react-aria/selection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"url": "https://github.com/adobe/react-spectrum"
},
"dependencies": {
"@react-aria/collections": "3.0.0-alpha.7",
"@react-aria/focus": "^3.19.1",
"@react-aria/i18n": "^3.12.5",
"@react-aria/interactions": "^3.23.0",
Expand Down
4 changes: 3 additions & 1 deletion packages/@react-aria/selection/src/DOMLayoutDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* governing permissions and limitations under the License.
*/

import {getNodeKey} from '@react-aria/collections';
import {Key, LayoutDelegate, Rect, RefObject, Size} from '@react-types/shared';

export class DOMLayoutDelegate implements LayoutDelegate {
Expand All @@ -24,7 +25,8 @@ export class DOMLayoutDelegate implements LayoutDelegate {
if (!container) {
return null;
}
let item = key != null ? container.querySelector(`[data-key="${CSS.escape(key.toString())}"]`) : null;
let idScope = this.ref.current?.dataset['scope'];
let item = key != null ? container.querySelector(`[data-key="${CSS.escape(getNodeKey(key, idScope))}"]`) : null;
if (!item) {
return null;
}
Expand Down
18 changes: 11 additions & 7 deletions packages/@react-aria/selection/src/useSelectableCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {flushSync} from 'react-dom';
import {FocusEvent, KeyboardEvent, useEffect, useRef} from 'react';
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
import {getInteractionModality} from '@react-aria/interactions';
import {getNodeKey} from '@react-aria/collections';
import {isNonContiguousSelectionModifier} from './utils';
import {MultipleSelectionManager} from '@react-stately/selection';
import {useLocale} from '@react-aria/i18n';
Expand All @@ -34,6 +35,8 @@ export interface AriaSelectableCollectionOptions {
* The ref attached to the element representing the collection.
*/
ref: RefObject<HTMLElement | null>,
/** A scope to prepend to all child node keys to ensure they are unique. */
idScope?: Key,
/**
* Whether the collection or one of its items should be automatically focused upon render.
* @default false
Expand Down Expand Up @@ -104,6 +107,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
selectionManager: manager,
keyboardDelegate: delegate,
ref,
idScope,
autoFocus = false,
shouldFocusWrap = false,
disallowEmptySelection = false,
Expand Down Expand Up @@ -140,7 +144,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
manager.setFocusedKey(key, childFocus);
});

let item = scrollRef.current?.querySelector(`[data-key="${CSS.escape(key.toString())}"]`);
let item = scrollRef.current?.querySelector(`[data-key="${CSS.escape(getNodeKey(key, idScope))}"]`);
let itemProps = manager.getItemProps(key);
if (item) {
router.open(item, e, itemProps.href, itemProps.routerOptions);
Expand Down Expand Up @@ -368,7 +372,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions

if (manager.focusedKey != null && scrollRef.current) {
// Refocus and scroll the focused item into view if it exists within the scrollable region.
let element = scrollRef.current.querySelector(`[data-key="${CSS.escape(manager.focusedKey.toString())}"]`) as HTMLElement;
let element = scrollRef.current.querySelector(`[data-key="${CSS.escape(getNodeKey(manager.focusedKey, idScope))}"]`) as HTMLElement;
if (element) {
// This prevents a flash of focus on the first/last element in the collection, or the collection itself.
if (!element.contains(document.activeElement)) {
Expand Down Expand Up @@ -499,7 +503,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
useEffect(() => {
if (manager.isFocused && manager.focusedKey != null && (manager.focusedKey !== lastFocusedKey.current || autoFocusRef.current) && scrollRef.current && ref.current) {
let modality = getInteractionModality();
let element = ref.current.querySelector(`[data-key="${CSS.escape(manager.focusedKey.toString())}"]`) as HTMLElement;
let element = ref.current.querySelector(`[data-key="${CSS.escape(getNodeKey(manager.focusedKey, idScope))}"]`) as HTMLElement;
if (!element) {
// If item element wasn't found, return early (don't update autoFocusRef and lastFocusedKey).
// The collection may initially be empty (e.g. virtualizer), so wait until the element exists.
Expand Down Expand Up @@ -563,9 +567,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
}

return {
collectionProps: {
...handlers,
tabIndex
}
collectionProps: mergeProps(handlers, {
tabIndex,
'data-scope': idScope
})
};
}
4 changes: 2 additions & 2 deletions packages/@react-spectrum/tag/src/TagGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ export const TagGroup = React.forwardRef(function TagGroup<T extends object>(pro
: new ListCollection([...state.collection])) as Collection<Node<T>>;
return new ListKeyboardDelegate({
collection,
ref: domRef,
ref: tagsRef,
direction,
orientation: 'horizontal'
});
}, [direction, isCollapsed, state.collection, tagState.visibleTagCount, domRef]) as ListKeyboardDelegate<T>;
}, [direction, isCollapsed, state.collection, tagState.visibleTagCount, tagsRef]) as ListKeyboardDelegate<T>;
// Remove onAction from props so it doesn't make it into useGridList.
delete props.onAction;
let {gridProps, labelProps, descriptionProps, errorMessageProps} = useTagGroup({...props, keyboardDelegate}, state, tagsRef);
Expand Down
Loading
Loading