@mui/x-tree-view 7.0.0-beta.7 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +195 -12
- package/README.md +1 -1
- package/RichTreeView/RichTreeView.js +12 -14
- package/RichTreeView/RichTreeView.types.d.ts +1 -1
- package/SimpleTreeView/SimpleTreeView.js +3 -4
- package/TreeItem/TreeItem.js +43 -35
- package/TreeItem/TreeItem.types.d.ts +3 -3
- package/TreeItem/TreeItemContent.d.ts +7 -7
- package/TreeItem/TreeItemContent.js +10 -10
- package/TreeItem/useTreeItemState.d.ts +1 -1
- package/TreeItem/useTreeItemState.js +13 -13
- package/TreeItem2/TreeItem2.js +16 -17
- package/TreeItem2Icon/TreeItem2Icon.js +5 -6
- package/TreeItem2Provider/TreeItem2Provider.js +3 -3
- package/TreeItem2Provider/TreeItem2Provider.types.d.ts +1 -1
- package/TreeView/TreeView.d.ts +1 -1
- package/TreeView/TreeView.js +1 -1
- package/hooks/useTreeItem2Utils/useTreeItem2Utils.d.ts +2 -2
- package/hooks/useTreeItem2Utils/useTreeItem2Utils.js +12 -12
- package/index.js +1 -1
- package/internals/hooks/useInstanceEventHandler.js +5 -10
- package/internals/models/plugin.d.ts +1 -1
- package/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.js +11 -18
- package/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.d.ts +3 -3
- package/internals/plugins/useTreeViewFocus/useTreeViewFocus.js +59 -43
- package/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.d.ts +6 -5
- package/internals/plugins/useTreeViewId/useTreeViewId.js +1 -1
- package/internals/plugins/useTreeViewId/useTreeViewId.types.d.ts +1 -1
- package/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.js +17 -18
- package/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.types.d.ts +2 -2
- package/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.js +70 -77
- package/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.d.ts +4 -1
- package/internals/plugins/useTreeViewNodes/useTreeViewNodes.js +24 -29
- package/internals/plugins/useTreeViewNodes/useTreeViewNodes.types.d.ts +11 -11
- package/internals/plugins/useTreeViewSelection/useTreeViewSelection.js +18 -21
- package/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.d.ts +4 -4
- package/internals/useTreeView/useTreeView.js +5 -6
- package/internals/useTreeView/useTreeView.utils.d.ts +2 -2
- package/internals/useTreeView/useTreeView.utils.js +22 -22
- package/internals/utils/extractPluginParamsFromProps.js +2 -2
- package/internals/utils/utils.js +1 -0
- package/modern/RichTreeView/RichTreeView.js +7 -7
- package/modern/SimpleTreeView/SimpleTreeView.js +1 -1
- package/modern/TreeItem/TreeItem.js +31 -22
- package/modern/TreeItem/TreeItemContent.js +10 -10
- package/modern/TreeItem/useTreeItemState.js +13 -13
- package/modern/TreeItem2/TreeItem2.js +11 -11
- package/modern/TreeItem2Provider/TreeItem2Provider.js +3 -3
- package/modern/TreeView/TreeView.js +1 -1
- package/modern/hooks/useTreeItem2Utils/useTreeItem2Utils.js +12 -12
- package/modern/index.js +1 -1
- package/modern/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.js +7 -7
- package/modern/internals/plugins/useTreeViewFocus/useTreeViewFocus.js +57 -38
- package/modern/internals/plugins/useTreeViewId/useTreeViewId.js +1 -1
- package/modern/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.js +17 -17
- package/modern/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.js +69 -74
- package/modern/internals/plugins/useTreeViewNodes/useTreeViewNodes.js +19 -20
- package/modern/internals/plugins/useTreeViewSelection/useTreeViewSelection.js +13 -13
- package/modern/internals/useTreeView/useTreeView.js +3 -4
- package/modern/internals/useTreeView/useTreeView.utils.js +22 -22
- package/modern/internals/utils/utils.js +1 -0
- package/modern/useTreeItem2/useTreeItem2.js +23 -12
- package/node/RichTreeView/RichTreeView.js +7 -7
- package/node/SimpleTreeView/SimpleTreeView.js +1 -1
- package/node/TreeItem/TreeItem.js +31 -22
- package/node/TreeItem/TreeItemContent.js +10 -10
- package/node/TreeItem/useTreeItemState.js +13 -13
- package/node/TreeItem2/TreeItem2.js +11 -11
- package/node/TreeItem2Provider/TreeItem2Provider.js +3 -3
- package/node/TreeView/TreeView.js +1 -1
- package/node/hooks/useTreeItem2Utils/useTreeItem2Utils.js +12 -12
- package/node/index.js +1 -1
- package/node/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.js +7 -7
- package/node/internals/plugins/useTreeViewFocus/useTreeViewFocus.js +57 -38
- package/node/internals/plugins/useTreeViewId/useTreeViewId.js +1 -1
- package/node/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.js +17 -17
- package/node/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.js +69 -74
- package/node/internals/plugins/useTreeViewNodes/useTreeViewNodes.js +19 -20
- package/node/internals/plugins/useTreeViewSelection/useTreeViewSelection.js +13 -13
- package/node/internals/useTreeView/useTreeView.js +3 -4
- package/node/internals/useTreeView/useTreeView.utils.js +22 -22
- package/node/internals/utils/utils.js +1 -0
- package/node/useTreeItem2/useTreeItem2.js +23 -12
- package/package.json +5 -5
- package/useTreeItem2/useTreeItem2.js +26 -18
- package/useTreeItem2/useTreeItem2.types.d.ts +9 -7
|
@@ -5,6 +5,22 @@ import ownerDocument from '@mui/utils/ownerDocument';
|
|
|
5
5
|
import { populateInstance, populatePublicAPI } from '../../useTreeView/useTreeView.utils';
|
|
6
6
|
import { useInstanceEventHandler } from '../../hooks/useInstanceEventHandler';
|
|
7
7
|
import { getActiveElement } from '../../utils/utils';
|
|
8
|
+
const useTabbableItemId = (instance, selectedItems) => {
|
|
9
|
+
const isItemVisible = itemId => {
|
|
10
|
+
const node = instance.getNode(itemId);
|
|
11
|
+
return node && (node.parentId == null || instance.isNodeExpanded(node.parentId));
|
|
12
|
+
};
|
|
13
|
+
let tabbableItemId;
|
|
14
|
+
if (Array.isArray(selectedItems)) {
|
|
15
|
+
tabbableItemId = selectedItems.find(isItemVisible);
|
|
16
|
+
} else if (selectedItems != null && isItemVisible(selectedItems)) {
|
|
17
|
+
tabbableItemId = selectedItems;
|
|
18
|
+
}
|
|
19
|
+
if (tabbableItemId == null) {
|
|
20
|
+
tabbableItemId = instance.getNavigableChildrenIds(null)[0];
|
|
21
|
+
}
|
|
22
|
+
return tabbableItemId;
|
|
23
|
+
};
|
|
8
24
|
export const useTreeViewFocus = ({
|
|
9
25
|
instance,
|
|
10
26
|
publicAPI,
|
|
@@ -14,30 +30,36 @@ export const useTreeViewFocus = ({
|
|
|
14
30
|
models,
|
|
15
31
|
rootRef
|
|
16
32
|
}) => {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
const tabbableItemId = useTabbableItemId(instance, models.selectedItems.value);
|
|
34
|
+
const setFocusedItemId = useEventCallback(itemId => {
|
|
35
|
+
const cleanItemId = typeof itemId === 'function' ? itemId(state.focusedNodeId) : itemId;
|
|
36
|
+
if (state.focusedNodeId !== cleanItemId) {
|
|
20
37
|
setState(prevState => _extends({}, prevState, {
|
|
21
|
-
focusedNodeId:
|
|
38
|
+
focusedNodeId: cleanItemId
|
|
22
39
|
}));
|
|
23
40
|
}
|
|
24
41
|
});
|
|
25
|
-
const isTreeViewFocused = React.useCallback(() => !!rootRef.current && rootRef.current
|
|
26
|
-
const isNodeFocused = React.useCallback(
|
|
27
|
-
const isNodeVisible =
|
|
28
|
-
const node = instance.getNode(
|
|
42
|
+
const isTreeViewFocused = React.useCallback(() => !!rootRef.current && rootRef.current.contains(getActiveElement(ownerDocument(rootRef.current))), [rootRef]);
|
|
43
|
+
const isNodeFocused = React.useCallback(itemId => state.focusedNodeId === itemId && isTreeViewFocused(), [state.focusedNodeId, isTreeViewFocused]);
|
|
44
|
+
const isNodeVisible = itemId => {
|
|
45
|
+
const node = instance.getNode(itemId);
|
|
29
46
|
return node && (node.parentId == null || instance.isNodeExpanded(node.parentId));
|
|
30
47
|
};
|
|
48
|
+
const innerFocusItem = (event, itemId) => {
|
|
49
|
+
const node = instance.getNode(itemId);
|
|
50
|
+
const itemElement = document.getElementById(instance.getTreeItemId(itemId, node.idAttribute));
|
|
51
|
+
if (itemElement) {
|
|
52
|
+
itemElement.focus();
|
|
53
|
+
}
|
|
54
|
+
setFocusedItemId(itemId);
|
|
55
|
+
if (params.onItemFocus) {
|
|
56
|
+
params.onItemFocus(event, itemId);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
31
59
|
const focusItem = useEventCallback((event, nodeId) => {
|
|
32
|
-
//
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
instance.focusRoot();
|
|
36
|
-
}
|
|
37
|
-
setFocusedNodeId(nodeId);
|
|
38
|
-
if (params.onItemFocus) {
|
|
39
|
-
params.onItemFocus(event, nodeId);
|
|
40
|
-
}
|
|
60
|
+
// If we receive a nodeId, and it is visible, the focus will be set to it
|
|
61
|
+
if (isNodeVisible(nodeId)) {
|
|
62
|
+
innerFocusItem(event, nodeId);
|
|
41
63
|
}
|
|
42
64
|
});
|
|
43
65
|
const focusDefaultNode = useEventCallback(event => {
|
|
@@ -50,22 +72,26 @@ export const useTreeViewFocus = ({
|
|
|
50
72
|
if (nodeToFocusId == null) {
|
|
51
73
|
nodeToFocusId = instance.getNavigableChildrenIds(null)[0];
|
|
52
74
|
}
|
|
53
|
-
|
|
54
|
-
if (params.onItemFocus) {
|
|
55
|
-
params.onItemFocus(event, nodeToFocusId);
|
|
56
|
-
}
|
|
75
|
+
innerFocusItem(event, nodeToFocusId);
|
|
57
76
|
});
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
77
|
+
const removeFocusedItem = useEventCallback(() => {
|
|
78
|
+
if (state.focusedNodeId == null) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const node = instance.getNode(state.focusedNodeId);
|
|
82
|
+
const itemElement = document.getElementById(instance.getTreeItemId(state.focusedNodeId, node.idAttribute));
|
|
83
|
+
if (itemElement) {
|
|
84
|
+
itemElement.blur();
|
|
85
|
+
}
|
|
86
|
+
setFocusedItemId(null);
|
|
63
87
|
});
|
|
88
|
+
const canItemBeTabbed = itemId => itemId === tabbableItemId;
|
|
64
89
|
populateInstance(instance, {
|
|
65
90
|
isNodeFocused,
|
|
91
|
+
canItemBeTabbed,
|
|
66
92
|
focusItem,
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
focusDefaultNode,
|
|
94
|
+
removeFocusedItem
|
|
69
95
|
});
|
|
70
96
|
populatePublicAPI(publicAPI, {
|
|
71
97
|
focusItem
|
|
@@ -73,33 +99,23 @@ export const useTreeViewFocus = ({
|
|
|
73
99
|
useInstanceEventHandler(instance, 'removeNode', ({
|
|
74
100
|
id
|
|
75
101
|
}) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
return oldFocusedNodeId;
|
|
81
|
-
});
|
|
102
|
+
if (state.focusedNodeId === id) {
|
|
103
|
+
instance.focusDefaultNode(null);
|
|
104
|
+
}
|
|
82
105
|
});
|
|
83
106
|
const createHandleFocus = otherHandlers => event => {
|
|
84
|
-
|
|
85
|
-
(_otherHandlers$onFocu = otherHandlers.onFocus) == null || _otherHandlers$onFocu.call(otherHandlers, event);
|
|
107
|
+
otherHandlers.onFocus?.(event);
|
|
86
108
|
// if the event bubbled (which is React specific) we don't want to steal focus
|
|
87
109
|
if (event.target === event.currentTarget) {
|
|
88
110
|
instance.focusDefaultNode(event);
|
|
89
111
|
}
|
|
90
112
|
};
|
|
91
|
-
const createHandleBlur = otherHandlers => event => {
|
|
92
|
-
var _otherHandlers$onBlur;
|
|
93
|
-
(_otherHandlers$onBlur = otherHandlers.onBlur) == null || _otherHandlers$onBlur.call(otherHandlers, event);
|
|
94
|
-
setFocusedNodeId(null);
|
|
95
|
-
};
|
|
96
113
|
const focusedNode = instance.getNode(state.focusedNodeId);
|
|
97
114
|
const activeDescendant = focusedNode ? instance.getTreeItemId(focusedNode.id, focusedNode.idAttribute) : null;
|
|
98
115
|
return {
|
|
99
116
|
getRootProps: otherHandlers => ({
|
|
100
117
|
onFocus: createHandleFocus(otherHandlers),
|
|
101
|
-
|
|
102
|
-
'aria-activedescendant': activeDescendant != null ? activeDescendant : undefined
|
|
118
|
+
'aria-activedescendant': activeDescendant ?? undefined
|
|
103
119
|
})
|
|
104
120
|
};
|
|
105
121
|
};
|
|
@@ -5,10 +5,11 @@ import type { UseTreeViewNodesSignature } from '../useTreeViewNodes';
|
|
|
5
5
|
import type { UseTreeViewSelectionSignature } from '../useTreeViewSelection';
|
|
6
6
|
import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion';
|
|
7
7
|
export interface UseTreeViewFocusInstance {
|
|
8
|
-
isNodeFocused: (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
isNodeFocused: (itemId: string) => boolean;
|
|
9
|
+
canItemBeTabbed: (itemId: string) => boolean;
|
|
10
|
+
focusItem: (event: React.SyntheticEvent, nodeId: string) => void;
|
|
11
|
+
focusDefaultNode: (event: React.SyntheticEvent | null) => void;
|
|
12
|
+
removeFocusedItem: () => void;
|
|
12
13
|
}
|
|
13
14
|
export interface UseTreeViewFocusPublicAPI extends Pick<UseTreeViewFocusInstance, 'focusItem'> {
|
|
14
15
|
}
|
|
@@ -19,7 +20,7 @@ export interface UseTreeViewFocusParameters {
|
|
|
19
20
|
* @param {string} itemId The id of the focused item.
|
|
20
21
|
* @param {string} value of the focused item.
|
|
21
22
|
*/
|
|
22
|
-
onItemFocus?: (event: React.SyntheticEvent, itemId: string) => void;
|
|
23
|
+
onItemFocus?: (event: React.SyntheticEvent | null, itemId: string) => void;
|
|
23
24
|
}
|
|
24
25
|
export type UseTreeViewFocusDefaultizedParameters = UseTreeViewFocusParameters;
|
|
25
26
|
export interface UseTreeViewFocusState {
|
|
@@ -6,7 +6,7 @@ export const useTreeViewId = ({
|
|
|
6
6
|
params
|
|
7
7
|
}) => {
|
|
8
8
|
const treeId = useId(params.id);
|
|
9
|
-
const getTreeItemId = React.useCallback((
|
|
9
|
+
const getTreeItemId = React.useCallback((itemId, idAttribute) => idAttribute ?? `${treeId}-${itemId}`, [treeId]);
|
|
10
10
|
populateInstance(instance, {
|
|
11
11
|
getTreeItemId
|
|
12
12
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TreeViewPluginSignature } from '../../models';
|
|
2
2
|
export interface UseTreeViewIdInstance {
|
|
3
|
-
getTreeItemId: (
|
|
3
|
+
getTreeItemId: (itemId: string, idAttribute: string | undefined) => string;
|
|
4
4
|
}
|
|
5
5
|
export interface UseTreeViewIdParameters {
|
|
6
6
|
/**
|
|
@@ -32,12 +32,12 @@ export const useTreeViewJSXNodes = ({
|
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
|
-
const removeJSXNode = useEventCallback(
|
|
35
|
+
const removeJSXNode = useEventCallback(itemId => {
|
|
36
36
|
setState(prevState => {
|
|
37
37
|
const newNodeMap = _extends({}, prevState.nodes.nodeMap);
|
|
38
38
|
const newItemMap = _extends({}, prevState.nodes.itemMap);
|
|
39
|
-
delete newNodeMap[
|
|
40
|
-
delete newItemMap[
|
|
39
|
+
delete newNodeMap[itemId];
|
|
40
|
+
delete newItemMap[itemId];
|
|
41
41
|
return _extends({}, prevState, {
|
|
42
42
|
nodes: _extends({}, prevState.nodes, {
|
|
43
43
|
nodeMap: newNodeMap,
|
|
@@ -46,18 +46,18 @@ export const useTreeViewJSXNodes = ({
|
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
publishTreeViewEvent(instance, 'removeNode', {
|
|
49
|
-
id:
|
|
49
|
+
id: itemId
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
-
const mapFirstCharFromJSX = useEventCallback((
|
|
52
|
+
const mapFirstCharFromJSX = useEventCallback((itemId, firstChar) => {
|
|
53
53
|
instance.updateFirstCharMap(firstCharMap => {
|
|
54
|
-
firstCharMap[
|
|
54
|
+
firstCharMap[itemId] = firstChar;
|
|
55
55
|
return firstCharMap;
|
|
56
56
|
});
|
|
57
57
|
return () => {
|
|
58
58
|
instance.updateFirstCharMap(firstCharMap => {
|
|
59
59
|
const newMap = _extends({}, firstCharMap);
|
|
60
|
-
delete newMap[
|
|
60
|
+
delete newMap[itemId];
|
|
61
61
|
return newMap;
|
|
62
62
|
});
|
|
63
63
|
};
|
|
@@ -77,7 +77,7 @@ const useTreeViewJSXNodesItemPlugin = ({
|
|
|
77
77
|
children,
|
|
78
78
|
disabled = false,
|
|
79
79
|
label,
|
|
80
|
-
|
|
80
|
+
itemId,
|
|
81
81
|
id
|
|
82
82
|
} = props;
|
|
83
83
|
const {
|
|
@@ -96,8 +96,8 @@ const useTreeViewJSXNodesItemPlugin = ({
|
|
|
96
96
|
const handleContentRef = useForkRef(pluginContentRef, contentRef);
|
|
97
97
|
const descendant = React.useMemo(() => ({
|
|
98
98
|
element: treeItemElement,
|
|
99
|
-
id:
|
|
100
|
-
}), [
|
|
99
|
+
id: itemId
|
|
100
|
+
}), [itemId, treeItemElement]);
|
|
101
101
|
const {
|
|
102
102
|
index,
|
|
103
103
|
parentId
|
|
@@ -106,24 +106,23 @@ const useTreeViewJSXNodesItemPlugin = ({
|
|
|
106
106
|
// On the first render a node's index will be -1. We want to wait for the real index.
|
|
107
107
|
if (index !== -1) {
|
|
108
108
|
instance.insertJSXNode({
|
|
109
|
-
id:
|
|
109
|
+
id: itemId,
|
|
110
110
|
idAttribute: id,
|
|
111
111
|
index,
|
|
112
112
|
parentId,
|
|
113
113
|
expandable,
|
|
114
114
|
disabled
|
|
115
115
|
});
|
|
116
|
-
return () => instance.removeJSXNode(
|
|
116
|
+
return () => instance.removeJSXNode(itemId);
|
|
117
117
|
}
|
|
118
118
|
return undefined;
|
|
119
|
-
}, [instance, parentId, index,
|
|
119
|
+
}, [instance, parentId, index, itemId, expandable, disabled, id]);
|
|
120
120
|
React.useEffect(() => {
|
|
121
121
|
if (label) {
|
|
122
|
-
|
|
123
|
-
return instance.mapFirstCharFromJSX(nodeId, ((_pluginContentRef$cur = (_pluginContentRef$cur2 = pluginContentRef.current) == null ? void 0 : _pluginContentRef$cur2.textContent) != null ? _pluginContentRef$cur : '').substring(0, 1).toLowerCase());
|
|
122
|
+
return instance.mapFirstCharFromJSX(itemId, (pluginContentRef.current?.textContent ?? '').substring(0, 1).toLowerCase());
|
|
124
123
|
}
|
|
125
124
|
return undefined;
|
|
126
|
-
}, [instance,
|
|
125
|
+
}, [instance, itemId, label]);
|
|
127
126
|
return {
|
|
128
127
|
contentRef: handleContentRef,
|
|
129
128
|
rootRef: handleRootRef
|
|
@@ -132,9 +131,9 @@ const useTreeViewJSXNodesItemPlugin = ({
|
|
|
132
131
|
useTreeViewJSXNodes.itemPlugin = useTreeViewJSXNodesItemPlugin;
|
|
133
132
|
useTreeViewJSXNodes.wrapItem = ({
|
|
134
133
|
children,
|
|
135
|
-
|
|
134
|
+
itemId
|
|
136
135
|
}) => /*#__PURE__*/_jsx(DescendantProvider, {
|
|
137
|
-
id:
|
|
136
|
+
id: itemId,
|
|
138
137
|
children: children
|
|
139
138
|
});
|
|
140
139
|
useTreeViewJSXNodes.params = {};
|
|
@@ -3,8 +3,8 @@ import { UseTreeViewNodesSignature } from '../useTreeViewNodes';
|
|
|
3
3
|
import { UseTreeViewKeyboardNavigationSignature } from '../useTreeViewKeyboardNavigation';
|
|
4
4
|
export interface UseTreeViewNodesInstance {
|
|
5
5
|
insertJSXNode: (node: TreeViewNode) => void;
|
|
6
|
-
removeJSXNode: (
|
|
7
|
-
mapFirstCharFromJSX: (
|
|
6
|
+
removeJSXNode: (itemId: string) => void;
|
|
7
|
+
mapFirstCharFromJSX: (itemId: string, firstChar: string) => () => void;
|
|
8
8
|
}
|
|
9
9
|
export interface UseTreeViewNodesParameters {
|
|
10
10
|
}
|
|
@@ -15,8 +15,7 @@ function findNextFirstChar(firstChars, startIndex, char) {
|
|
|
15
15
|
}
|
|
16
16
|
export const useTreeViewKeyboardNavigation = ({
|
|
17
17
|
instance,
|
|
18
|
-
params
|
|
19
|
-
state
|
|
18
|
+
params
|
|
20
19
|
}) => {
|
|
21
20
|
const theme = useTheme();
|
|
22
21
|
const isRTL = theme.direction === 'rtl';
|
|
@@ -32,37 +31,33 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
32
31
|
}
|
|
33
32
|
const newFirstCharMap = {};
|
|
34
33
|
const processItem = item => {
|
|
35
|
-
var _item$children;
|
|
36
34
|
const getItemId = params.getItemId;
|
|
37
|
-
const
|
|
38
|
-
newFirstCharMap[
|
|
39
|
-
|
|
35
|
+
const itemId = getItemId ? getItemId(item) : item.id;
|
|
36
|
+
newFirstCharMap[itemId] = instance.getNode(itemId).label.substring(0, 1).toLowerCase();
|
|
37
|
+
item.children?.forEach(processItem);
|
|
40
38
|
};
|
|
41
39
|
params.items.forEach(processItem);
|
|
42
40
|
firstCharMap.current = newFirstCharMap;
|
|
43
41
|
}, [params.items, params.getItemId, instance]);
|
|
44
|
-
|
|
45
|
-
updateFirstCharMap
|
|
46
|
-
});
|
|
47
|
-
const getFirstMatchingNode = (nodeId, firstChar) => {
|
|
42
|
+
const getFirstMatchingItem = (itemId, firstChar) => {
|
|
48
43
|
let start;
|
|
49
44
|
let index;
|
|
50
45
|
const lowercaseChar = firstChar.toLowerCase();
|
|
51
46
|
const firstCharIds = [];
|
|
52
47
|
const firstChars = [];
|
|
53
48
|
// This really only works since the ids are strings
|
|
54
|
-
Object.keys(firstCharMap.current).forEach(
|
|
55
|
-
const map = instance.getNode(
|
|
49
|
+
Object.keys(firstCharMap.current).forEach(mapItemId => {
|
|
50
|
+
const map = instance.getNode(mapItemId);
|
|
56
51
|
const visible = map.parentId ? instance.isNodeExpanded(map.parentId) : true;
|
|
57
|
-
const shouldBeSkipped = params.disabledItemsFocusable ? false : instance.isNodeDisabled(
|
|
52
|
+
const shouldBeSkipped = params.disabledItemsFocusable ? false : instance.isNodeDisabled(mapItemId);
|
|
58
53
|
if (visible && !shouldBeSkipped) {
|
|
59
|
-
firstCharIds.push(
|
|
60
|
-
firstChars.push(firstCharMap.current[
|
|
54
|
+
firstCharIds.push(mapItemId);
|
|
55
|
+
firstChars.push(firstCharMap.current[mapItemId]);
|
|
61
56
|
}
|
|
62
57
|
});
|
|
63
58
|
|
|
64
59
|
// Get start index for search based on position of currentItem
|
|
65
|
-
start = firstCharIds.indexOf(
|
|
60
|
+
start = firstCharIds.indexOf(itemId) + 1;
|
|
66
61
|
if (start >= firstCharIds.length) {
|
|
67
62
|
start = 0;
|
|
68
63
|
}
|
|
@@ -81,21 +76,17 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
81
76
|
}
|
|
82
77
|
return null;
|
|
83
78
|
};
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
return !instance.isNodeDisabled(
|
|
79
|
+
const canToggleItemSelection = itemId => !params.disableSelection && !instance.isNodeDisabled(itemId);
|
|
80
|
+
const canToggleItemExpansion = itemId => {
|
|
81
|
+
return !instance.isNodeDisabled(itemId) && instance.isNodeExpandable(itemId);
|
|
87
82
|
};
|
|
88
83
|
|
|
89
84
|
// ARIA specification: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/#keyboardinteraction
|
|
90
|
-
const
|
|
91
|
-
var _otherHandlers$onKeyD;
|
|
92
|
-
(_otherHandlers$onKeyD = otherHandlers.onKeyDown) == null || _otherHandlers$onKeyD.call(otherHandlers, event);
|
|
85
|
+
const handleItemKeyDown = (event, itemId) => {
|
|
93
86
|
if (event.defaultMuiPrevented) {
|
|
94
87
|
return;
|
|
95
88
|
}
|
|
96
|
-
|
|
97
|
-
// If the tree is empty, there will be no focused node
|
|
98
|
-
if (event.altKey || event.currentTarget !== event.target || state.focusedNodeId == null) {
|
|
89
|
+
if (event.altKey || event.currentTarget !== event.target) {
|
|
99
90
|
return;
|
|
100
91
|
}
|
|
101
92
|
const ctrlPressed = event.ctrlKey || event.metaKey;
|
|
@@ -104,17 +95,17 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
104
95
|
// eslint-disable-next-line default-case
|
|
105
96
|
switch (true) {
|
|
106
97
|
// Select the node when pressing "Space"
|
|
107
|
-
case key === ' ' &&
|
|
98
|
+
case key === ' ' && canToggleItemSelection(itemId):
|
|
108
99
|
{
|
|
109
100
|
event.preventDefault();
|
|
110
101
|
if (params.multiSelect && event.shiftKey) {
|
|
111
102
|
instance.selectRange(event, {
|
|
112
|
-
end:
|
|
103
|
+
end: itemId
|
|
113
104
|
});
|
|
114
105
|
} else if (params.multiSelect) {
|
|
115
|
-
instance.selectNode(event,
|
|
106
|
+
instance.selectNode(event, itemId, true);
|
|
116
107
|
} else {
|
|
117
|
-
instance.selectNode(event,
|
|
108
|
+
instance.selectNode(event, itemId);
|
|
118
109
|
}
|
|
119
110
|
break;
|
|
120
111
|
}
|
|
@@ -123,84 +114,87 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
123
114
|
// If the focused node has no children, we select it.
|
|
124
115
|
case key === 'Enter':
|
|
125
116
|
{
|
|
126
|
-
if (
|
|
127
|
-
instance.toggleNodeExpansion(event,
|
|
117
|
+
if (canToggleItemExpansion(itemId)) {
|
|
118
|
+
instance.toggleNodeExpansion(event, itemId);
|
|
128
119
|
event.preventDefault();
|
|
129
|
-
} else if (
|
|
120
|
+
} else if (canToggleItemSelection(itemId)) {
|
|
130
121
|
if (params.multiSelect) {
|
|
131
122
|
event.preventDefault();
|
|
132
|
-
instance.selectNode(event,
|
|
133
|
-
} else if (!instance.isNodeSelected(
|
|
134
|
-
instance.selectNode(event,
|
|
123
|
+
instance.selectNode(event, itemId, true);
|
|
124
|
+
} else if (!instance.isNodeSelected(itemId)) {
|
|
125
|
+
instance.selectNode(event, itemId);
|
|
135
126
|
event.preventDefault();
|
|
136
127
|
}
|
|
137
128
|
}
|
|
138
129
|
break;
|
|
139
130
|
}
|
|
140
131
|
|
|
141
|
-
// Focus the next focusable
|
|
132
|
+
// Focus the next focusable item
|
|
142
133
|
case key === 'ArrowDown':
|
|
143
134
|
{
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
135
|
+
const nextItem = getNextNode(instance, itemId);
|
|
136
|
+
if (nextItem) {
|
|
146
137
|
event.preventDefault();
|
|
147
|
-
instance.focusItem(event,
|
|
138
|
+
instance.focusItem(event, nextItem);
|
|
148
139
|
|
|
149
140
|
// Multi select behavior when pressing Shift + ArrowDown
|
|
150
|
-
// Toggles the selection state of the next
|
|
151
|
-
if (params.multiSelect && event.shiftKey &&
|
|
141
|
+
// Toggles the selection state of the next item
|
|
142
|
+
if (params.multiSelect && event.shiftKey && canToggleItemSelection(nextItem)) {
|
|
152
143
|
instance.selectRange(event, {
|
|
153
|
-
end:
|
|
154
|
-
current:
|
|
144
|
+
end: nextItem,
|
|
145
|
+
current: itemId
|
|
155
146
|
}, true);
|
|
156
147
|
}
|
|
157
148
|
}
|
|
158
149
|
break;
|
|
159
150
|
}
|
|
160
151
|
|
|
161
|
-
// Focuses the previous focusable
|
|
152
|
+
// Focuses the previous focusable item
|
|
162
153
|
case key === 'ArrowUp':
|
|
163
154
|
{
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
155
|
+
const previousItem = getPreviousNode(instance, itemId);
|
|
156
|
+
if (previousItem) {
|
|
166
157
|
event.preventDefault();
|
|
167
|
-
instance.focusItem(event,
|
|
158
|
+
instance.focusItem(event, previousItem);
|
|
168
159
|
|
|
169
160
|
// Multi select behavior when pressing Shift + ArrowUp
|
|
170
|
-
// Toggles the selection state of the previous
|
|
171
|
-
if (params.multiSelect && event.shiftKey &&
|
|
161
|
+
// Toggles the selection state of the previous item
|
|
162
|
+
if (params.multiSelect && event.shiftKey && canToggleItemSelection(previousItem)) {
|
|
172
163
|
instance.selectRange(event, {
|
|
173
|
-
end:
|
|
174
|
-
current:
|
|
164
|
+
end: previousItem,
|
|
165
|
+
current: itemId
|
|
175
166
|
}, true);
|
|
176
167
|
}
|
|
177
168
|
}
|
|
178
169
|
break;
|
|
179
170
|
}
|
|
180
171
|
|
|
181
|
-
// If the focused
|
|
182
|
-
// If the focused
|
|
172
|
+
// If the focused item is expanded, we move the focus to its first child
|
|
173
|
+
// If the focused item is collapsed and has children, we expand it
|
|
183
174
|
case key === 'ArrowRight' && !isRTL || key === 'ArrowLeft' && isRTL:
|
|
184
175
|
{
|
|
185
|
-
if (instance.isNodeExpanded(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
176
|
+
if (instance.isNodeExpanded(itemId)) {
|
|
177
|
+
const nextNodeId = getNextNode(instance, itemId);
|
|
178
|
+
if (nextNodeId) {
|
|
179
|
+
instance.focusItem(event, nextNodeId);
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
}
|
|
182
|
+
} else if (canToggleItemExpansion(itemId)) {
|
|
183
|
+
instance.toggleNodeExpansion(event, itemId);
|
|
190
184
|
event.preventDefault();
|
|
191
185
|
}
|
|
192
186
|
break;
|
|
193
187
|
}
|
|
194
188
|
|
|
195
|
-
// If the focused
|
|
196
|
-
// If the focused
|
|
189
|
+
// If the focused item is expanded, we collapse it
|
|
190
|
+
// If the focused item is collapsed and has a parent, we move the focus to this parent
|
|
197
191
|
case key === 'ArrowLeft' && !isRTL || key === 'ArrowRight' && isRTL:
|
|
198
192
|
{
|
|
199
|
-
if (
|
|
200
|
-
instance.toggleNodeExpansion(event,
|
|
193
|
+
if (canToggleItemExpansion(itemId) && instance.isNodeExpanded(itemId)) {
|
|
194
|
+
instance.toggleNodeExpansion(event, itemId);
|
|
201
195
|
event.preventDefault();
|
|
202
196
|
} else {
|
|
203
|
-
const parent = instance.getNode(
|
|
197
|
+
const parent = instance.getNode(itemId).parentId;
|
|
204
198
|
if (parent) {
|
|
205
199
|
instance.focusItem(event, parent);
|
|
206
200
|
event.preventDefault();
|
|
@@ -216,31 +210,31 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
216
210
|
|
|
217
211
|
// Multi select behavior when pressing Ctrl + Shift + Home
|
|
218
212
|
// Selects the focused node and all nodes up to the first node.
|
|
219
|
-
if (
|
|
220
|
-
instance.rangeSelectToFirst(event,
|
|
213
|
+
if (canToggleItemSelection(itemId) && params.multiSelect && ctrlPressed && event.shiftKey) {
|
|
214
|
+
instance.rangeSelectToFirst(event, itemId);
|
|
221
215
|
}
|
|
222
216
|
event.preventDefault();
|
|
223
217
|
break;
|
|
224
218
|
}
|
|
225
219
|
|
|
226
|
-
// Focuses the last
|
|
220
|
+
// Focuses the last item in the tree
|
|
227
221
|
case key === 'End':
|
|
228
222
|
{
|
|
229
223
|
instance.focusItem(event, getLastNode(instance));
|
|
230
224
|
|
|
231
225
|
// Multi select behavior when pressing Ctrl + Shirt + End
|
|
232
|
-
// Selects the focused
|
|
233
|
-
if (
|
|
234
|
-
instance.rangeSelectToLast(event,
|
|
226
|
+
// Selects the focused item and all the items down to the last item.
|
|
227
|
+
if (canToggleItemSelection(itemId) && params.multiSelect && ctrlPressed && event.shiftKey) {
|
|
228
|
+
instance.rangeSelectToLast(event, itemId);
|
|
235
229
|
}
|
|
236
230
|
event.preventDefault();
|
|
237
231
|
break;
|
|
238
232
|
}
|
|
239
233
|
|
|
240
|
-
// Expand all siblings that are at the same level as the focused
|
|
234
|
+
// Expand all siblings that are at the same level as the focused item
|
|
241
235
|
case key === '*':
|
|
242
236
|
{
|
|
243
|
-
instance.expandAllSiblings(event,
|
|
237
|
+
instance.expandAllSiblings(event, itemId);
|
|
244
238
|
event.preventDefault();
|
|
245
239
|
break;
|
|
246
240
|
}
|
|
@@ -261,7 +255,7 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
261
255
|
// TODO: Support typing multiple characters
|
|
262
256
|
case !ctrlPressed && !event.shiftKey && isPrintableCharacter(key):
|
|
263
257
|
{
|
|
264
|
-
const matchingNode =
|
|
258
|
+
const matchingNode = getFirstMatchingItem(itemId, key);
|
|
265
259
|
if (matchingNode != null) {
|
|
266
260
|
instance.focusItem(event, matchingNode);
|
|
267
261
|
event.preventDefault();
|
|
@@ -270,10 +264,9 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
270
264
|
}
|
|
271
265
|
}
|
|
272
266
|
};
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
};
|
|
267
|
+
populateInstance(instance, {
|
|
268
|
+
updateFirstCharMap,
|
|
269
|
+
handleItemKeyDown
|
|
270
|
+
});
|
|
278
271
|
};
|
|
279
272
|
useTreeViewKeyboardNavigation.params = {};
|
package/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
1
2
|
import { TreeViewPluginSignature } from '../../models';
|
|
2
3
|
import { UseTreeViewNodesSignature } from '../useTreeViewNodes';
|
|
3
4
|
import { UseTreeViewSelectionSignature } from '../useTreeViewSelection';
|
|
4
5
|
import { UseTreeViewFocusSignature } from '../useTreeViewFocus';
|
|
5
6
|
import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion';
|
|
7
|
+
import { MuiCancellableEvent } from '../../models/MuiCancellableEvent';
|
|
6
8
|
export interface UseTreeViewKeyboardNavigationInstance {
|
|
7
9
|
updateFirstCharMap: (updater: (map: TreeViewFirstCharMap) => TreeViewFirstCharMap) => void;
|
|
10
|
+
handleItemKeyDown: (event: React.KeyboardEvent<HTMLElement> & MuiCancellableEvent, itemId: string) => void;
|
|
8
11
|
}
|
|
9
12
|
export type UseTreeViewKeyboardNavigationSignature = TreeViewPluginSignature<{
|
|
10
13
|
instance: UseTreeViewKeyboardNavigationInstance;
|
|
@@ -16,5 +19,5 @@ export type UseTreeViewKeyboardNavigationSignature = TreeViewPluginSignature<{
|
|
|
16
19
|
];
|
|
17
20
|
}>;
|
|
18
21
|
export type TreeViewFirstCharMap = {
|
|
19
|
-
[
|
|
22
|
+
[itemId: string]: string;
|
|
20
23
|
};
|