From 54c44fe3be4fca7d75eb1500f1cf8e84ec89a9ad Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 31 Jan 2025 14:23:08 +1100 Subject: [PATCH 1/2] fix logic to determine hasChildNodes --- .../@react-aria/collections/src/Document.ts | 13 ++++++++- .../gridlist/src/useGridListItem.ts | 10 +++---- packages/@react-spectrum/s2/src/TreeView.tsx | 23 ++++++--------- .../@react-spectrum/tree/src/TreeView.tsx | 28 ++++++------------- .../tree/test/TreeView.test.tsx | 2 +- .../@react-stately/grid/src/useGridState.ts | 5 ++-- .../selection/src/SelectionManager.ts | 2 +- .../@react-types/shared/src/collections.d.ts | 4 +++ packages/react-aria-components/src/Table.tsx | 2 +- packages/react-aria-components/src/Tree.tsx | 6 ++-- .../react-aria-components/test/Table.test.js | 6 ++-- 11 files changed, 48 insertions(+), 53 deletions(-) diff --git a/packages/@react-aria/collections/src/Document.ts b/packages/@react-aria/collections/src/Document.ts index 7f95421b42d..4d4a79b7563 100644 --- a/packages/@react-aria/collections/src/Document.ts +++ b/packages/@react-aria/collections/src/Document.ts @@ -257,7 +257,18 @@ export class ElementNode extends BaseNode { node.parentKey = this.parentNode instanceof ElementNode ? this.parentNode.node.key : null; node.prevKey = this.previousSibling?.node.key ?? null; node.nextKey = this.nextSibling?.node.key ?? null; - node.hasChildNodes = !!this.firstChild; + + // Check if this node has any child nodes, but specifically any that are items. + let child = this.firstChild; + let hasChildNodes = false; + while (!hasChildNodes && child && child?.nextSibling) { + child = child.nextSibling; + if (child.node.type === 'item') { + hasChildNodes = true; + } + } + + node.hasChildNodes = hasChildNodes; node.firstChildKey = this.firstChild?.node.key ?? null; node.lastChildKey = this.lastChild?.node.key ?? null; } diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index a6e5a910838..7fd56d6fe1c 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -28,7 +28,9 @@ export interface AriaGridListItemOptions { /** Whether the list row is contained in a virtual scroller. */ isVirtualized?: boolean, /** Whether selection should occur on press up instead of press down. */ - shouldSelectOnPressUp?: boolean + shouldSelectOnPressUp?: boolean, + /** Whether this item has children, even if not loaded yet. */ + hasChildItems?: boolean } export interface GridListItemAria extends SelectableItemStates { @@ -86,13 +88,9 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt }; let treeGridRowProps: HTMLAttributes = {}; - let hasChildRows; + let hasChildRows = props.hasChildItems || node.hasChildNodes; let hasLink = state.selectionManager.isLink(node.key); if (node != null && 'expandedKeys' in state) { - // TODO: ideally node.hasChildNodes would be a way to tell if a row has child nodes, but the row's contents make it so that value is always - // true... - let children = state.collection.getChildren?.(node.key); - hasChildRows = [...(children ?? [])].length > 1; if (onAction == null && !hasLink && state.selectionManager.selectionMode === 'none' && hasChildRows) { onAction = () => state.toggleKey(node.key); } diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index b82e6337bec..61b46379fa0 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -294,8 +294,6 @@ const treeRowFocusIndicator = raw(` }` ); -let TreeItemContext = createContext<{hasChildItems?: boolean}>({}); - export const TreeViewItem = (props: TreeViewItemProps) => { let { href @@ -303,15 +301,12 @@ export const TreeViewItem = (props: TreeViewItemProps) => { let {isDetached, isEmphasized} = useContext(InternalTreeContext); return ( - // TODO right now all the tree rows have the various data attributes applied on their dom nodes, should they? Doesn't feel very useful - - treeRow({ - ...renderProps, - isLink: !!href, isEmphasized - }) + (renderProps.isFocusVisible && !isDetached ? ' ' + treeRowFocusIndicator : '')} /> - + treeRow({ + ...renderProps, + isLink: !!href, isEmphasized + }) + (renderProps.isFocusVisible && !isDetached ? ' ' + treeRowFocusIndicator : '')} /> ); }; @@ -320,7 +315,6 @@ export const TreeItemContent = (props: Omit & children } = props; let {isDetached, isEmphasized} = useContext(InternalTreeContext); - let {hasChildItems} = useContext(TreeItemContext); return ( @@ -348,7 +342,7 @@ export const TreeItemContent = (props: Omit & width: '[calc(calc(var(--tree-item-level, 0) - 1) * var(--indent))]' })} /> {/* TODO: revisit when we do async loading, at the moment hasChildItems will only cause the chevron to be rendered, no aria/data attributes indicating the row's expandability are added */} - {(hasChildRows || hasChildItems) && } + {hasChildRows && } & styles: style({size: fontRelative(20), flexShrink: 0}) }], [ActionButtonGroupContext, {styles: treeActions}], - [ActionMenuContext, {styles: treeActionMenu, isQuiet: true, 'aria-label': 'Actions'}], - [TreeItemContext, {}] + [ActionMenuContext, {styles: treeActionMenu, isQuiet: true, 'aria-label': 'Actions'}] ]}> {children} diff --git a/packages/@react-spectrum/tree/src/TreeView.tsx b/packages/@react-spectrum/tree/src/TreeView.tsx index 361e3276333..c26e56e7d68 100644 --- a/packages/@react-spectrum/tree/src/TreeView.tsx +++ b/packages/@react-spectrum/tree/src/TreeView.tsx @@ -17,7 +17,7 @@ import ChevronLeftMedium from '@spectrum-icons/ui/ChevronLeftMedium'; import ChevronRightMedium from '@spectrum-icons/ui/ChevronRightMedium'; import {DOMRef, Expandable, Key, SelectionBehavior, SpectrumSelectionProps, StyleProps} from '@react-types/shared'; import {isAndroid} from '@react-aria/utils'; -import React, {createContext, JSX, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; +import React, {createContext, JSX, JSXElementConstructor, ReactElement, ReactNode, useRef} from 'react'; import {SlotProvider, useDOMRef, useStyleProps} from '@react-spectrum/utils'; import {style} from '@react-spectrum/style-macro-s1' with {type: 'macro'}; import {useButton} from '@react-aria/button'; @@ -40,8 +40,6 @@ export interface SpectrumTreeViewProps extends Omit, export interface SpectrumTreeViewItemProps extends Omit { /** Rendered contents of the tree item or child items. */ children: ReactNode, - /** Whether this item has children, even if not loaded yet. */ - hasChildItems?: boolean, /** A list of child tree item objects used when dynamically rendering the tree item children. */ childItems?: Iterable } @@ -219,23 +217,18 @@ const treeRowOutline = style({ } }); -let TreeItemContext = createContext<{hasChildItems?: boolean}>({}); - export const TreeViewItem = (props: SpectrumTreeViewItemProps) => { let { href } = props; return ( - // TODO right now all the tree rows have the various data attributes applied on their dom nodes, should they? Doesn't feel very useful - - treeRow({ - ...renderProps, - isLink: !!href - })} /> - + treeRow({ + ...renderProps, + isLink: !!href + })} /> ); }; @@ -244,7 +237,6 @@ export const TreeItemContent = (props: Omit & let { children } = props; - let {hasChildItems} = useContext(TreeItemContext); return ( @@ -260,7 +252,7 @@ export const TreeItemContent = (props: Omit & )}
{/* TODO: revisit when we do async loading, at the moment hasChildItems will only cause the chevron to be rendered, no aria/data attributes indicating the row's expandability are added */} - {(hasChildRows || hasChildItems) && } + {hasChildRows && } & }, actionMenu: {UNSAFE_className: treeActionMenu(), UNSAFE_style: {marginInlineEnd: '.5rem'}, isQuiet: true} }}> - - {children} - + {children}
diff --git a/packages/@react-spectrum/tree/test/TreeView.test.tsx b/packages/@react-spectrum/tree/test/TreeView.test.tsx index d82df73f14d..dceb1c96756 100644 --- a/packages/@react-spectrum/tree/test/TreeView.test.tsx +++ b/packages/@react-spectrum/tree/test/TreeView.test.tsx @@ -480,7 +480,7 @@ describe('Tree', () => { expect(rows[0]).toHaveAttribute('aria-label', 'Test'); // Until the row gets children, don't mark it with the aria/data attributes. expect(rows[0]).not.toHaveAttribute('aria-expanded'); - expect(rows[0]).not.toHaveAttribute('data-has-child-rows'); + expect(rows[0]).toHaveAttribute('data-has-child-rows'); expect(chevron).toBeTruthy(); }); diff --git a/packages/@react-stately/grid/src/useGridState.ts b/packages/@react-stately/grid/src/useGridState.ts index 818c23391cb..229e9498efa 100644 --- a/packages/@react-stately/grid/src/useGridState.ts +++ b/packages/@react-stately/grid/src/useGridState.ts @@ -97,9 +97,10 @@ export function useGridState>(prop } } if (newRow) { - const childNodes = newRow.hasChildNodes ? [...getChildNodes(newRow, collection)] : []; + let hasChildNodes = newRow.firstChildKey === undefined ? newRow.hasChildNodes : newRow.firstChildKey != null; + const childNodes = hasChildNodes ? [...getChildNodes(newRow, collection)] : []; const keyToFocus = - newRow.hasChildNodes && + hasChildNodes && parentNode !== node && node && node.index < childNodes.length ? diff --git a/packages/@react-stately/selection/src/SelectionManager.ts b/packages/@react-stately/selection/src/SelectionManager.ts index 5c30d4f4c29..47d7c77c1c3 100644 --- a/packages/@react-stately/selection/src/SelectionManager.ts +++ b/packages/@react-stately/selection/src/SelectionManager.ts @@ -398,7 +398,7 @@ export class SelectionManager implements MultipleSelectionManager { } // Add child keys. If cell selection is allowed, then include item children too. - if (item?.hasChildNodes && (this.allowsCellSelection || item.type !== 'item')) { + if (item && (item?.firstChildKey === undefined ? item?.hasChildNodes : item?.firstChildKey != null) && (this.allowsCellSelection || item.type !== 'item')) { addKeys(getFirstItem(getChildNodes(item, this.collection))?.key ?? null); } } diff --git a/packages/@react-types/shared/src/collections.d.ts b/packages/@react-types/shared/src/collections.d.ts index f627cdf3511..5f52b03796f 100644 --- a/packages/@react-types/shared/src/collections.d.ts +++ b/packages/@react-types/shared/src/collections.d.ts @@ -215,6 +215,10 @@ export interface Node { prevKey?: Key | null, /** The key of the node after this node. */ nextKey?: Key | null, + /** The key of the first child node. */ + firstChildKey?: Key | null, + /** The key of the last child node. */ + lastChildKey?: Key | null, /** Additional properties specific to a particular node type. */ props?: any, /** @private */ diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index f0010c1a9c0..4956d9b3d33 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -58,7 +58,7 @@ class TableCollection extends BaseCollection implements ITableCollection(null); -export interface TreeItemProps extends StyleRenderProps, LinkDOMProps, HoverEvents { +export interface TreeItemProps extends StyleRenderProps, LinkDOMProps, HoverEvents, Pick { /** The unique id of the tree row. */ id?: Key, /** The object value that this tree item represents. When using dynamic collections, this is set automatically. */ @@ -325,7 +325,7 @@ export const UNSTABLE_TreeItem = /*#__PURE__*/ createBranchComponent('item', 1; + let hasChildRows = props.hasChildItems || item.hasChildNodes; let level = rowProps['aria-level'] || 1; let {hoverProps, isHovered} = useHover({ diff --git a/packages/react-aria-components/test/Table.test.js b/packages/react-aria-components/test/Table.test.js index 5c5f5e61228..e1e6c3c1c4d 100644 --- a/packages/react-aria-components/test/Table.test.js +++ b/packages/react-aria-components/test/Table.test.js @@ -668,10 +668,8 @@ describe('Table', () => { let tableTester = testUtilUser.createTester('Table', {root: getByRole('grid')}); await user.tab(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'}); - fireEvent.keyDown(document.activeElement, {key: 'ArrowRight'}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowRight'}); + await user.keyboard('{ArrowDown}'); + await user.keyboard('{ArrowRight}'); let gridRows = tableTester.rows; expect(gridRows).toHaveLength(4); From afe91b853a6eab2b4ea2361dd6da2dcad3fb49d5 Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 12 Feb 2025 09:53:39 +1100 Subject: [PATCH 2/2] revert breaking changes but use prop override --- .../@react-aria/collections/src/Document.ts | 13 +----- .../gridlist/src/useGridListItem.ts | 7 ++- packages/@react-spectrum/s2/src/TreeView.tsx | 4 +- .../@react-spectrum/tree/src/TreeView.tsx | 4 +- .../tree/test/TreeView.test.tsx | 38 +++++++-------- .../@react-stately/grid/src/useGridState.ts | 5 +- .../selection/src/SelectionManager.ts | 2 +- .../@react-types/shared/src/collections.d.ts | 4 -- packages/react-aria-components/docs/Tree.mdx | 2 +- packages/react-aria-components/src/Table.tsx | 2 +- packages/react-aria-components/src/Tree.tsx | 16 +++---- .../stories/Tree.stories.tsx | 18 ++++---- .../react-aria-components/test/Tree.test.tsx | 46 +++++++++---------- 13 files changed, 75 insertions(+), 86 deletions(-) diff --git a/packages/@react-aria/collections/src/Document.ts b/packages/@react-aria/collections/src/Document.ts index 4d4a79b7563..7f95421b42d 100644 --- a/packages/@react-aria/collections/src/Document.ts +++ b/packages/@react-aria/collections/src/Document.ts @@ -257,18 +257,7 @@ export class ElementNode extends BaseNode { node.parentKey = this.parentNode instanceof ElementNode ? this.parentNode.node.key : null; node.prevKey = this.previousSibling?.node.key ?? null; node.nextKey = this.nextSibling?.node.key ?? null; - - // Check if this node has any child nodes, but specifically any that are items. - let child = this.firstChild; - let hasChildNodes = false; - while (!hasChildNodes && child && child?.nextSibling) { - child = child.nextSibling; - if (child.node.type === 'item') { - hasChildNodes = true; - } - } - - node.hasChildNodes = hasChildNodes; + node.hasChildNodes = !!this.firstChild; node.firstChildKey = this.firstChild?.node.key ?? null; node.lastChildKey = this.lastChild?.node.key ?? null; } diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index 7fd56d6fe1c..0e83bc86839 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -88,9 +88,14 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt }; let treeGridRowProps: HTMLAttributes = {}; - let hasChildRows = props.hasChildItems || node.hasChildNodes; + let hasChildRows = props.hasChildItems; let hasLink = state.selectionManager.isLink(node.key); if (node != null && 'expandedKeys' in state) { + // TODO: ideally node.hasChildNodes would be a way to tell if a row has child nodes, but the row's contents make it so that value is always + // true... + let children = state.collection.getChildren?.(node.key); + hasChildRows = [...(children ?? [])].length > 1; + if (onAction == null && !hasLink && state.selectionManager.selectionMode === 'none' && hasChildRows) { onAction = () => state.toggleKey(node.key); } diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 61b46379fa0..40cc5194ede 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -318,7 +318,7 @@ export const TreeItemContent = (props: Omit & return ( - {({isExpanded, hasChildRows, selectionMode, selectionBehavior, isDisabled, isFocusVisible, isSelected, id, state}) => { + {({isExpanded, hasChildItems, selectionMode, selectionBehavior, isDisabled, isFocusVisible, isSelected, id, state}) => { let isNextSelected = false; let isNextFocused = false; let keyAfter = state.collection.getKeyAfter(id); @@ -342,7 +342,7 @@ export const TreeItemContent = (props: Omit & width: '[calc(calc(var(--tree-item-level, 0) - 1) * var(--indent))]' })} /> {/* TODO: revisit when we do async loading, at the moment hasChildItems will only cause the chevron to be rendered, no aria/data attributes indicating the row's expandability are added */} - {hasChildRows && } + {hasChildItems && } & return ( - {({isExpanded, hasChildRows, level, selectionMode, selectionBehavior, isDisabled, isSelected, isFocusVisible}) => ( + {({isExpanded, hasChildItems, level, selectionMode, selectionBehavior, isDisabled, isSelected, isFocusVisible}) => (
{selectionMode !== 'none' && selectionBehavior === 'toggle' && ( // TODO: add transition? @@ -252,7 +252,7 @@ export const TreeItemContent = (props: Omit & )}
{/* TODO: revisit when we do async loading, at the moment hasChildItems will only cause the chevron to be rendered, no aria/data attributes indicating the row's expandability are added */} - {hasChildRows && } + {hasChildItems && } { expect(rowNoChild).toHaveAttribute('data-level', '1'); expect(rowNoChild).toHaveAttribute('aria-posinset', '1'); expect(rowNoChild).toHaveAttribute('aria-setsize', '2'); - expect(rowNoChild).not.toHaveAttribute('data-has-child-rows'); + expect(rowNoChild).not.toHaveAttribute('data-has-child-items'); expect(rowNoChild).toHaveAttribute('data-rac'); let rowWithChildren = rows[1]; @@ -305,7 +305,7 @@ describe('Tree', () => { expect(rowWithChildren).toHaveAttribute('data-level', '1'); expect(rowWithChildren).toHaveAttribute('aria-posinset', '2'); expect(rowWithChildren).toHaveAttribute('aria-setsize', '2'); - expect(rowWithChildren).toHaveAttribute('data-has-child-rows', 'true'); + expect(rowWithChildren).toHaveAttribute('data-has-child-items', 'true'); expect(rowWithChildren).toHaveAttribute('data-rac'); let level2ChildRow = rows[2]; @@ -316,7 +316,7 @@ describe('Tree', () => { expect(level2ChildRow).toHaveAttribute('data-level', '2'); expect(level2ChildRow).toHaveAttribute('aria-posinset', '1'); expect(level2ChildRow).toHaveAttribute('aria-setsize', '3'); - expect(level2ChildRow).toHaveAttribute('data-has-child-rows', 'true'); + expect(level2ChildRow).toHaveAttribute('data-has-child-items', 'true'); expect(level2ChildRow).toHaveAttribute('data-rac'); let level3ChildRow = rows[3]; @@ -327,7 +327,7 @@ describe('Tree', () => { expect(level3ChildRow).toHaveAttribute('data-level', '3'); expect(level3ChildRow).toHaveAttribute('aria-posinset', '1'); expect(level3ChildRow).toHaveAttribute('aria-setsize', '1'); - expect(level3ChildRow).not.toHaveAttribute('data-has-child-rows'); + expect(level3ChildRow).not.toHaveAttribute('data-has-child-items'); expect(level3ChildRow).toHaveAttribute('data-rac'); let level2ChildRow2 = rows[4]; @@ -338,7 +338,7 @@ describe('Tree', () => { expect(level2ChildRow2).toHaveAttribute('data-level', '2'); expect(level2ChildRow2).toHaveAttribute('aria-posinset', '2'); expect(level2ChildRow2).toHaveAttribute('aria-setsize', '3'); - expect(level2ChildRow2).not.toHaveAttribute('data-has-child-rows'); + expect(level2ChildRow2).not.toHaveAttribute('data-has-child-items'); expect(level2ChildRow2).toHaveAttribute('data-rac'); let level2ChildRow3 = rows[5]; @@ -349,7 +349,7 @@ describe('Tree', () => { expect(level2ChildRow3).toHaveAttribute('data-level', '2'); expect(level2ChildRow3).toHaveAttribute('aria-posinset', '3'); expect(level2ChildRow3).toHaveAttribute('aria-setsize', '3'); - expect(level2ChildRow3).not.toHaveAttribute('data-has-child-rows'); + expect(level2ChildRow3).not.toHaveAttribute('data-has-child-items'); expect(level2ChildRow3).toHaveAttribute('data-rac'); }); @@ -358,7 +358,7 @@ describe('Tree', () => { let rows = getAllByRole('row'); expect(rows[1]).toHaveAttribute('aria-label', 'Projects'); - expect(rows[1]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[1]).toHaveAttribute('data-has-child-items', 'true'); expect(rows[1]).toHaveAttribute('aria-selected', 'false'); }); @@ -374,28 +374,28 @@ describe('Tree', () => { expect(rows[0]).toHaveAttribute('aria-level', '1'); expect(rows[0]).toHaveAttribute('aria-posinset', '1'); expect(rows[0]).toHaveAttribute('aria-setsize', '2'); - expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[0]).toHaveAttribute('data-has-child-items', 'true'); expect(rows[2]).toHaveAttribute('aria-label', 'Project 2'); expect(rows[2]).toHaveAttribute('aria-expanded', 'true'); expect(rows[2]).toHaveAttribute('aria-level', '2'); expect(rows[2]).toHaveAttribute('aria-posinset', '2'); expect(rows[2]).toHaveAttribute('aria-setsize', '5'); - expect(rows[2]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[2]).toHaveAttribute('data-has-child-items', 'true'); expect(rows[8]).toHaveAttribute('aria-label', 'Project 5'); expect(rows[8]).toHaveAttribute('aria-expanded', 'true'); expect(rows[8]).toHaveAttribute('aria-level', '2'); expect(rows[8]).toHaveAttribute('aria-posinset', '5'); expect(rows[8]).toHaveAttribute('aria-setsize', '5'); - expect(rows[8]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[8]).toHaveAttribute('data-has-child-items', 'true'); expect(rows[12]).toHaveAttribute('aria-label', 'Reports'); expect(rows[12]).toHaveAttribute('aria-expanded', 'true'); expect(rows[12]).toHaveAttribute('aria-level', '1'); expect(rows[12]).toHaveAttribute('aria-posinset', '2'); expect(rows[12]).toHaveAttribute('aria-setsize', '2'); - expect(rows[12]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[12]).toHaveAttribute('data-has-child-items', 'true'); expect(rows[16]).toHaveAttribute('aria-label', 'Reports 1ABC'); expect(rows[16]).toHaveAttribute('aria-level', '5'); @@ -463,7 +463,7 @@ describe('Tree', () => { expect(treeTester.selectedRows[0]).toBe(row1); }); - it('should render a chevron for an expandable row marked with hasChildRows', () => { + it('should render a chevron for an expandable row marked with hasChildItems', () => { let {getAllByRole} = render( @@ -480,7 +480,7 @@ describe('Tree', () => { expect(rows[0]).toHaveAttribute('aria-label', 'Test'); // Until the row gets children, don't mark it with the aria/data attributes. expect(rows[0]).not.toHaveAttribute('aria-expanded'); - expect(rows[0]).toHaveAttribute('data-has-child-rows'); + expect(rows[0]).toHaveAttribute('data-has-child-items'); expect(chevron).toBeTruthy(); }); @@ -893,7 +893,7 @@ describe('Tree', () => { expect(rows[0]).toHaveAttribute('aria-level', '1'); expect(rows[0]).toHaveAttribute('aria-posinset', '1'); expect(rows[0]).toHaveAttribute('aria-setsize', '2'); - expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[0]).toHaveAttribute('data-has-child-items', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(0); // Check we can open/close a top level row @@ -904,7 +904,7 @@ describe('Tree', () => { expect(rows[0]).toHaveAttribute('aria-level', '1'); expect(rows[0]).toHaveAttribute('aria-posinset', '1'); expect(rows[0]).toHaveAttribute('aria-setsize', '2'); - expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[0]).toHaveAttribute('data-has-child-items', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(1); // Note that the children of the parent row will still be in the "expanded" array expect(new Set(onExpandedChange.mock.calls[0][0])).toEqual(new Set(['Project-2', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); @@ -918,7 +918,7 @@ describe('Tree', () => { expect(rows[0]).toHaveAttribute('aria-level', '1'); expect(rows[0]).toHaveAttribute('aria-posinset', '1'); expect(rows[0]).toHaveAttribute('aria-setsize', '2'); - expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[0]).toHaveAttribute('data-has-child-items', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(2); expect(new Set(onExpandedChange.mock.calls[1][0])).toEqual(new Set(['Projects', 'Project-2', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); rows = treeTester.rows; @@ -932,7 +932,7 @@ describe('Tree', () => { expect(rows[2]).toHaveAttribute('aria-level', '2'); expect(rows[2]).toHaveAttribute('aria-posinset', '2'); expect(rows[2]).toHaveAttribute('aria-setsize', '5'); - expect(rows[2]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[2]).toHaveAttribute('data-has-child-items', 'true'); // Check we can close a nested row and it doesn't affect the parent await treeTester.toggleRowExpansion({row: rows[2], interactionType: type as 'mouse' | 'keyboard'}); @@ -942,13 +942,13 @@ describe('Tree', () => { expect(rows[2]).toHaveAttribute('aria-level', '2'); expect(rows[2]).toHaveAttribute('aria-posinset', '2'); expect(rows[2]).toHaveAttribute('aria-setsize', '5'); - expect(rows[2]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[2]).toHaveAttribute('data-has-child-items', 'true'); expect(rows[0]).toHaveAttribute('aria-expanded', 'true'); expect(rows[0]).toHaveAttribute('data-expanded', 'true'); expect(rows[0]).toHaveAttribute('aria-level', '1'); expect(rows[0]).toHaveAttribute('aria-posinset', '1'); expect(rows[0]).toHaveAttribute('aria-setsize', '2'); - expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[0]).toHaveAttribute('data-has-child-items', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(3); expect(new Set(onExpandedChange.mock.calls[2][0])).toEqual(new Set(['Projects', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); rows = treeTester.rows; diff --git a/packages/@react-stately/grid/src/useGridState.ts b/packages/@react-stately/grid/src/useGridState.ts index 229e9498efa..818c23391cb 100644 --- a/packages/@react-stately/grid/src/useGridState.ts +++ b/packages/@react-stately/grid/src/useGridState.ts @@ -97,10 +97,9 @@ export function useGridState>(prop } } if (newRow) { - let hasChildNodes = newRow.firstChildKey === undefined ? newRow.hasChildNodes : newRow.firstChildKey != null; - const childNodes = hasChildNodes ? [...getChildNodes(newRow, collection)] : []; + const childNodes = newRow.hasChildNodes ? [...getChildNodes(newRow, collection)] : []; const keyToFocus = - hasChildNodes && + newRow.hasChildNodes && parentNode !== node && node && node.index < childNodes.length ? diff --git a/packages/@react-stately/selection/src/SelectionManager.ts b/packages/@react-stately/selection/src/SelectionManager.ts index 47d7c77c1c3..5c30d4f4c29 100644 --- a/packages/@react-stately/selection/src/SelectionManager.ts +++ b/packages/@react-stately/selection/src/SelectionManager.ts @@ -398,7 +398,7 @@ export class SelectionManager implements MultipleSelectionManager { } // Add child keys. If cell selection is allowed, then include item children too. - if (item && (item?.firstChildKey === undefined ? item?.hasChildNodes : item?.firstChildKey != null) && (this.allowsCellSelection || item.type !== 'item')) { + if (item?.hasChildNodes && (this.allowsCellSelection || item.type !== 'item')) { addKeys(getFirstItem(getChildNodes(item, this.collection))?.key ?? null); } } diff --git a/packages/@react-types/shared/src/collections.d.ts b/packages/@react-types/shared/src/collections.d.ts index 5f52b03796f..f627cdf3511 100644 --- a/packages/@react-types/shared/src/collections.d.ts +++ b/packages/@react-types/shared/src/collections.d.ts @@ -215,10 +215,6 @@ export interface Node { prevKey?: Key | null, /** The key of the node after this node. */ nextKey?: Key | null, - /** The key of the first child node. */ - firstChildKey?: Key | null, - /** The key of the last child node. */ - lastChildKey?: Key | null, /** Additional properties specific to a particular node type. */ props?: any, /** @private */ diff --git a/packages/react-aria-components/docs/Tree.mdx b/packages/react-aria-components/docs/Tree.mdx index 2b9a2de801d..ff9253ed617 100644 --- a/packages/react-aria-components/docs/Tree.mdx +++ b/packages/react-aria-components/docs/Tree.mdx @@ -134,7 +134,7 @@ let items = [ position: relative; transform: translateZ(0); - &[data-has-child-rows] { + &[data-has-child-items] { --padding: 0px; } diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index 4956d9b3d33..f0010c1a9c0 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -58,7 +58,7 @@ class TableCollection extends BaseCollection implements ITableCollection 1;; let level = rowProps['aria-level'] || 1; let {hoverProps, isHovered} = useHover({ @@ -350,14 +350,14 @@ export const UNSTABLE_TreeItem = /*#__PURE__*/ createBranchComponent('item', (null); useEffect(() => { - if (hasChildRows && !expandButtonRef.current) { + if (hasChildItems && !expandButtonRef.current) { console.warn('Expandable tree items must contain a expand button so screen reader users can expand/collapse the item.'); } // eslint-disable-next-line @@ -410,8 +410,8 @@ export const UNSTABLE_TreeItem = /*#__PURE__*/ createBranchComponent('item', { hovered: isHovered })}> - {({isExpanded, hasChildRows, level, selectionMode, selectionBehavior}) => ( + {({isExpanded, hasChildItems, level, selectionMode, selectionBehavior}) => ( <> {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( )}
- {hasChildRows && } + style={{marginInlineStart: `${(!hasChildItems ? 20 : 0) + (level - 1) * 15}px`}}> + {hasChildItems && } {props.title || props.children} @@ -230,13 +230,13 @@ const DynamicTreeItem = (props: DynamicTreeItemProps) => { hovered: isHovered })}> - {({isExpanded, hasChildRows, level, selectionBehavior, selectionMode}) => ( + {({isExpanded, hasChildItems, level, selectionBehavior, selectionMode}) => ( <> {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( )} -
- {hasChildRows && } +
+ {hasChildItems && } {props.children} @@ -429,13 +429,13 @@ const DynamicTreeItemWithButtonLoader = (props: DynamicTreeItemProps) => { hovered: isHovered })}> - {({isExpanded, hasChildRows, level, selectionBehavior, selectionMode}) => ( + {({isExpanded, hasChildItems, level, selectionBehavior, selectionMode}) => ( <> {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( )} -
- {hasChildRows && } +
+ {hasChildItems && } {props.children} diff --git a/packages/react-aria-components/test/Tree.test.tsx b/packages/react-aria-components/test/Tree.test.tsx index ebe938a635f..366782d3136 100644 --- a/packages/react-aria-components/test/Tree.test.tsx +++ b/packages/react-aria-components/test/Tree.test.tsx @@ -31,12 +31,12 @@ let StaticTreeItem = (props) => { return ( - {({isExpanded, hasChildRows, selectionMode, selectionBehavior}) => ( + {({isExpanded, hasChildItems, selectionMode, selectionBehavior}) => ( <> {(selectionMode !== 'none' || props.href != null) && selectionBehavior === 'toggle' && ( )} - {hasChildRows && } + {hasChildItems && } {props.title || props.children} @@ -101,12 +101,12 @@ let DynamicTreeItem = (props) => { return ( - {({isExpanded, hasChildRows, selectionMode, selectionBehavior}) => ( + {({isExpanded, hasChildItems, selectionMode, selectionBehavior}) => ( <> {(selectionMode !== 'none' || props.href != null) && selectionBehavior === 'toggle' && ( )} - {hasChildRows && } + {hasChildItems && } {props.title || props.children} @@ -217,7 +217,7 @@ describe('Tree', () => { expect(rowNoChild).not.toHaveAttribute('aria-expanded'); expect(rowNoChild).not.toHaveAttribute('data-expanded'); expect(rowNoChild).toHaveAttribute('data-level', '1'); - expect(rowNoChild).not.toHaveAttribute('data-has-child-rows'); + expect(rowNoChild).not.toHaveAttribute('data-has-child-items'); expect(rowNoChild).toHaveAttribute('data-rac'); let rowWithChildren = rows[1]; @@ -225,35 +225,35 @@ describe('Tree', () => { expect(rowWithChildren).toHaveAttribute('aria-label', 'Projects'); expect(rowWithChildren).toHaveAttribute('data-expanded', 'true'); expect(rowWithChildren).toHaveAttribute('data-level', '1'); - expect(rowWithChildren).toHaveAttribute('data-has-child-rows', 'true'); + expect(rowWithChildren).toHaveAttribute('data-has-child-items', 'true'); expect(rowWithChildren).toHaveAttribute('data-rac'); let level2ChildRow = rows[2]; expect(level2ChildRow).toHaveAttribute('aria-label', 'Projects-1'); expect(level2ChildRow).toHaveAttribute('data-expanded', 'true'); expect(level2ChildRow).toHaveAttribute('data-level', '2'); - expect(level2ChildRow).toHaveAttribute('data-has-child-rows', 'true'); + expect(level2ChildRow).toHaveAttribute('data-has-child-items', 'true'); expect(level2ChildRow).toHaveAttribute('data-rac'); let level3ChildRow = rows[3]; expect(level3ChildRow).toHaveAttribute('aria-label', 'Projects-1A'); expect(level3ChildRow).not.toHaveAttribute('data-expanded'); expect(level3ChildRow).toHaveAttribute('data-level', '3'); - expect(level3ChildRow).not.toHaveAttribute('data-has-child-rows'); + expect(level3ChildRow).not.toHaveAttribute('data-has-child-items'); expect(level3ChildRow).toHaveAttribute('data-rac'); let level2ChildRow2 = rows[4]; expect(level2ChildRow2).toHaveAttribute('aria-label', 'Projects-2'); expect(level2ChildRow2).not.toHaveAttribute('data-expanded'); expect(level2ChildRow2).toHaveAttribute('data-level', '2'); - expect(level2ChildRow2).not.toHaveAttribute('data-has-child-rows'); + expect(level2ChildRow2).not.toHaveAttribute('data-has-child-items'); expect(level2ChildRow2).toHaveAttribute('data-rac'); let level2ChildRow3 = rows[5]; expect(level2ChildRow3).toHaveAttribute('aria-label', 'Projects-3'); expect(level2ChildRow3).not.toHaveAttribute('data-expanded'); expect(level2ChildRow3).toHaveAttribute('data-level', '2'); - expect(level2ChildRow3).not.toHaveAttribute('data-has-child-rows'); + expect(level2ChildRow3).not.toHaveAttribute('data-has-child-items'); expect(level2ChildRow3).toHaveAttribute('data-rac'); }); @@ -261,7 +261,7 @@ describe('Tree', () => { let {getAllByRole} = render(); let rows = getAllByRole('row'); - expect(rows[1]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[1]).toHaveAttribute('data-has-child-items', 'true'); }); it('should support dynamic trees', () => { @@ -278,28 +278,28 @@ describe('Tree', () => { expect(rows[0]).toHaveAttribute('aria-level', '1'); expect(rows[0]).toHaveAttribute('aria-posinset', '1'); expect(rows[0]).toHaveAttribute('aria-setsize', '2'); - expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[0]).toHaveAttribute('data-has-child-items', 'true'); expect(rows[2]).toHaveAttribute('aria-label', 'Project 2'); expect(rows[2]).toHaveAttribute('aria-expanded', 'true'); expect(rows[2]).toHaveAttribute('aria-level', '2'); expect(rows[2]).toHaveAttribute('aria-posinset', '2'); expect(rows[2]).toHaveAttribute('aria-setsize', '5'); - expect(rows[2]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[2]).toHaveAttribute('data-has-child-items', 'true'); expect(rows[8]).toHaveAttribute('aria-label', 'Project 5'); expect(rows[8]).toHaveAttribute('aria-expanded', 'true'); expect(rows[8]).toHaveAttribute('aria-level', '2'); expect(rows[8]).toHaveAttribute('aria-posinset', '5'); expect(rows[8]).toHaveAttribute('aria-setsize', '5'); - expect(rows[8]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[8]).toHaveAttribute('data-has-child-items', 'true'); expect(rows[12]).toHaveAttribute('aria-label', 'Reports'); expect(rows[12]).toHaveAttribute('aria-expanded', 'true'); expect(rows[12]).toHaveAttribute('aria-level', '1'); expect(rows[12]).toHaveAttribute('aria-posinset', '2'); expect(rows[12]).toHaveAttribute('aria-setsize', '2'); - expect(rows[12]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[12]).toHaveAttribute('data-has-child-items', 'true'); expect(rows[16]).toHaveAttribute('aria-label', 'Reports 1ABC'); expect(rows[16]).toHaveAttribute('aria-level', '5'); @@ -843,14 +843,14 @@ describe('Tree', () => { await user.tab(); expect(document.activeElement).toBe(rows[0]); expect(rows[0]).toHaveAttribute('data-expanded', 'true'); - expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[0]).toHaveAttribute('data-has-child-items', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(0); // Check we can open/close a top level row await trigger(rows[0], 'Enter'); expect(document.activeElement).toBe(rows[0]); expect(rows[0]).not.toHaveAttribute('data-expanded'); - expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[0]).toHaveAttribute('data-has-child-items', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(1); // Note that the children of the parent row will still be in the "expanded" array expect(new Set(onExpandedChange.mock.calls[0][0])).toEqual(new Set(['project-2', 'project-5', 'reports', 'reports-1', 'reports-1A', 'reports-1AB'])); @@ -860,7 +860,7 @@ describe('Tree', () => { await trigger(rows[0], 'Enter'); expect(document.activeElement).toBe(rows[0]); expect(rows[0]).toHaveAttribute('data-expanded', 'true'); - expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[0]).toHaveAttribute('data-has-child-items', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(2); expect(new Set(onExpandedChange.mock.calls[1][0])).toEqual(new Set(['projects', 'project-2', 'project-5', 'reports', 'reports-1', 'reports-1A', 'reports-1AB'])); rows = getAllByRole('row'); @@ -870,15 +870,15 @@ describe('Tree', () => { await user.keyboard('{ArrowDown}'); expect(document.activeElement).toBe(rows[2]); expect(rows[2]).toHaveAttribute('data-expanded', 'true'); - expect(rows[2]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[2]).toHaveAttribute('data-has-child-items', 'true'); // Check we can close a nested row and it doesn't affect the parent await trigger(rows[2], 'ArrowLeft'); expect(document.activeElement).toBe(rows[2]); expect(rows[2]).not.toHaveAttribute('data-expanded'); - expect(rows[2]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[2]).toHaveAttribute('data-has-child-items', 'true'); expect(rows[0]).toHaveAttribute('data-expanded', 'true'); - expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); + expect(rows[0]).toHaveAttribute('data-has-child-items', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(3); expect(new Set(onExpandedChange.mock.calls[2][0])).toEqual(new Set(['projects', 'project-5', 'reports', 'reports-1', 'reports-1A', 'reports-1AB'])); rows = getAllByRole('row'); @@ -1297,12 +1297,12 @@ let ControlledDynamicTreeItem = (props) => { return ( - {({isExpanded, hasChildRows, selectionMode, selectionBehavior}) => ( + {({isExpanded, hasChildItems, selectionMode, selectionBehavior}) => ( <> {(selectionMode !== 'none' || props.href != null) && selectionBehavior === 'toggle' && ( )} - {hasChildRows && } + {hasChildItems && } {props.title || props.children}