-
Notifications
You must be signed in to change notification settings - Fork 60
Expand file tree
/
Copy pathuseTreeGridLevelNavigation.ts
More file actions
124 lines (103 loc) · 3.19 KB
/
Copy pathuseTreeGridLevelNavigation.ts
File metadata and controls
124 lines (103 loc) · 3.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import { isHTMLElement } from '@fluentui/react-utilities';
import {
type TreeGridNavigationOverrideProps,
useTreeGridNavigationOverride,
} from './useTreeGridNavigationOverrides';
import * as React from 'react';
import { ArrowDown, ArrowUp } from '@fluentui/keyboard-keys';
import { TreeGridTabsterMoveFocusEventDetail } from '../components/TreeGrid/TreeGrid.types';
type AdjacentRowAtLevelResult =
| { type: 'same-level'; row: HTMLElement }
| { type: 'shallower-boundary' }
| { type: 'none' };
/**
* Keeps vertical arrow navigation at the current row level until TreeGrid
* reaches a shallower boundary, where default TreeGrid navigation resumes.
*/
export const useTreeGridLevelNavigation = (): TreeGridNavigationOverrideProps =>
useTreeGridNavigationOverride({
focusPrevious: { shouldOverride, onKeyDown },
focusNext: { shouldOverride, onKeyDown },
});
const findAdjacentRowAtLevel = (
row: HTMLElement,
direction: Direction
): AdjacentRowAtLevelResult => {
const level = Number(row.getAttribute('aria-level'));
if (Number.isNaN(level)) {
return { type: 'none' };
}
const treeGrid = row.closest<HTMLElement>('[role="treegrid"]');
if (!treeGrid) {
return { type: 'none' };
}
const nodeFilter = row.ownerDocument.defaultView?.NodeFilter;
if (!nodeFilter) {
return { type: 'none' };
}
const rowWalker = row.ownerDocument.createTreeWalker(
treeGrid,
nodeFilter.SHOW_ELEMENT,
{
acceptNode: (node) =>
isHTMLElement(node) && node.getAttribute('role') === 'row'
? nodeFilter.FILTER_ACCEPT
: nodeFilter.FILTER_SKIP,
}
);
rowWalker.currentNode = row;
for (
let sibling =
direction === ArrowDown ? rowWalker.nextNode() : rowWalker.previousNode();
sibling;
sibling =
direction === ArrowDown ? rowWalker.nextNode() : rowWalker.previousNode()
) {
if (!isHTMLElement(sibling)) {
continue;
}
const siblingLevel = Number(sibling.getAttribute('aria-level'));
if (Number.isNaN(siblingLevel)) {
return { type: 'none' };
}
if (siblingLevel < level) {
return { type: 'shallower-boundary' };
}
if (siblingLevel === level) {
return { type: 'same-level', row: sibling };
}
}
return { type: 'none' };
};
type Direction = typeof ArrowUp | typeof ArrowDown;
const getAdjacentRowAtLevel = (
event: KeyboardEvent
): AdjacentRowAtLevelResult => {
if (!isHTMLElement(event.target)) {
return { type: 'none' };
}
const row = event.target.closest<HTMLElement>('[role="row"]');
if (!row) {
return { type: 'none' };
}
return findAdjacentRowAtLevel(row, event.key as Direction);
};
const shouldOverride = (
event: CustomEvent<TreeGridTabsterMoveFocusEventDetail>
): boolean => {
return (
getAdjacentRowAtLevel(event.detail.relatedEvent).type !==
'shallower-boundary'
);
};
const onKeyDown = (event: React.KeyboardEvent<HTMLElement>): void => {
const adjacentRow = getAdjacentRowAtLevel(event.nativeEvent);
if (adjacentRow.type === 'shallower-boundary') {
return;
}
event.preventDefault();
if (adjacentRow.type === 'same-level') {
adjacentRow.row.scrollIntoView({ block: 'nearest' });
adjacentRow.row.focus();
}
};