Skip to content

Commit 54c44fe

Browse files
committed
fix logic to determine hasChildNodes
1 parent 04adce4 commit 54c44fe

File tree

11 files changed

+48
-53
lines changed

11 files changed

+48
-53
lines changed

packages/@react-aria/collections/src/Document.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,18 @@ export class ElementNode<T> extends BaseNode<T> {
257257
node.parentKey = this.parentNode instanceof ElementNode ? this.parentNode.node.key : null;
258258
node.prevKey = this.previousSibling?.node.key ?? null;
259259
node.nextKey = this.nextSibling?.node.key ?? null;
260-
node.hasChildNodes = !!this.firstChild;
260+
261+
// Check if this node has any child nodes, but specifically any that are items.
262+
let child = this.firstChild;
263+
let hasChildNodes = false;
264+
while (!hasChildNodes && child && child?.nextSibling) {
265+
child = child.nextSibling;
266+
if (child.node.type === 'item') {
267+
hasChildNodes = true;
268+
}
269+
}
270+
271+
node.hasChildNodes = hasChildNodes;
261272
node.firstChildKey = this.firstChild?.node.key ?? null;
262273
node.lastChildKey = this.lastChild?.node.key ?? null;
263274
}

packages/@react-aria/gridlist/src/useGridListItem.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ export interface AriaGridListItemOptions {
2828
/** Whether the list row is contained in a virtual scroller. */
2929
isVirtualized?: boolean,
3030
/** Whether selection should occur on press up instead of press down. */
31-
shouldSelectOnPressUp?: boolean
31+
shouldSelectOnPressUp?: boolean,
32+
/** Whether this item has children, even if not loaded yet. */
33+
hasChildItems?: boolean
3234
}
3335

3436
export interface GridListItemAria extends SelectableItemStates {
@@ -86,13 +88,9 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
8688
};
8789

8890
let treeGridRowProps: HTMLAttributes<HTMLElement> = {};
89-
let hasChildRows;
91+
let hasChildRows = props.hasChildItems || node.hasChildNodes;
9092
let hasLink = state.selectionManager.isLink(node.key);
9193
if (node != null && 'expandedKeys' in state) {
92-
// 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
93-
// true...
94-
let children = state.collection.getChildren?.(node.key);
95-
hasChildRows = [...(children ?? [])].length > 1;
9694
if (onAction == null && !hasLink && state.selectionManager.selectionMode === 'none' && hasChildRows) {
9795
onAction = () => state.toggleKey(node.key);
9896
}

packages/@react-spectrum/s2/src/TreeView.tsx

+8-15
Original file line numberDiff line numberDiff line change
@@ -294,24 +294,19 @@ const treeRowFocusIndicator = raw(`
294294
}`
295295
);
296296

297-
let TreeItemContext = createContext<{hasChildItems?: boolean}>({});
298-
299297
export const TreeViewItem = <T extends object>(props: TreeViewItemProps<T>) => {
300298
let {
301299
href
302300
} = props;
303301
let {isDetached, isEmphasized} = useContext(InternalTreeContext);
304302

305303
return (
306-
// TODO right now all the tree rows have the various data attributes applied on their dom nodes, should they? Doesn't feel very useful
307-
<TreeItemContext.Provider value={{hasChildItems: !!props.childItems}}>
308-
<UNSTABLE_TreeItem
309-
{...props}
310-
className={(renderProps) => treeRow({
311-
...renderProps,
312-
isLink: !!href, isEmphasized
313-
}) + (renderProps.isFocusVisible && !isDetached ? ' ' + treeRowFocusIndicator : '')} />
314-
</TreeItemContext.Provider>
304+
<UNSTABLE_TreeItem
305+
{...props}
306+
className={(renderProps) => treeRow({
307+
...renderProps,
308+
isLink: !!href, isEmphasized
309+
}) + (renderProps.isFocusVisible && !isDetached ? ' ' + treeRowFocusIndicator : '')} />
315310
);
316311
};
317312

@@ -320,7 +315,6 @@ export const TreeItemContent = (props: Omit<TreeItemContentProps, 'children'> &
320315
children
321316
} = props;
322317
let {isDetached, isEmphasized} = useContext(InternalTreeContext);
323-
let {hasChildItems} = useContext(TreeItemContext);
324318

325319
return (
326320
<UNSTABLE_TreeItemContent>
@@ -348,7 +342,7 @@ export const TreeItemContent = (props: Omit<TreeItemContentProps, 'children'> &
348342
width: '[calc(calc(var(--tree-item-level, 0) - 1) * var(--indent))]'
349343
})} />
350344
{/* 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 */}
351-
{(hasChildRows || hasChildItems) && <ExpandableRowChevron isDisabled={isDisabled} isExpanded={isExpanded} />}
345+
{hasChildRows && <ExpandableRowChevron isDisabled={isDisabled} isExpanded={isExpanded} />}
352346
<Provider
353347
values={[
354348
[TextContext, {styles: treeContent}],
@@ -357,8 +351,7 @@ export const TreeItemContent = (props: Omit<TreeItemContentProps, 'children'> &
357351
styles: style({size: fontRelative(20), flexShrink: 0})
358352
}],
359353
[ActionButtonGroupContext, {styles: treeActions}],
360-
[ActionMenuContext, {styles: treeActionMenu, isQuiet: true, 'aria-label': 'Actions'}],
361-
[TreeItemContext, {}]
354+
[ActionMenuContext, {styles: treeActionMenu, isQuiet: true, 'aria-label': 'Actions'}]
362355
]}>
363356
{children}
364357
</Provider>

packages/@react-spectrum/tree/src/TreeView.tsx

+9-19
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import ChevronLeftMedium from '@spectrum-icons/ui/ChevronLeftMedium';
1717
import ChevronRightMedium from '@spectrum-icons/ui/ChevronRightMedium';
1818
import {DOMRef, Expandable, Key, SelectionBehavior, SpectrumSelectionProps, StyleProps} from '@react-types/shared';
1919
import {isAndroid} from '@react-aria/utils';
20-
import React, {createContext, JSX, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react';
20+
import React, {createContext, JSX, JSXElementConstructor, ReactElement, ReactNode, useRef} from 'react';
2121
import {SlotProvider, useDOMRef, useStyleProps} from '@react-spectrum/utils';
2222
import {style} from '@react-spectrum/style-macro-s1' with {type: 'macro'};
2323
import {useButton} from '@react-aria/button';
@@ -40,8 +40,6 @@ export interface SpectrumTreeViewProps<T> extends Omit<AriaTreeGridListProps<T>,
4040
export interface SpectrumTreeViewItemProps<T extends object = object> extends Omit<TreeItemProps, 'className' | 'style' | 'value' | 'onHoverStart' | 'onHoverEnd' | 'onHoverChange'> {
4141
/** Rendered contents of the tree item or child items. */
4242
children: ReactNode,
43-
/** Whether this item has children, even if not loaded yet. */
44-
hasChildItems?: boolean,
4543
/** A list of child tree item objects used when dynamically rendering the tree item children. */
4644
childItems?: Iterable<T>
4745
}
@@ -219,23 +217,18 @@ const treeRowOutline = style({
219217
}
220218
});
221219

222-
let TreeItemContext = createContext<{hasChildItems?: boolean}>({});
223-
224220
export const TreeViewItem = <T extends object>(props: SpectrumTreeViewItemProps<T>) => {
225221
let {
226222
href
227223
} = props;
228224

229225
return (
230-
// TODO right now all the tree rows have the various data attributes applied on their dom nodes, should they? Doesn't feel very useful
231-
<TreeItemContext.Provider value={{hasChildItems: !!props.childItems || props.hasChildItems}}>
232-
<UNSTABLE_TreeItem
233-
{...props}
234-
className={renderProps => treeRow({
235-
...renderProps,
236-
isLink: !!href
237-
})} />
238-
</TreeItemContext.Provider>
226+
<UNSTABLE_TreeItem
227+
{...props}
228+
className={renderProps => treeRow({
229+
...renderProps,
230+
isLink: !!href
231+
})} />
239232
);
240233
};
241234

@@ -244,7 +237,6 @@ export const TreeItemContent = (props: Omit<TreeItemContentProps, 'children'> &
244237
let {
245238
children
246239
} = props;
247-
let {hasChildItems} = useContext(TreeItemContext);
248240

249241
return (
250242
<UNSTABLE_TreeItemContent>
@@ -260,7 +252,7 @@ export const TreeItemContent = (props: Omit<TreeItemContentProps, 'children'> &
260252
)}
261253
<div style={{gridArea: 'level-padding', marginInlineEnd: `calc(${level - 1} * var(--spectrum-global-dimension-size-200))`}} />
262254
{/* 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 */}
263-
{(hasChildRows || hasChildItems) && <ExpandableRowChevron isDisabled={isDisabled} isExpanded={isExpanded} />}
255+
{hasChildRows && <ExpandableRowChevron isDisabled={isDisabled} isExpanded={isExpanded} />}
264256
<SlotProvider
265257
slots={{
266258
text: {UNSAFE_className: treeContent({isDisabled})},
@@ -278,9 +270,7 @@ export const TreeItemContent = (props: Omit<TreeItemContentProps, 'children'> &
278270
},
279271
actionMenu: {UNSAFE_className: treeActionMenu(), UNSAFE_style: {marginInlineEnd: '.5rem'}, isQuiet: true}
280272
}}>
281-
<TreeItemContext.Provider value={{}}>
282-
{children}
283-
</TreeItemContext.Provider>
273+
{children}
284274
</SlotProvider>
285275
<div className={treeRowOutline({isFocusVisible, isSelected})} />
286276
</div>

packages/@react-spectrum/tree/test/TreeView.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ describe('Tree', () => {
480480
expect(rows[0]).toHaveAttribute('aria-label', 'Test');
481481
// Until the row gets children, don't mark it with the aria/data attributes.
482482
expect(rows[0]).not.toHaveAttribute('aria-expanded');
483-
expect(rows[0]).not.toHaveAttribute('data-has-child-rows');
483+
expect(rows[0]).toHaveAttribute('data-has-child-rows');
484484
expect(chevron).toBeTruthy();
485485
});
486486

packages/@react-stately/grid/src/useGridState.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,10 @@ export function useGridState<T extends object, C extends GridCollection<T>>(prop
9797
}
9898
}
9999
if (newRow) {
100-
const childNodes = newRow.hasChildNodes ? [...getChildNodes(newRow, collection)] : [];
100+
let hasChildNodes = newRow.firstChildKey === undefined ? newRow.hasChildNodes : newRow.firstChildKey != null;
101+
const childNodes = hasChildNodes ? [...getChildNodes(newRow, collection)] : [];
101102
const keyToFocus =
102-
newRow.hasChildNodes &&
103+
hasChildNodes &&
103104
parentNode !== node &&
104105
node &&
105106
node.index < childNodes.length ?

packages/@react-stately/selection/src/SelectionManager.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export class SelectionManager implements MultipleSelectionManager {
398398
}
399399

400400
// Add child keys. If cell selection is allowed, then include item children too.
401-
if (item?.hasChildNodes && (this.allowsCellSelection || item.type !== 'item')) {
401+
if (item && (item?.firstChildKey === undefined ? item?.hasChildNodes : item?.firstChildKey != null) && (this.allowsCellSelection || item.type !== 'item')) {
402402
addKeys(getFirstItem(getChildNodes(item, this.collection))?.key ?? null);
403403
}
404404
}

packages/@react-types/shared/src/collections.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ export interface Node<T> {
215215
prevKey?: Key | null,
216216
/** The key of the node after this node. */
217217
nextKey?: Key | null,
218+
/** The key of the first child node. */
219+
firstChildKey?: Key | null,
220+
/** The key of the last child node. */
221+
lastChildKey?: Key | null,
218222
/** Additional properties specific to a particular node type. */
219223
props?: any,
220224
/** @private */

packages/react-aria-components/src/Table.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class TableCollection<T> extends BaseCollection<T> implements ITableCollection<T
5858
switch (node.type) {
5959
case 'column':
6060
columnKeyMap.set(node.key, node);
61-
if (!node.hasChildNodes) {
61+
if (node.firstChildKey == null) {
6262
node.index = this.columns.length;
6363
this.columns.push(node);
6464

packages/react-aria-components/src/Tree.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {AriaTreeGridListProps, useTreeGridList, useTreeGridListItem} from '@react-aria/tree';
13+
import {AriaTreeGridListItemOptions, AriaTreeGridListProps, useTreeGridList, useTreeGridListItem} from '@react-aria/tree';
1414
import {ButtonContext} from './Button';
1515
import {CheckboxContext} from './RSPContexts';
1616
import {Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent, useCachedChildren} from '@react-aria/collections';
@@ -302,7 +302,7 @@ export const UNSTABLE_TreeItemContent = /*#__PURE__*/ createLeafComponent('conte
302302

303303
export const TreeItemContentContext = createContext<TreeItemContentRenderProps | null>(null);
304304

305-
export interface TreeItemProps<T = object> extends StyleRenderProps<TreeItemRenderProps>, LinkDOMProps, HoverEvents {
305+
export interface TreeItemProps<T = object> extends StyleRenderProps<TreeItemRenderProps>, LinkDOMProps, HoverEvents, Pick<AriaTreeGridListItemOptions, 'hasChildItems'> {
306306
/** The unique id of the tree row. */
307307
id?: Key,
308308
/** 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', <T
325325
// eslint-disable-next-line @typescript-eslint/no-unused-vars
326326
let {rowProps, gridCellProps, expandButtonProps, descriptionProps, ...states} = useTreeGridListItem({node: item}, state, ref);
327327
let isExpanded = rowProps['aria-expanded'] === true;
328-
let hasChildRows = [...state.collection.getChildren!(item.key)]?.length > 1;
328+
let hasChildRows = props.hasChildItems || item.hasChildNodes;
329329
let level = rowProps['aria-level'] || 1;
330330

331331
let {hoverProps, isHovered} = useHover({

packages/react-aria-components/test/Table.test.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -668,10 +668,8 @@ describe('Table', () => {
668668

669669
let tableTester = testUtilUser.createTester('Table', {root: getByRole('grid')});
670670
await user.tab();
671-
fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'});
672-
fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'});
673-
fireEvent.keyDown(document.activeElement, {key: 'ArrowRight'});
674-
fireEvent.keyUp(document.activeElement, {key: 'ArrowRight'});
671+
await user.keyboard('{ArrowDown}');
672+
await user.keyboard('{ArrowRight}');
675673

676674
let gridRows = tableTester.rows;
677675
expect(gridRows).toHaveLength(4);

0 commit comments

Comments
 (0)