@mui/x-tree-view 8.14.1 → 8.16.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 CHANGED
@@ -5,6 +5,213 @@
5
5
  All notable changes to this project will be documented in this file.
6
6
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
7
7
 
8
+ ## 8.16.0
9
+
10
+ _Oct 29, 2025_
11
+
12
+ We'd like to extend a big thank you to the 14 contributors who made this release possible. Here are some highlights ✨:
13
+
14
+ - 🖌️ Add `brush` zoom interaction to charts
15
+ - 🔁 [Server-side update](https://mui.com/x/react-data-grid/server-side-data/#updating-server-side-data) in a grid with tree data/row grouping and aggregation will trigger re-fetch for all parent levels of that row to update aggregated values. See the [demo](https://mui.com/x/react-data-grid/server-side-data/aggregation/#usage-with-tree-data).
16
+
17
+ Special thanks go out to the community members for their valuable contributions:
18
+ @felix-wg, @frncesc, @sai6855
19
+
20
+ The following are all team members who have contributed to this release:
21
+ @alexfauquette, @arminmeh, @bernardobelchior, @brijeshb42, @flaviendelangle, @JCQuintas, @MBilalShafi, @mbrookes, @michelengelen, @noraleonte, @rita-codes
22
+
23
+ ### Data Grid
24
+
25
+ #### `@mui/x-data-grid@8.16.0`
26
+
27
+ - [DataGrid] Ignore `Ctrl+A` key combination for the row selection in the community version (#20110) @felix-wg
28
+ - [DataGrid][l10n] Improve Spanish (es-ES) locale (#20134) @frncesc
29
+
30
+ #### `@mui/x-data-grid-pro@8.16.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
31
+
32
+ Same changes as in `@mui/x-data-grid@8.16.0`, plus:
33
+
34
+ - [DataGridPro] Add explicit return type to `getVisibleRowsLookup()` to fix the build with `tsc` (#20116) @arminmeh
35
+ - [DataGridPro] Retain the expansion state with expansion configuration props (#20126) @MBilalShafi
36
+
37
+ #### `@mui/x-data-grid-premium@8.16.0` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
38
+
39
+ Same changes as in `@mui/x-data-grid-pro@8.16.0`, plus:
40
+
41
+ - [DataGridPremium] Export and restore chart integration state (#20079) @arminmeh
42
+ - [DataGridPremium] Fix grouping column `valueFormatter()` crash (#20070) @sai6855
43
+ - [DataGridPremium] Refetch aggregation data after row update with server-side aggregation (#20039) @arminmeh
44
+
45
+ ### Date and Time Pickers
46
+
47
+ #### `@mui/x-date-pickers@8.16.0`
48
+
49
+ - [pickers] Prevent blur event propagation on individual sections (#19825) @michelengelen
50
+
51
+ #### `@mui/x-date-pickers-pro@8.16.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
52
+
53
+ Same changes as in `@mui/x-date-pickers@8.16.0`.
54
+
55
+ ### Charts
56
+
57
+ #### `@mui/x-charts@8.16.0`
58
+
59
+ - [charts] Allow tooltip to anchor items (#19954) @alexfauquette
60
+ - [charts] Fix behavior of grouped axis (#20118) @JCQuintas
61
+ - [charts] Move scale symlog inside scales (#20137) @JCQuintas
62
+ - [charts] Fix AreaChartConnectNulls demo height not correctly resizing (#20078) @sai6855
63
+ - [charts] Fix charts resizing overflow (#20080) @alexfauquette
64
+ - [charts] Fix tooltip not showing on first render (#20115) @bernardobelchior
65
+ - [charts] Handle `undefined` id and color in series (#20087) @bernardobelchior
66
+ - [charts] Remove `useMemo` from isZoomOn*Enabled and isPanOn*Enabled hooks (#20132) @Copilot
67
+ - [charts] Use static data for perf (#20072) @JCQuintas
68
+ - [charts] Move scale symlog inside scales (#20137) @JCQuintas
69
+
70
+ #### `@mui/x-charts-pro@8.16.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
71
+
72
+ Same changes as in `@mui/x-charts@8.16.0`, plus:
73
+
74
+ - [charts-pro] Add `brush` zoom interaction (#19899) @JCQuintas
75
+ - [charts-pro] Add sankey performance check (#20069) @JCQuintas
76
+
77
+ #### `@mui/x-charts-premium@8.16.0` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
78
+
79
+ Same changes as in `@mui/x-charts-pro@8.16.0`.
80
+
81
+ ### Tree View
82
+
83
+ #### `@mui/x-tree-view@8.16.0`
84
+
85
+ Internal changes.
86
+
87
+ #### `@mui/x-tree-view-pro@8.16.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
88
+
89
+ Same changes as in `@mui/x-tree-view@8.16.0`.
90
+
91
+ ### Codemod
92
+
93
+ #### `@mui/x-codemod@8.16.0`
94
+
95
+ Internal changes.
96
+
97
+ ### Core
98
+
99
+ - [code-infra] Setup eslint compat plugin (#20105) @brijeshb42
100
+ - [code-infra] Improve store types (#20129) @JCQuintas
101
+ - [docs] Update the callout in `rows` prop documentation (#20127) @MBilalShafi
102
+ - [docs-infra] Refine changelog contributor acknowledgment messages (#20123) @mbrookes
103
+
104
+ ### Miscellaneous
105
+
106
+ - [x-telemetry] Skip telemetry tests on browser mode (#20122) @bernardobelchior
107
+
108
+ ## 8.15.0
109
+
110
+ _Oct 23, 2025_
111
+
112
+ We'd like to extend a big thank you to the 14 contributors who made this release possible. Here are some highlights ✨:
113
+
114
+ - 🖌️ Add new [`brush` charts interaction](https://mui.com/x/react-charts/brush/) for building custom behavior.
115
+ ![brush visualization example](https://github.com/user-attachments/assets/60c382a1-e418-4736-8dcb-1567c4e361e3)
116
+ - ⚡️ Performance improvements for large bar charts
117
+ - 🤖 Data Grid AI assistant can now [visualize the query results](https://mui.com/x/react-data-grid/ai-assistant/#data-visualization) by controlling the chart integration settings
118
+ - 📦 DataGrid uses an internal MUI fork of ExcelJS that does not depend on vulnerable versions of NPM packages
119
+ - 🐞 Bugfixes
120
+ - 📚 Documentation improvements
121
+
122
+ Special thanks go out to the community members for their valuable contributions:
123
+ @ZagrebaAlex
124
+
125
+ The following are all team members who have contributed to this release:
126
+ @alexfauquette, @bernardobelchior, @cherniavskii, @flaviendelangle, @Janpot, @JCQuintas, @KenanYusuf, @prakhargupta1, @rita-codes, @siriwatknp, @arminmeh, @brijeshb42, @noraleonte
127
+
128
+ ### Data Grid
129
+
130
+ #### `@mui/x-data-grid@8.15.0`
131
+
132
+ - [DataGrid] Fix `dataSource.fetchRows` API's return type (#20068) @arminmeh
133
+
134
+ #### `@mui/x-data-grid-pro@8.15.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
135
+
136
+ Same changes as in `@mui/x-data-grid@8.15.0`, plus:
137
+
138
+ - [DataGridPro] Keep children in the tree after parent row is re-fetched with the data source (#19934) @arminmeh
139
+ - [DataGridPro] Support scroll shadows customization (#19982) @KenanYusuf
140
+
141
+ #### `@mui/x-data-grid-premium@8.15.0` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
142
+
143
+ Same changes as in `@mui/x-data-grid-pro@8.15.0`, plus:
144
+
145
+ - [DataGridPremium] Use ExcelJS fork (#19796) @cherniavskii
146
+ - [DataGridPremium] Support data visualization in AI Assistant (#19831) @arminmeh
147
+
148
+ ### Date and Time Pickers
149
+
150
+ #### `@mui/x-date-pickers@8.15.0`
151
+
152
+ Internal changes.
153
+
154
+ #### `@mui/x-date-pickers-pro@8.15.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
155
+
156
+ Same changes as in `@mui/x-date-pickers@8.15.0`.
157
+
158
+ ### Charts
159
+
160
+ #### `@mui/x-charts@8.15.0`
161
+
162
+ - [charts] Add `ChartsBrushOverlay` and allow brush configuration (#19956) @JCQuintas
163
+ - [charts] Add `getStringSize` benchmark. Remove benchmarks from built package. (#19995) @bernardobelchior
164
+ - [charts] Batch string size measurement (#19994) @bernardobelchior
165
+ - [charts] Fix console issue (#20025) @JCQuintas
166
+ - [charts] Fix is[ZoomFeature]Enabled type (#20058) @alexfauquette
167
+ - [charts] Fix reference line middle spacing (#20004) @JCQuintas
168
+ - [charts] Improve `getStringSize` and `batchMeasureStrings` performance (#19996) @bernardobelchior
169
+ - [charts] Improve deep export script (#20007) @JCQuintas
170
+ - [charts] Improve string measurement benchmarks (#19999) @bernardobelchior
171
+ - [charts] Measure string sizes using SVG elements (#19981) @bernardobelchior
172
+ - [l10n] Improve Greek (gr-GR) locale (#20060) @ZagrebaAlex
173
+
174
+ #### `@mui/x-charts-pro@8.15.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
175
+
176
+ Same changes as in `@mui/x-charts@8.15.0`, plus:
177
+
178
+ - [charts-pro] Fix pan with `axis.reverse` (#20031) @JCQuintas
179
+
180
+ #### `@mui/x-charts-premium@8.15.0` [![premium](https://mui.com/r/x-premium-svg)](https://mui.com/r/x-premium-svg-link 'Premium plan')
181
+
182
+ Same changes as in `@mui/x-charts-pro@8.15.0`.
183
+
184
+ ### Tree View
185
+
186
+ #### `@mui/x-tree-view@8.15.0`
187
+
188
+ - [tree view] Multi character type-ahead (#19942) @noraleonte
189
+
190
+ #### `@mui/x-tree-view-pro@8.15.0` [![pro](https://mui.com/r/x-pro-svg)](https://mui.com/r/x-pro-svg-link 'Pro plan')
191
+
192
+ Same changes as in `@mui/x-tree-view@8.15.0`.
193
+
194
+ ### Codemod
195
+
196
+ #### `@mui/x-codemod@8.14.0`
197
+
198
+ Internal changes.
199
+
200
+ ### Docs
201
+
202
+ - [docs] Add overview section for scatter chart and heatmap (#19888) @prakhargupta1
203
+ - [docs] Add charts bell curve example (#20003) @JCQuintas
204
+ - [docs] Add grouped multiple fields for Data Grid row grouping recipe (#19964) @siriwatknp
205
+ - [docs] Add Data Grid loading state recipe (#19958) @siriwatknp
206
+
207
+ ### Core
208
+
209
+ - [code-infra] Remove @mui/monorepo usage for react versioning (#19894) @Janpot
210
+ - [code-infra] Remove invalid `environment: 'browser'` from vitest browser config (#19993) @bernardobelchior
211
+ - [code-infra] Remove unused babel aliases (#19987) @Janpot
212
+ - [code-infra] Turn on all testing-library eslint rules (#19946) @brijeshb42
213
+ - [docs-infra] Fix broken hash link (#20062) @Janpot
214
+
8
215
  ## 8.14.1
9
216
 
10
217
  _Oct 16, 2025_
package/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-tree-view v8.14.1
2
+ * @mui/x-tree-view v8.16.0
3
3
  *
4
4
  * @license MIT
5
5
  * This source code is licensed under the MIT license found in the
@@ -57,14 +57,14 @@ export const useTreeViewJSXItems = ({
57
57
  })
58
58
  }));
59
59
  };
60
- const mapFirstCharFromJSX = useEventCallback((itemId, firstChar) => {
61
- instance.updateFirstCharMap(firstCharMap => {
62
- firstCharMap[itemId] = firstChar;
63
- return firstCharMap;
60
+ const mapLabelFromJSX = useEventCallback((itemId, label) => {
61
+ instance.updateLabelMap(labelMap => {
62
+ labelMap[itemId] = label;
63
+ return labelMap;
64
64
  });
65
65
  return () => {
66
- instance.updateFirstCharMap(firstCharMap => {
67
- const newMap = _extends({}, firstCharMap);
66
+ instance.updateLabelMap(labelMap => {
67
+ const newMap = _extends({}, labelMap);
68
68
  delete newMap[itemId];
69
69
  return newMap;
70
70
  });
@@ -74,7 +74,7 @@ export const useTreeViewJSXItems = ({
74
74
  instance: {
75
75
  insertJSXItem,
76
76
  setJSXItemsOrderedChildrenIds,
77
- mapFirstCharFromJSX
77
+ mapLabelFromJSX
78
78
  }
79
79
  };
80
80
  };
@@ -132,7 +132,7 @@ const useTreeViewJSXItemsItemPlugin = ({
132
132
  }, [instance, parentId, itemId, expandable, disabled, id]);
133
133
  React.useEffect(() => {
134
134
  if (label) {
135
- return instance.mapFirstCharFromJSX(itemId, (pluginContentRef.current?.textContent ?? '').substring(0, 1).toLowerCase());
135
+ return instance.mapLabelFromJSX(itemId, (pluginContentRef.current?.textContent ?? '').toLowerCase());
136
136
  }
137
137
  return undefined;
138
138
  }, [instance, itemId, label]);
@@ -10,13 +10,13 @@ export interface UseTreeViewItemsInstance {
10
10
  */
11
11
  insertJSXItem: (item: TreeViewItemMeta) => () => void;
12
12
  /**
13
- * Updates the `firstCharMap` to register the first character of the given item's label.
13
+ * Updates the `labelMap` to register the first character of the given item's label.
14
14
  * This map is used to navigate the tree using type-ahead search.
15
- * @param {TreeViewItemId} itemId The id of the item to map the first character of.
16
- * @param {string} firstChar The first character of the item's label.
17
- * @returns {() => void} A function to remove the item from the `firstCharMap`.
15
+ * @param {TreeViewItemId} itemId The id of the item to map the label of.
16
+ * @param {string} label The item's label.
17
+ * @returns {() => void} A function to remove the item from the `labelMap`.
18
18
  */
19
- mapFirstCharFromJSX: (itemId: TreeViewItemId, firstChar: string) => () => void;
19
+ mapLabelFromJSX: (itemId: TreeViewItemId, label: string) => () => void;
20
20
  /**
21
21
  * Store the ids of a given item's children in the state.
22
22
  * Those ids must be passed in the order they should be rendered.
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react';
4
4
  import { useStore } from '@mui/x-internals/store';
5
5
  import { useRtl } from '@mui/system/RtlProvider';
6
+ import { useTimeout } from '@base-ui-components/utils/useTimeout';
6
7
  import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
7
8
  import { getFirstNavigableItem, getLastNavigableItem, getNextNavigableItem, getPreviousNavigableItem, isTargetInDescendants } from "../../utils/tree.js";
8
9
  import { hasPlugin } from "../../utils/plugins.js";
@@ -14,44 +15,48 @@ import { expansionSelectors } from "../useTreeViewExpansion/useTreeViewExpansion
14
15
  function isPrintableKey(string) {
15
16
  return !!string && string.length === 1 && !!string.match(/\S/);
16
17
  }
18
+ const TYPEAHEAD_TIMEOUT = 500;
17
19
  export const useTreeViewKeyboardNavigation = ({
18
20
  instance,
19
21
  store,
20
22
  params
21
23
  }) => {
22
24
  const isRtl = useRtl();
23
- const firstCharMap = React.useRef({});
24
- const updateFirstCharMap = useEventCallback(callback => {
25
- firstCharMap.current = callback(firstCharMap.current);
25
+ const labelMap = React.useRef({});
26
+ const typeaheadQueryRef = React.useRef('');
27
+ const typeaheadTimeout = useTimeout();
28
+ const updateLabelMap = useEventCallback(callback => {
29
+ labelMap.current = callback(labelMap.current);
26
30
  });
27
31
  const itemMetaLookup = useStore(store, itemsSelectors.itemMetaLookup);
28
32
  React.useEffect(() => {
29
33
  if (instance.areItemUpdatesPrevented()) {
30
34
  return;
31
35
  }
32
- const newFirstCharMap = {};
36
+ const newLabelMap = {};
33
37
  const processItem = item => {
34
- newFirstCharMap[item.id] = item.label.substring(0, 1).toLowerCase();
38
+ newLabelMap[item.id] = item.label.toLowerCase();
35
39
  };
36
40
  Object.values(itemMetaLookup).forEach(processItem);
37
- firstCharMap.current = newFirstCharMap;
41
+ labelMap.current = newLabelMap;
38
42
  }, [itemMetaLookup, params.getItemId, instance]);
39
- const getFirstMatchingItem = (itemId, query) => {
40
- const cleanQuery = query.toLowerCase();
41
- const getNextItem = itemIdToCheck => {
42
- const nextItemId = getNextNavigableItem(store.state, itemIdToCheck);
43
- // We reached the end of the tree, check from the beginning
44
- if (nextItemId === null) {
45
- return getFirstNavigableItem(store.state);
46
- }
47
- return nextItemId;
48
- };
43
+ const getNextItem = itemIdToCheck => {
44
+ const nextItemId = getNextNavigableItem(store.state, itemIdToCheck);
45
+ // We reached the end of the tree, check from the beginning
46
+ if (nextItemId === null) {
47
+ return getFirstNavigableItem(store.state);
48
+ }
49
+ return nextItemId;
50
+ };
51
+ const getNextMatchingItemId = (itemId, query) => {
49
52
  let matchingItemId = null;
50
- let currentItemId = getNextItem(itemId);
51
53
  const checkedItems = {};
54
+ // If query length > 1, first check if current item matches
55
+ let currentItemId = query.length > 1 ? itemId : getNextItem(itemId);
52
56
  // The "!checkedItems[currentItemId]" condition avoids an infinite loop when there is no matching item.
53
57
  while (matchingItemId == null && !checkedItems[currentItemId]) {
54
- if (firstCharMap.current[currentItemId] === cleanQuery) {
58
+ const itemLabel = labelMap.current[currentItemId];
59
+ if (itemLabel?.startsWith(query)) {
55
60
  matchingItemId = currentItemId;
56
61
  } else {
57
62
  checkedItems[currentItemId] = true;
@@ -60,6 +65,26 @@ export const useTreeViewKeyboardNavigation = ({
60
65
  }
61
66
  return matchingItemId;
62
67
  };
68
+ const getFirstMatchingItem = (itemId, newKey) => {
69
+ const cleanNewKey = newKey.toLowerCase();
70
+
71
+ // Try matching with accumulated query + new key
72
+ const concatenatedQuery = `${typeaheadQueryRef.current}${cleanNewKey}`;
73
+
74
+ // check if the entire typed query matches an item
75
+ const concatenatedQueryMatchingItemId = getNextMatchingItemId(itemId, concatenatedQuery);
76
+ if (concatenatedQueryMatchingItemId != null) {
77
+ typeaheadQueryRef.current = concatenatedQuery;
78
+ return concatenatedQueryMatchingItemId;
79
+ }
80
+ const newKeyMatchingItemId = getNextMatchingItemId(itemId, cleanNewKey);
81
+ if (newKeyMatchingItemId != null) {
82
+ typeaheadQueryRef.current = cleanNewKey;
83
+ return newKeyMatchingItemId;
84
+ }
85
+ typeaheadQueryRef.current = '';
86
+ return null;
87
+ };
63
88
  const canToggleItemSelection = itemId => selectionSelectors.enabled(store.state) && !itemsSelectors.isItemDisabled(store.state, itemId);
64
89
  const canToggleItemExpansion = itemId => {
65
90
  return !itemsSelectors.isItemDisabled(store.state, itemId) && expansionSelectors.isItemExpandable(store.state, itemId);
@@ -253,21 +278,26 @@ export const useTreeViewKeyboardNavigation = ({
253
278
  }
254
279
 
255
280
  // Type-ahead
256
- // TODO: Support typing multiple characters
257
281
  case !ctrlPressed && !event.shiftKey && isPrintableKey(key):
258
282
  {
283
+ typeaheadTimeout.clear();
259
284
  const matchingItem = getFirstMatchingItem(itemId, key);
260
285
  if (matchingItem != null) {
261
286
  instance.focusItem(event, matchingItem);
262
287
  event.preventDefault();
288
+ } else {
289
+ typeaheadQueryRef.current = '';
263
290
  }
291
+ typeaheadTimeout.start(TYPEAHEAD_TIMEOUT, () => {
292
+ typeaheadQueryRef.current = '';
293
+ });
264
294
  break;
265
295
  }
266
296
  }
267
297
  };
268
298
  return {
269
299
  instance: {
270
- updateFirstCharMap,
300
+ updateLabelMap,
271
301
  handleItemKeyDown
272
302
  }
273
303
  };
@@ -8,12 +8,12 @@ import { UseTreeViewLabelSignature } from "../useTreeViewLabel/index.js";
8
8
  import { TreeViewItemId, TreeViewCancellableEvent } from "../../../models/index.js";
9
9
  export interface UseTreeViewKeyboardNavigationInstance {
10
10
  /**
11
- * Updates the `firstCharMap` to add/remove the first character of some item's labels.
11
+ * Updates the `labelMap` to add/remove the first character of some item's labels.
12
12
  * This map is used to navigate the tree using type-ahead search.
13
13
  * This method is only used by the `useTreeViewJSXItems` plugin, otherwise the updates are handled internally.
14
- * @param {(map: TreeViewFirstCharMap) => TreeViewFirstCharMap} updater The function to update the map.
14
+ * @param {(map: TreeViewLabelMap) => TreeViewLabelMap} updater The function to update the map.
15
15
  */
16
- updateFirstCharMap: (updater: (map: TreeViewFirstCharMap) => TreeViewFirstCharMap) => void;
16
+ updateLabelMap: (updater: (map: TreeViewLabelMap) => TreeViewLabelMap) => void;
17
17
  /**
18
18
  * Callback fired when a key is pressed on an item.
19
19
  * Handles all the keyboard navigation logic.
@@ -27,6 +27,6 @@ export type UseTreeViewKeyboardNavigationSignature = TreeViewPluginSignature<{
27
27
  dependencies: [UseTreeViewItemsSignature, UseTreeViewSelectionSignature, UseTreeViewFocusSignature, UseTreeViewExpansionSignature];
28
28
  optionalDependencies: [UseTreeViewLabelSignature];
29
29
  }>;
30
- export type TreeViewFirstCharMap = {
30
+ export type TreeViewLabelMap = {
31
31
  [itemId: string]: string;
32
32
  };
@@ -37,7 +37,7 @@ export const useTreeViewLabel = ({
37
37
  }
38
38
  };
39
39
  useIsoLayoutEffect(() => {
40
- store.set('label', _extends({}, store.state.items, {
40
+ store.set('label', _extends({}, store.state.label, {
41
41
  isItemEditable: params.isItemEditable
42
42
  }));
43
43
  }, [store, params.isItemEditable]);
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-tree-view v8.14.1
2
+ * @mui/x-tree-view v8.16.0
3
3
  *
4
4
  * @license MIT
5
5
  * This source code is licensed under the MIT license found in the
@@ -64,14 +64,14 @@ const useTreeViewJSXItems = ({
64
64
  })
65
65
  }));
66
66
  };
67
- const mapFirstCharFromJSX = (0, _useEventCallback.useEventCallback)((itemId, firstChar) => {
68
- instance.updateFirstCharMap(firstCharMap => {
69
- firstCharMap[itemId] = firstChar;
70
- return firstCharMap;
67
+ const mapLabelFromJSX = (0, _useEventCallback.useEventCallback)((itemId, label) => {
68
+ instance.updateLabelMap(labelMap => {
69
+ labelMap[itemId] = label;
70
+ return labelMap;
71
71
  });
72
72
  return () => {
73
- instance.updateFirstCharMap(firstCharMap => {
74
- const newMap = (0, _extends2.default)({}, firstCharMap);
73
+ instance.updateLabelMap(labelMap => {
74
+ const newMap = (0, _extends2.default)({}, labelMap);
75
75
  delete newMap[itemId];
76
76
  return newMap;
77
77
  });
@@ -81,7 +81,7 @@ const useTreeViewJSXItems = ({
81
81
  instance: {
82
82
  insertJSXItem,
83
83
  setJSXItemsOrderedChildrenIds,
84
- mapFirstCharFromJSX
84
+ mapLabelFromJSX
85
85
  }
86
86
  };
87
87
  };
@@ -140,7 +140,7 @@ const useTreeViewJSXItemsItemPlugin = ({
140
140
  }, [instance, parentId, itemId, expandable, disabled, id]);
141
141
  React.useEffect(() => {
142
142
  if (label) {
143
- return instance.mapFirstCharFromJSX(itemId, (pluginContentRef.current?.textContent ?? '').substring(0, 1).toLowerCase());
143
+ return instance.mapLabelFromJSX(itemId, (pluginContentRef.current?.textContent ?? '').toLowerCase());
144
144
  }
145
145
  return undefined;
146
146
  }, [instance, itemId, label]);
@@ -10,13 +10,13 @@ export interface UseTreeViewItemsInstance {
10
10
  */
11
11
  insertJSXItem: (item: TreeViewItemMeta) => () => void;
12
12
  /**
13
- * Updates the `firstCharMap` to register the first character of the given item's label.
13
+ * Updates the `labelMap` to register the first character of the given item's label.
14
14
  * This map is used to navigate the tree using type-ahead search.
15
- * @param {TreeViewItemId} itemId The id of the item to map the first character of.
16
- * @param {string} firstChar The first character of the item's label.
17
- * @returns {() => void} A function to remove the item from the `firstCharMap`.
15
+ * @param {TreeViewItemId} itemId The id of the item to map the label of.
16
+ * @param {string} label The item's label.
17
+ * @returns {() => void} A function to remove the item from the `labelMap`.
18
18
  */
19
- mapFirstCharFromJSX: (itemId: TreeViewItemId, firstChar: string) => () => void;
19
+ mapLabelFromJSX: (itemId: TreeViewItemId, label: string) => () => void;
20
20
  /**
21
21
  * Store the ids of a given item's children in the state.
22
22
  * Those ids must be passed in the order they should be rendered.
@@ -9,6 +9,7 @@ exports.useTreeViewKeyboardNavigation = void 0;
9
9
  var React = _interopRequireWildcard(require("react"));
10
10
  var _store = require("@mui/x-internals/store");
11
11
  var _RtlProvider = require("@mui/system/RtlProvider");
12
+ var _useTimeout = require("@base-ui-components/utils/useTimeout");
12
13
  var _useEventCallback = require("@base-ui-components/utils/useEventCallback");
13
14
  var _tree = require("../../utils/tree");
14
15
  var _plugins = require("../../utils/plugins");
@@ -20,44 +21,48 @@ var _useTreeViewExpansion = require("../useTreeViewExpansion/useTreeViewExpansio
20
21
  function isPrintableKey(string) {
21
22
  return !!string && string.length === 1 && !!string.match(/\S/);
22
23
  }
24
+ const TYPEAHEAD_TIMEOUT = 500;
23
25
  const useTreeViewKeyboardNavigation = ({
24
26
  instance,
25
27
  store,
26
28
  params
27
29
  }) => {
28
30
  const isRtl = (0, _RtlProvider.useRtl)();
29
- const firstCharMap = React.useRef({});
30
- const updateFirstCharMap = (0, _useEventCallback.useEventCallback)(callback => {
31
- firstCharMap.current = callback(firstCharMap.current);
31
+ const labelMap = React.useRef({});
32
+ const typeaheadQueryRef = React.useRef('');
33
+ const typeaheadTimeout = (0, _useTimeout.useTimeout)();
34
+ const updateLabelMap = (0, _useEventCallback.useEventCallback)(callback => {
35
+ labelMap.current = callback(labelMap.current);
32
36
  });
33
37
  const itemMetaLookup = (0, _store.useStore)(store, _useTreeViewItems.itemsSelectors.itemMetaLookup);
34
38
  React.useEffect(() => {
35
39
  if (instance.areItemUpdatesPrevented()) {
36
40
  return;
37
41
  }
38
- const newFirstCharMap = {};
42
+ const newLabelMap = {};
39
43
  const processItem = item => {
40
- newFirstCharMap[item.id] = item.label.substring(0, 1).toLowerCase();
44
+ newLabelMap[item.id] = item.label.toLowerCase();
41
45
  };
42
46
  Object.values(itemMetaLookup).forEach(processItem);
43
- firstCharMap.current = newFirstCharMap;
47
+ labelMap.current = newLabelMap;
44
48
  }, [itemMetaLookup, params.getItemId, instance]);
45
- const getFirstMatchingItem = (itemId, query) => {
46
- const cleanQuery = query.toLowerCase();
47
- const getNextItem = itemIdToCheck => {
48
- const nextItemId = (0, _tree.getNextNavigableItem)(store.state, itemIdToCheck);
49
- // We reached the end of the tree, check from the beginning
50
- if (nextItemId === null) {
51
- return (0, _tree.getFirstNavigableItem)(store.state);
52
- }
53
- return nextItemId;
54
- };
49
+ const getNextItem = itemIdToCheck => {
50
+ const nextItemId = (0, _tree.getNextNavigableItem)(store.state, itemIdToCheck);
51
+ // We reached the end of the tree, check from the beginning
52
+ if (nextItemId === null) {
53
+ return (0, _tree.getFirstNavigableItem)(store.state);
54
+ }
55
+ return nextItemId;
56
+ };
57
+ const getNextMatchingItemId = (itemId, query) => {
55
58
  let matchingItemId = null;
56
- let currentItemId = getNextItem(itemId);
57
59
  const checkedItems = {};
60
+ // If query length > 1, first check if current item matches
61
+ let currentItemId = query.length > 1 ? itemId : getNextItem(itemId);
58
62
  // The "!checkedItems[currentItemId]" condition avoids an infinite loop when there is no matching item.
59
63
  while (matchingItemId == null && !checkedItems[currentItemId]) {
60
- if (firstCharMap.current[currentItemId] === cleanQuery) {
64
+ const itemLabel = labelMap.current[currentItemId];
65
+ if (itemLabel?.startsWith(query)) {
61
66
  matchingItemId = currentItemId;
62
67
  } else {
63
68
  checkedItems[currentItemId] = true;
@@ -66,6 +71,26 @@ const useTreeViewKeyboardNavigation = ({
66
71
  }
67
72
  return matchingItemId;
68
73
  };
74
+ const getFirstMatchingItem = (itemId, newKey) => {
75
+ const cleanNewKey = newKey.toLowerCase();
76
+
77
+ // Try matching with accumulated query + new key
78
+ const concatenatedQuery = `${typeaheadQueryRef.current}${cleanNewKey}`;
79
+
80
+ // check if the entire typed query matches an item
81
+ const concatenatedQueryMatchingItemId = getNextMatchingItemId(itemId, concatenatedQuery);
82
+ if (concatenatedQueryMatchingItemId != null) {
83
+ typeaheadQueryRef.current = concatenatedQuery;
84
+ return concatenatedQueryMatchingItemId;
85
+ }
86
+ const newKeyMatchingItemId = getNextMatchingItemId(itemId, cleanNewKey);
87
+ if (newKeyMatchingItemId != null) {
88
+ typeaheadQueryRef.current = cleanNewKey;
89
+ return newKeyMatchingItemId;
90
+ }
91
+ typeaheadQueryRef.current = '';
92
+ return null;
93
+ };
69
94
  const canToggleItemSelection = itemId => _useTreeViewSelection.selectionSelectors.enabled(store.state) && !_useTreeViewItems.itemsSelectors.isItemDisabled(store.state, itemId);
70
95
  const canToggleItemExpansion = itemId => {
71
96
  return !_useTreeViewItems.itemsSelectors.isItemDisabled(store.state, itemId) && _useTreeViewExpansion.expansionSelectors.isItemExpandable(store.state, itemId);
@@ -259,21 +284,26 @@ const useTreeViewKeyboardNavigation = ({
259
284
  }
260
285
 
261
286
  // Type-ahead
262
- // TODO: Support typing multiple characters
263
287
  case !ctrlPressed && !event.shiftKey && isPrintableKey(key):
264
288
  {
289
+ typeaheadTimeout.clear();
265
290
  const matchingItem = getFirstMatchingItem(itemId, key);
266
291
  if (matchingItem != null) {
267
292
  instance.focusItem(event, matchingItem);
268
293
  event.preventDefault();
294
+ } else {
295
+ typeaheadQueryRef.current = '';
269
296
  }
297
+ typeaheadTimeout.start(TYPEAHEAD_TIMEOUT, () => {
298
+ typeaheadQueryRef.current = '';
299
+ });
270
300
  break;
271
301
  }
272
302
  }
273
303
  };
274
304
  return {
275
305
  instance: {
276
- updateFirstCharMap,
306
+ updateLabelMap,
277
307
  handleItemKeyDown
278
308
  }
279
309
  };
@@ -8,12 +8,12 @@ import { UseTreeViewLabelSignature } from "../useTreeViewLabel/index.js";
8
8
  import { TreeViewItemId, TreeViewCancellableEvent } from "../../../models/index.js";
9
9
  export interface UseTreeViewKeyboardNavigationInstance {
10
10
  /**
11
- * Updates the `firstCharMap` to add/remove the first character of some item's labels.
11
+ * Updates the `labelMap` to add/remove the first character of some item's labels.
12
12
  * This map is used to navigate the tree using type-ahead search.
13
13
  * This method is only used by the `useTreeViewJSXItems` plugin, otherwise the updates are handled internally.
14
- * @param {(map: TreeViewFirstCharMap) => TreeViewFirstCharMap} updater The function to update the map.
14
+ * @param {(map: TreeViewLabelMap) => TreeViewLabelMap} updater The function to update the map.
15
15
  */
16
- updateFirstCharMap: (updater: (map: TreeViewFirstCharMap) => TreeViewFirstCharMap) => void;
16
+ updateLabelMap: (updater: (map: TreeViewLabelMap) => TreeViewLabelMap) => void;
17
17
  /**
18
18
  * Callback fired when a key is pressed on an item.
19
19
  * Handles all the keyboard navigation logic.
@@ -27,6 +27,6 @@ export type UseTreeViewKeyboardNavigationSignature = TreeViewPluginSignature<{
27
27
  dependencies: [UseTreeViewItemsSignature, UseTreeViewSelectionSignature, UseTreeViewFocusSignature, UseTreeViewExpansionSignature];
28
28
  optionalDependencies: [UseTreeViewLabelSignature];
29
29
  }>;
30
- export type TreeViewFirstCharMap = {
30
+ export type TreeViewLabelMap = {
31
31
  [itemId: string]: string;
32
32
  };
@@ -44,7 +44,7 @@ const useTreeViewLabel = ({
44
44
  }
45
45
  };
46
46
  (0, _useIsoLayoutEffect.useIsoLayoutEffect)(() => {
47
- store.set('label', (0, _extends2.default)({}, store.state.items, {
47
+ store.set('label', (0, _extends2.default)({}, store.state.label, {
48
48
  isItemEditable: params.isItemEditable
49
49
  }));
50
50
  }, [store, params.isItemEditable]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/x-tree-view",
3
- "version": "8.14.1",
3
+ "version": "8.16.0",
4
4
  "author": "MUI Team",
5
5
  "description": "The community edition of the MUI X Tree View components.",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@
38
38
  "clsx": "^2.1.1",
39
39
  "prop-types": "^15.8.1",
40
40
  "react-transition-group": "^4.4.5",
41
- "@mui/x-internals": "8.14.0"
41
+ "@mui/x-internals": "8.16.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@emotion/react": "^11.9.0",