Skip to content

Commit f70849d

Browse files
committed
fix(react-tree-grid): handle wrapped rows in navigation
1 parent 62a90f7 commit f70849d

4 files changed

Lines changed: 132 additions & 61 deletions

File tree

packages/react-tree-grid/src/components/TreeGrid/__tests__/fixtures/ThreadedVirtualizedNavigation.fixture.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,24 +63,28 @@ const ThreadedVirtualizedRow = ({
6363

6464
if (item.type === 'header') {
6565
return (
66-
<TreeGridRow data-item-id={item.value} open style={style} subtree>
67-
<TreeGridCell header>{item.value}</TreeGridCell>
68-
<TreeGridCell>Header</TreeGridCell>
69-
</TreeGridRow>
66+
<div>
67+
<TreeGridRow data-item-id={item.value} open style={style} subtree>
68+
<TreeGridCell header>{item.value}</TreeGridCell>
69+
<TreeGridCell>Header</TreeGridCell>
70+
</TreeGridRow>
71+
</div>
7072
);
7173
}
7274

7375
return (
74-
<TreeGridRow
75-
data-item-id={item.value}
76-
data-item-parent-id={item.parentValue}
77-
data-rowtype={item.type}
78-
level={2}
79-
style={style}
80-
>
81-
<TreeGridCell header>{item.value}</TreeGridCell>
82-
<TreeGridCell>{item.type}</TreeGridCell>
83-
</TreeGridRow>
76+
<div>
77+
<TreeGridRow
78+
data-item-id={item.value}
79+
data-item-parent-id={item.parentValue}
80+
data-rowtype={item.type}
81+
level={2}
82+
style={style}
83+
>
84+
<TreeGridCell header>{item.value}</TreeGridCell>
85+
<TreeGridCell>{item.type}</TreeGridCell>
86+
</TreeGridRow>
87+
</div>
8488
);
8589
};
8690

packages/react-tree-grid/src/components/TreeGrid/__tests__/fixtures/VirtualizedMeetingsNavigation.fixture.tsx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,30 +58,34 @@ const VirtualizedMeetingsRow = ({
5858

5959
if (item.type === 'section') {
6060
return (
61+
<div>
62+
<TreeGridRow
63+
data-item-id={item.value}
64+
open={isOpen}
65+
style={style}
66+
subtree
67+
>
68+
<TreeGridCell header>{item.value}</TreeGridCell>
69+
<TreeGridCell>Section</TreeGridCell>
70+
</TreeGridRow>
71+
</div>
72+
);
73+
}
74+
75+
return (
76+
<div>
6177
<TreeGridRow
6278
data-item-id={item.value}
63-
open={isOpen}
79+
data-item-parent-id={item.parentValue}
80+
level={2}
6481
style={style}
65-
subtree
6682
>
67-
<TreeGridCell header>{item.value}</TreeGridCell>
68-
<TreeGridCell>Section</TreeGridCell>
83+
<TreeGridCell header>
84+
<Button>{item.value}</Button>
85+
</TreeGridCell>
86+
<TreeGridCell>Meeting</TreeGridCell>
6987
</TreeGridRow>
70-
);
71-
}
72-
73-
return (
74-
<TreeGridRow
75-
data-item-id={item.value}
76-
data-item-parent-id={item.parentValue}
77-
level={2}
78-
style={style}
79-
>
80-
<TreeGridCell header>
81-
<Button>{item.value}</Button>
82-
</TreeGridCell>
83-
<TreeGridCell>Meeting</TreeGridCell>
84-
</TreeGridRow>
88+
</div>
8589
);
8690
};
8791

packages/react-tree-grid/src/hooks/useTreeGridLevelNavigation.test.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,31 @@ const createRow = (level: number): HTMLDivElement => {
1414
return row;
1515
};
1616

17+
const createWrappedRow = (
18+
level: number,
19+
treeGrid: HTMLElement
20+
): HTMLDivElement => {
21+
const wrapper = document.createElement('div');
22+
const row = document.createElement('div');
23+
24+
row.setAttribute('role', 'row');
25+
row.setAttribute('aria-level', String(level));
26+
row.tabIndex = 0;
27+
row.scrollIntoView = jest.fn();
28+
29+
wrapper.appendChild(row);
30+
treeGrid.appendChild(wrapper);
31+
32+
return row;
33+
};
34+
35+
const createTreeGrid = (): HTMLDivElement => {
36+
const treeGrid = document.createElement('div');
37+
treeGrid.setAttribute('role', 'treegrid');
38+
document.body.appendChild(treeGrid);
39+
return treeGrid;
40+
};
41+
1742
const createKeyboardEvent = (
1843
key: string,
1944
target: HTMLElement
@@ -62,9 +87,10 @@ describe('useTreeGridLevelNavigation', () => {
6287

6388
it('focuses the next row at the same level', () => {
6489
const { result } = renderHook(() => useTreeGridLevelNavigation());
65-
const currentRow = createRow(2);
66-
createRow(3);
67-
const nextRow = createRow(2);
90+
const treeGrid = createTreeGrid();
91+
const currentRow = createWrappedRow(2, treeGrid);
92+
createWrappedRow(3, treeGrid);
93+
const nextRow = createWrappedRow(2, treeGrid);
6894
const nativeEvent = createKeyboardEvent(ArrowDown, currentRow);
6995
const keyboardEvent = createReactKeyboardEvent(nativeEvent);
7096

@@ -77,8 +103,9 @@ describe('useTreeGridLevelNavigation', () => {
77103

78104
it('allows shallower-boundary navigation to fall through', () => {
79105
const { result } = renderHook(() => useTreeGridLevelNavigation());
80-
const parentRow = createRow(1);
81-
const currentRow = createRow(2);
106+
const treeGrid = createTreeGrid();
107+
const parentRow = createWrappedRow(1, treeGrid);
108+
const currentRow = createWrappedRow(2, treeGrid);
82109
const nativeEvent = createKeyboardEvent(ArrowUp, currentRow);
83110
const keyboardEvent = createReactKeyboardEvent(nativeEvent);
84111
const moveFocusEvent = createMoveFocusEvent(nativeEvent, parentRow);
@@ -89,4 +116,19 @@ describe('useTreeGridLevelNavigation', () => {
89116
expect(moveFocusEvent.defaultPrevented).toBe(false);
90117
expect(keyboardEvent.preventDefault).not.toHaveBeenCalled();
91118
});
119+
120+
it('skips wrapper elements when finding adjacent rows', () => {
121+
const { result } = renderHook(() => useTreeGridLevelNavigation());
122+
const treeGrid = createTreeGrid();
123+
const currentRow = createWrappedRow(1, treeGrid);
124+
const nextRow = createWrappedRow(1, treeGrid);
125+
const nativeEvent = createKeyboardEvent(ArrowDown, currentRow);
126+
const keyboardEvent = createReactKeyboardEvent(nativeEvent);
127+
128+
result.current.onKeyDown(keyboardEvent);
129+
130+
expect(keyboardEvent.preventDefault).toHaveBeenCalledTimes(1);
131+
expect(nextRow.scrollIntoView).toHaveBeenCalledWith({ block: 'nearest' });
132+
expect(nextRow).toBe(document.activeElement);
133+
});
92134
});

packages/react-tree-grid/src/hooks/useTreeGridLevelNavigation.ts

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,53 @@ const findAdjacentRowAtLevel = (
3131
return { type: 'none' };
3232
}
3333

34-
let sibling: Element | null =
35-
direction === ArrowDown
36-
? row.nextElementSibling
37-
: row.previousElementSibling;
38-
39-
while (sibling) {
40-
if (isHTMLElement(sibling) && sibling.getAttribute('role') === 'row') {
41-
const siblingLevel = Number(sibling.getAttribute('aria-level'));
42-
43-
if (Number.isNaN(siblingLevel)) {
44-
return { type: 'none' };
45-
}
46-
47-
if (siblingLevel < level) {
48-
return { type: 'shallower-boundary' };
49-
}
50-
51-
if (siblingLevel === level) {
52-
return { type: 'same-level', row: sibling };
53-
}
34+
const treeGrid = row.closest<HTMLElement>('[role="treegrid"]');
35+
if (!treeGrid) {
36+
return { type: 'none' };
37+
}
38+
39+
const nodeFilter = row.ownerDocument.defaultView?.NodeFilter;
40+
if (!nodeFilter) {
41+
return { type: 'none' };
42+
}
43+
44+
const rowWalker = row.ownerDocument.createTreeWalker(
45+
treeGrid,
46+
nodeFilter.SHOW_ELEMENT,
47+
{
48+
acceptNode: (node) =>
49+
isHTMLElement(node) && node.getAttribute('role') === 'row'
50+
? nodeFilter.FILTER_ACCEPT
51+
: nodeFilter.FILTER_SKIP,
5452
}
53+
);
54+
55+
rowWalker.currentNode = row;
5556

57+
for (
58+
let sibling =
59+
direction === ArrowDown ? rowWalker.nextNode() : rowWalker.previousNode();
60+
sibling;
5661
sibling =
57-
direction === ArrowDown
58-
? sibling.nextElementSibling
59-
: sibling.previousElementSibling;
62+
direction === ArrowDown ? rowWalker.nextNode() : rowWalker.previousNode()
63+
) {
64+
if (!isHTMLElement(sibling)) {
65+
continue;
66+
}
67+
68+
const siblingLevel = Number(sibling.getAttribute('aria-level'));
69+
70+
if (Number.isNaN(siblingLevel)) {
71+
return { type: 'none' };
72+
}
73+
74+
if (siblingLevel < level) {
75+
return { type: 'shallower-boundary' };
76+
}
77+
78+
if (siblingLevel === level) {
79+
return { type: 'same-level', row: sibling };
80+
}
6081
}
6182

6283
return { type: 'none' };

0 commit comments

Comments
 (0)