@mui/x-tree-view 9.10.1 → 9.12.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +1 -11
  3. package/RichTreeView/RichTreeView.js +2 -0
  4. package/RichTreeView/RichTreeView.mjs +2 -0
  5. package/SimpleTreeView/SimpleTreeView.js +1 -0
  6. package/SimpleTreeView/SimpleTreeView.mjs +1 -0
  7. package/TreeItemIcon/TreeItemIcon.js +1 -0
  8. package/TreeItemIcon/TreeItemIcon.mjs +1 -0
  9. package/hooks/useTreeItemUtils/useTreeItemUtils.js +2 -0
  10. package/hooks/useTreeItemUtils/useTreeItemUtils.mjs +2 -0
  11. package/index.js +1 -1
  12. package/index.mjs +1 -1
  13. package/internals/MinimalTreeViewStore/MinimalTreeViewStore.d.mts +1 -0
  14. package/internals/MinimalTreeViewStore/MinimalTreeViewStore.d.ts +1 -0
  15. package/internals/RichTreeViewStore/RichTreeViewStore.d.mts +8 -2
  16. package/internals/RichTreeViewStore/RichTreeViewStore.d.ts +8 -2
  17. package/internals/RichTreeViewStore/RichTreeViewStore.js +5 -2
  18. package/internals/RichTreeViewStore/RichTreeViewStore.mjs +5 -2
  19. package/internals/plugins/items/TreeViewItemsPlugin.d.mts +27 -0
  20. package/internals/plugins/items/TreeViewItemsPlugin.d.ts +27 -0
  21. package/internals/plugins/items/TreeViewItemsPlugin.js +81 -35
  22. package/internals/plugins/items/TreeViewItemsPlugin.mjs +82 -36
  23. package/internals/plugins/items/utils.d.mts +28 -0
  24. package/internals/plugins/items/utils.d.ts +28 -0
  25. package/internals/plugins/items/utils.js +49 -0
  26. package/internals/plugins/items/utils.mjs +48 -0
  27. package/internals/plugins/selection/TreeViewSelectionPlugin.d.mts +15 -0
  28. package/internals/plugins/selection/TreeViewSelectionPlugin.d.ts +15 -0
  29. package/internals/plugins/selection/TreeViewSelectionPlugin.js +50 -10
  30. package/internals/plugins/selection/TreeViewSelectionPlugin.mjs +50 -10
  31. package/internals/plugins/selection/itemPlugin.js +3 -37
  32. package/internals/plugins/selection/itemPlugin.mjs +4 -38
  33. package/internals/plugins/selection/selectors.d.mts +11 -0
  34. package/internals/plugins/selection/selectors.d.ts +11 -0
  35. package/internals/plugins/selection/selectors.js +56 -3
  36. package/internals/plugins/selection/selectors.mjs +56 -3
  37. package/models/items.d.mts +2 -1
  38. package/models/items.d.ts +2 -1
  39. package/package.json +3 -3
  40. package/useTreeItem/useTreeItem.types.d.mts +4 -0
  41. package/useTreeItem/useTreeItem.types.d.ts +4 -0
@@ -34,10 +34,16 @@ export class TreeViewSelectionPlugin {
34
34
  } else {
35
35
  cleanModel = newModel;
36
36
  }
37
+
38
+ // The store is updated before the callbacks are fired,
39
+ // so that the selection selectors and the `getItemSelection` API method
40
+ // return the new selection status when called from `onItemSelectionToggle` or `onSelectedItemsChange`.
41
+ if (selectedItems === undefined) {
42
+ this.store.set('selectedItems', cleanModel);
43
+ }
37
44
  if (onItemSelectionToggle) {
38
45
  if (isMultiSelectEnabled) {
39
46
  const changes = getAddedAndRemovedItems({
40
- store: this.store,
41
47
  newModel: cleanModel,
42
48
  oldModel: oldModel
43
49
  });
@@ -58,9 +64,6 @@ export class TreeViewSelectionPlugin {
58
64
  }
59
65
  }
60
66
  }
61
- if (selectedItems === undefined) {
62
- this.store.set('selectedItems', cleanModel);
63
- }
64
67
  onSelectedItemsChange?.(event, cleanModel);
65
68
  };
66
69
  selectRange = (event, [start, end]) => {
@@ -84,12 +87,39 @@ export class TreeViewSelectionPlugin {
84
87
  this.setSelectedItems(event, newSelectedItems);
85
88
  this.lastSelectedRange = getLookupFromArray(range);
86
89
  };
90
+
91
+ /**
92
+ * Get the selection status of an item.
93
+ * An item that is not selected is `indeterminate` when some of its selectable descendants are selected.
94
+ * @param {TreeViewItemId} itemId The id of the item to get the selection status of.
95
+ * @returns {TreeViewItemSelectionStatus} The selection status of the item.
96
+ */
97
+ getItemSelection = itemId => selectionSelectors.itemSelectionStatus(this.store.state, itemId);
87
98
  buildPublicAPI = () => {
88
99
  return {
100
+ getItemSelection: this.getItemSelection,
89
101
  setItemSelection: this.setItemSelection
90
102
  };
91
103
  };
92
104
 
105
+ /**
106
+ * Select the items added below a selected parent when the selection propagates to the descendants.
107
+ * @param {TreeViewItemId | null} parentId The id of the item the new items were added to.
108
+ * @param {TreeViewItemId[]} newItemIds The ids of the items that were just added.
109
+ */
110
+ propagateSelectionToNewItems = (parentId, newItemIds) => {
111
+ const {
112
+ selectionPropagation = EMPTY_OBJECT
113
+ } = this.store.parameters;
114
+ if (parentId == null || newItemIds.length === 0 || !selectionPropagation.descendants || !selectionSelectors.isMultiSelectEnabled(this.store.state) || !selectionSelectors.isItemSelected(this.store.state, parentId)) {
115
+ return;
116
+ }
117
+
118
+ // Only propagate to the new items, the rest of the parent's subtree is already up to date.
119
+ const newModel = selectionSelectors.selectedItems(this.store.state).concat(newItemIds);
120
+ this.setSelectedItems(null, newModel, newItemIds);
121
+ };
122
+
93
123
  /**
94
124
  * Select or deselect an item.
95
125
  * @param {object} parameters The parameters of the method.
@@ -109,7 +139,7 @@ export class TreeViewSelectionPlugin {
109
139
  }
110
140
  let newSelected;
111
141
  const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(this.store.state);
112
- if (keepExistingSelection) {
142
+ if (keepExistingSelection && isMultiSelectEnabled) {
113
143
  const oldSelected = selectionSelectors.selectedItems(this.store.state);
114
144
  const isSelectedBefore = selectionSelectors.isItemSelected(this.store.state, itemId);
115
145
  if (isSelectedBefore && (shouldBeSelected === false || shouldBeSelected == null)) {
@@ -192,7 +222,9 @@ export class TreeViewSelectionPlugin {
192
222
  }
193
223
  let newSelectedItems = selectionSelectors.selectedItems(this.store.state).slice();
194
224
  if (Object.keys(this.lastSelectedRange).length === 0) {
195
- newSelectedItems.push(nextItem);
225
+ if (!selectionSelectors.isItemSelected(this.store.state, nextItem)) {
226
+ newSelectedItems.push(nextItem);
227
+ }
196
228
  this.lastSelectedRange = {
197
229
  [currentItem]: true,
198
230
  [nextItem]: true
@@ -205,7 +237,9 @@ export class TreeViewSelectionPlugin {
205
237
  newSelectedItems = newSelectedItems.filter(id => id !== currentItem);
206
238
  delete this.lastSelectedRange[currentItem];
207
239
  } else {
208
- newSelectedItems.push(nextItem);
240
+ if (!selectionSelectors.isItemSelected(this.store.state, nextItem)) {
241
+ newSelectedItems.push(nextItem);
242
+ }
209
243
  this.lastSelectedRange[nextItem] = true;
210
244
  }
211
245
  }
@@ -225,7 +259,6 @@ function propagateSelection({
225
259
  let shouldRegenerateModel = false;
226
260
  const newModelLookup = getLookupFromArray(newModel);
227
261
  const changes = getAddedAndRemovedItems({
228
- store,
229
262
  newModel,
230
263
  oldModel
231
264
  });
@@ -305,8 +338,11 @@ function propagateSelection({
305
338
  });
306
339
  return shouldRegenerateModel ? Object.keys(newModelLookup) : newModel;
307
340
  }
341
+
342
+ // This method only diffs the two models it receives,
343
+ // it must not read the selection from the store,
344
+ // otherwise the result would depend on whether the store has already been updated or not.
308
345
  function getAddedAndRemovedItems({
309
- store,
310
346
  oldModel,
311
347
  newModel
312
348
  }) {
@@ -314,8 +350,12 @@ function getAddedAndRemovedItems({
314
350
  newModel.forEach(id => {
315
351
  newModelMap.set(id, true);
316
352
  });
353
+ const oldModelMap = new Map();
354
+ oldModel.forEach(id => {
355
+ oldModelMap.set(id, true);
356
+ });
317
357
  return {
318
- added: newModel.filter(itemId => !selectionSelectors.isItemSelected(store.state, itemId)),
358
+ added: newModel.filter(itemId => !oldModelMap.has(itemId)),
319
359
  removed: oldModel.filter(itemId => !newModelMap.has(itemId))
320
360
  };
321
361
  }
@@ -8,40 +8,6 @@ var _store = require("@mui/x-internals/store");
8
8
  var _TreeViewProvider = require("../../TreeViewProvider");
9
9
  var _selectors = require("../items/selectors");
10
10
  var _selectors2 = require("./selectors");
11
- const selectorCheckboxSelectionStatus = (0, _store.createSelector)((state, itemId) => {
12
- if (_selectors2.selectionSelectors.isItemSelected(state, itemId)) {
13
- return 'checked';
14
- }
15
- let hasSelectedDescendant = false;
16
- let hasUnSelectedDescendant = false;
17
- const traverseDescendants = itemToTraverseId => {
18
- if (itemToTraverseId !== itemId) {
19
- if (_selectors2.selectionSelectors.canItemBeSelected(state, itemToTraverseId)) {
20
- if (_selectors2.selectionSelectors.isItemSelected(state, itemToTraverseId)) {
21
- hasSelectedDescendant = true;
22
- } else {
23
- hasUnSelectedDescendant = true;
24
- }
25
- }
26
- }
27
- _selectors.itemsSelectors.itemOrderedChildrenIds(state, itemToTraverseId).forEach(traverseDescendants);
28
- };
29
- traverseDescendants(itemId);
30
- const shouldSelectBasedOnDescendants = _selectors2.selectionSelectors.propagationRules(state).parents;
31
- if (shouldSelectBasedOnDescendants) {
32
- if (hasSelectedDescendant && hasUnSelectedDescendant) {
33
- return 'indeterminate';
34
- }
35
- if (hasSelectedDescendant && !hasUnSelectedDescendant) {
36
- return 'checked';
37
- }
38
- return 'empty';
39
- }
40
- if (hasSelectedDescendant) {
41
- return 'indeterminate';
42
- }
43
- return 'empty';
44
- });
45
11
  const useSelectionItemPlugin = ({
46
12
  props
47
13
  }) => {
@@ -56,7 +22,7 @@ const useSelectionItemPlugin = ({
56
22
  const canItemBeSelected = (0, _store.useStore)(store, _selectors2.selectionSelectors.canItemBeSelected, itemId);
57
23
  const isItemDisabled = (0, _store.useStore)(store, _selectors.itemsSelectors.isItemDisabled, itemId);
58
24
  const isItemSelectable = (0, _store.useStore)(store, _selectors2.selectionSelectors.isItemSelectable, itemId);
59
- const selectionStatus = (0, _store.useStore)(store, selectorCheckboxSelectionStatus, itemId);
25
+ const selectionStatus = (0, _store.useStore)(store, _selectors2.selectionSelectors.itemSelectionStatus, itemId);
60
26
 
61
27
  // An item is "inherently not selectable" when disabled or excluded via isItemSelectionDisabled,
62
28
  // regardless of the global disableSelection flag. Such items must not have aria-checked.
@@ -69,7 +35,7 @@ const useSelectionItemPlugin = ({
69
35
  if (isItemInherentlyNotSelectable) {
70
36
  // - if the tree contains nodes that are not selectable, aria-checked is not present on those nodes.
71
37
  ariaChecked = undefined;
72
- } else if (selectionStatus === 'checked') {
38
+ } else if (selectionStatus === 'selected') {
73
39
  // - each selected node has aria-checked set to true.
74
40
  ariaChecked = true;
75
41
  } else if (selectionStatus === 'indeterminate') {
@@ -104,7 +70,7 @@ const useSelectionItemPlugin = ({
104
70
  onChange: handleChange,
105
71
  visible: isCheckboxSelectionEnabled && isFeatureEnabledForItem,
106
72
  disabled: !canItemBeSelected,
107
- checked: selectionStatus === 'checked',
73
+ checked: selectionStatus === 'selected',
108
74
  indeterminate: selectionStatus === 'indeterminate'
109
75
  };
110
76
  }
@@ -1,41 +1,7 @@
1
- import { createSelector, useStore } from '@mui/x-internals/store';
1
+ import { useStore } from '@mui/x-internals/store';
2
2
  import { useTreeViewContext } from "../../TreeViewProvider/index.mjs";
3
3
  import { itemsSelectors } from "../items/selectors.mjs";
4
4
  import { selectionSelectors } from "./selectors.mjs";
5
- const selectorCheckboxSelectionStatus = createSelector((state, itemId) => {
6
- if (selectionSelectors.isItemSelected(state, itemId)) {
7
- return 'checked';
8
- }
9
- let hasSelectedDescendant = false;
10
- let hasUnSelectedDescendant = false;
11
- const traverseDescendants = itemToTraverseId => {
12
- if (itemToTraverseId !== itemId) {
13
- if (selectionSelectors.canItemBeSelected(state, itemToTraverseId)) {
14
- if (selectionSelectors.isItemSelected(state, itemToTraverseId)) {
15
- hasSelectedDescendant = true;
16
- } else {
17
- hasUnSelectedDescendant = true;
18
- }
19
- }
20
- }
21
- itemsSelectors.itemOrderedChildrenIds(state, itemToTraverseId).forEach(traverseDescendants);
22
- };
23
- traverseDescendants(itemId);
24
- const shouldSelectBasedOnDescendants = selectionSelectors.propagationRules(state).parents;
25
- if (shouldSelectBasedOnDescendants) {
26
- if (hasSelectedDescendant && hasUnSelectedDescendant) {
27
- return 'indeterminate';
28
- }
29
- if (hasSelectedDescendant && !hasUnSelectedDescendant) {
30
- return 'checked';
31
- }
32
- return 'empty';
33
- }
34
- if (hasSelectedDescendant) {
35
- return 'indeterminate';
36
- }
37
- return 'empty';
38
- });
39
5
  export const useSelectionItemPlugin = ({
40
6
  props
41
7
  }) => {
@@ -50,7 +16,7 @@ export const useSelectionItemPlugin = ({
50
16
  const canItemBeSelected = useStore(store, selectionSelectors.canItemBeSelected, itemId);
51
17
  const isItemDisabled = useStore(store, itemsSelectors.isItemDisabled, itemId);
52
18
  const isItemSelectable = useStore(store, selectionSelectors.isItemSelectable, itemId);
53
- const selectionStatus = useStore(store, selectorCheckboxSelectionStatus, itemId);
19
+ const selectionStatus = useStore(store, selectionSelectors.itemSelectionStatus, itemId);
54
20
 
55
21
  // An item is "inherently not selectable" when disabled or excluded via isItemSelectionDisabled,
56
22
  // regardless of the global disableSelection flag. Such items must not have aria-checked.
@@ -63,7 +29,7 @@ export const useSelectionItemPlugin = ({
63
29
  if (isItemInherentlyNotSelectable) {
64
30
  // - if the tree contains nodes that are not selectable, aria-checked is not present on those nodes.
65
31
  ariaChecked = undefined;
66
- } else if (selectionStatus === 'checked') {
32
+ } else if (selectionStatus === 'selected') {
67
33
  // - each selected node has aria-checked set to true.
68
34
  ariaChecked = true;
69
35
  } else if (selectionStatus === 'indeterminate') {
@@ -98,7 +64,7 @@ export const useSelectionItemPlugin = ({
98
64
  onChange: handleChange,
99
65
  visible: isCheckboxSelectionEnabled && isFeatureEnabledForItem,
100
66
  disabled: !canItemBeSelected,
101
- checked: selectionStatus === 'checked',
67
+ checked: selectionStatus === 'selected',
102
68
  indeterminate: selectionStatus === 'indeterminate'
103
69
  };
104
70
  }
@@ -1,3 +1,4 @@
1
+ import type { TreeViewItemSelectionStatus } from "../../../models/index.mjs";
1
2
  import type { MinimalTreeViewState } from "../../MinimalTreeViewStore/index.mjs";
2
3
  export declare const selectionSelectors: {
3
4
  /**
@@ -32,6 +33,16 @@ export declare const selectionSelectors: {
32
33
  * Checks whether an item is selected.
33
34
  */
34
35
  isItemSelected: (args_0: MinimalTreeViewState<any, any>, itemId: string) => boolean;
36
+ /**
37
+ * Gets the selection status of an item.
38
+ * An item that is not selected is `indeterminate` when some of its selectable descendants are selected.
39
+ * When `selectionPropagation.parents` is enabled, an item whose selectable descendants are all selected is `selected`.
40
+ */
41
+ itemSelectionStatus: (state: MinimalTreeViewState<any, any>, itemId: string) => TreeViewItemSelectionStatus;
42
+ /**
43
+ * Checks whether an item is indeterminate (it is not selected but some of its selectable descendants are).
44
+ */
45
+ isItemIndeterminate: (args_0: MinimalTreeViewState<any, any>, _itemId: string) => boolean;
35
46
  /**
36
47
  * Checks whether the selection feature is enabled for an item.
37
48
  * Returns `true` when selection is enabled on the Tree View and the item is selectable (even if the item is disabled).
@@ -1,3 +1,4 @@
1
+ import type { TreeViewItemSelectionStatus } from "../../../models/index.js";
1
2
  import type { MinimalTreeViewState } from "../../MinimalTreeViewStore/index.js";
2
3
  export declare const selectionSelectors: {
3
4
  /**
@@ -32,6 +33,16 @@ export declare const selectionSelectors: {
32
33
  * Checks whether an item is selected.
33
34
  */
34
35
  isItemSelected: (args_0: MinimalTreeViewState<any, any>, itemId: string) => boolean;
36
+ /**
37
+ * Gets the selection status of an item.
38
+ * An item that is not selected is `indeterminate` when some of its selectable descendants are selected.
39
+ * When `selectionPropagation.parents` is enabled, an item whose selectable descendants are all selected is `selected`.
40
+ */
41
+ itemSelectionStatus: (state: MinimalTreeViewState<any, any>, itemId: string) => TreeViewItemSelectionStatus;
42
+ /**
43
+ * Checks whether an item is indeterminate (it is not selected but some of its selectable descendants are).
44
+ */
45
+ isItemIndeterminate: (args_0: MinimalTreeViewState<any, any>, _itemId: string) => boolean;
35
46
  /**
36
47
  * Checks whether the selection feature is enabled for an item.
37
48
  * Returns `true` when selection is enabled on the Tree View and the item is selectable (even if the item is disabled).
@@ -23,6 +23,49 @@ const selectedItemsMapSelector = (0, _store.createSelectorMemoized)(selectedItem
23
23
  return selectedItemsMap;
24
24
  });
25
25
  const isItemSelectableSelector = (0, _store.createSelector)((state, itemId) => state.itemMetaLookup[itemId]?.selectable ?? true);
26
+ const isItemSelectedSelector = (0, _store.createSelector)(selectedItemsMapSelector, (selectedItemsMap, itemId) => selectedItemsMap.has(itemId));
27
+ const canItemBeSelectedSelector = (0, _store.createSelector)(_selectors.itemsSelectors.isItemDisabled, isItemSelectableSelector, state => !state.disableSelection, (isItemDisabled, isItemSelectable, isSelectionEnabled, _itemId) => isSelectionEnabled && !isItemDisabled && isItemSelectable);
28
+ const propagationRulesSelector = (0, _store.createSelector)(state => state.selectionPropagation);
29
+ const itemSelectionStatusSelector = (0, _store.createSelector)((state, itemId) => {
30
+ if (isItemSelectedSelector(state, itemId)) {
31
+ return 'selected';
32
+ }
33
+
34
+ // Only the descendants can make an unselected item `indeterminate`, so leaves can be resolved
35
+ // without traversing the tree.
36
+ if (_selectors.itemsSelectors.itemOrderedChildrenIds(state, itemId).length === 0) {
37
+ return 'unselected';
38
+ }
39
+ let hasSelectedDescendant = false;
40
+ let hasUnSelectedDescendant = false;
41
+ const traverseDescendants = itemToTraverseId => {
42
+ if (itemToTraverseId !== itemId) {
43
+ if (canItemBeSelectedSelector(state, itemToTraverseId)) {
44
+ if (isItemSelectedSelector(state, itemToTraverseId)) {
45
+ hasSelectedDescendant = true;
46
+ } else {
47
+ hasUnSelectedDescendant = true;
48
+ }
49
+ }
50
+ }
51
+ _selectors.itemsSelectors.itemOrderedChildrenIds(state, itemToTraverseId).forEach(traverseDescendants);
52
+ };
53
+ traverseDescendants(itemId);
54
+ const shouldSelectBasedOnDescendants = propagationRulesSelector(state).parents;
55
+ if (shouldSelectBasedOnDescendants) {
56
+ if (hasSelectedDescendant && hasUnSelectedDescendant) {
57
+ return 'indeterminate';
58
+ }
59
+ if (hasSelectedDescendant && !hasUnSelectedDescendant) {
60
+ return 'selected';
61
+ }
62
+ return 'unselected';
63
+ }
64
+ if (hasSelectedDescendant) {
65
+ return 'indeterminate';
66
+ }
67
+ return 'unselected';
68
+ });
26
69
  const selectionSelectors = exports.selectionSelectors = {
27
70
  /**
28
71
  * Gets the selected items as provided to the component.
@@ -51,11 +94,21 @@ const selectionSelectors = exports.selectionSelectors = {
51
94
  /**
52
95
  * Gets the selection propagation rules.
53
96
  */
54
- propagationRules: (0, _store.createSelector)(state => state.selectionPropagation),
97
+ propagationRules: propagationRulesSelector,
55
98
  /**
56
99
  * Checks whether an item is selected.
57
100
  */
58
- isItemSelected: (0, _store.createSelector)(selectedItemsMapSelector, (selectedItemsMap, itemId) => selectedItemsMap.has(itemId)),
101
+ isItemSelected: isItemSelectedSelector,
102
+ /**
103
+ * Gets the selection status of an item.
104
+ * An item that is not selected is `indeterminate` when some of its selectable descendants are selected.
105
+ * When `selectionPropagation.parents` is enabled, an item whose selectable descendants are all selected is `selected`.
106
+ */
107
+ itemSelectionStatus: itemSelectionStatusSelector,
108
+ /**
109
+ * Checks whether an item is indeterminate (it is not selected but some of its selectable descendants are).
110
+ */
111
+ isItemIndeterminate: (0, _store.createSelector)(itemSelectionStatusSelector, (selectionStatus, _itemId) => selectionStatus === 'indeterminate'),
59
112
  /**
60
113
  * Checks whether the selection feature is enabled for an item.
61
114
  * Returns `true` when selection is enabled on the Tree View and the item is selectable (even if the item is disabled).
@@ -64,7 +117,7 @@ const selectionSelectors = exports.selectionSelectors = {
64
117
  /**
65
118
  * Checks whether an item can be selected (if selection is enabled, if the item is not disabled, and if the item is selectable).
66
119
  */
67
- canItemBeSelected: (0, _store.createSelector)(_selectors.itemsSelectors.isItemDisabled, isItemSelectableSelector, state => !state.disableSelection, (isItemDisabled, isItemSelectable, isSelectionEnabled, _itemId) => isSelectionEnabled && !isItemDisabled && isItemSelectable),
120
+ canItemBeSelected: canItemBeSelectedSelector,
68
121
  /**
69
122
  * Checks whether an item is selectable based on the `isItemSelectionDisabled` prop.
70
123
  */
@@ -17,6 +17,49 @@ const selectedItemsMapSelector = createSelectorMemoized(selectedItemsSelector, s
17
17
  return selectedItemsMap;
18
18
  });
19
19
  const isItemSelectableSelector = createSelector((state, itemId) => state.itemMetaLookup[itemId]?.selectable ?? true);
20
+ const isItemSelectedSelector = createSelector(selectedItemsMapSelector, (selectedItemsMap, itemId) => selectedItemsMap.has(itemId));
21
+ const canItemBeSelectedSelector = createSelector(itemsSelectors.isItemDisabled, isItemSelectableSelector, state => !state.disableSelection, (isItemDisabled, isItemSelectable, isSelectionEnabled, _itemId) => isSelectionEnabled && !isItemDisabled && isItemSelectable);
22
+ const propagationRulesSelector = createSelector(state => state.selectionPropagation);
23
+ const itemSelectionStatusSelector = createSelector((state, itemId) => {
24
+ if (isItemSelectedSelector(state, itemId)) {
25
+ return 'selected';
26
+ }
27
+
28
+ // Only the descendants can make an unselected item `indeterminate`, so leaves can be resolved
29
+ // without traversing the tree.
30
+ if (itemsSelectors.itemOrderedChildrenIds(state, itemId).length === 0) {
31
+ return 'unselected';
32
+ }
33
+ let hasSelectedDescendant = false;
34
+ let hasUnSelectedDescendant = false;
35
+ const traverseDescendants = itemToTraverseId => {
36
+ if (itemToTraverseId !== itemId) {
37
+ if (canItemBeSelectedSelector(state, itemToTraverseId)) {
38
+ if (isItemSelectedSelector(state, itemToTraverseId)) {
39
+ hasSelectedDescendant = true;
40
+ } else {
41
+ hasUnSelectedDescendant = true;
42
+ }
43
+ }
44
+ }
45
+ itemsSelectors.itemOrderedChildrenIds(state, itemToTraverseId).forEach(traverseDescendants);
46
+ };
47
+ traverseDescendants(itemId);
48
+ const shouldSelectBasedOnDescendants = propagationRulesSelector(state).parents;
49
+ if (shouldSelectBasedOnDescendants) {
50
+ if (hasSelectedDescendant && hasUnSelectedDescendant) {
51
+ return 'indeterminate';
52
+ }
53
+ if (hasSelectedDescendant && !hasUnSelectedDescendant) {
54
+ return 'selected';
55
+ }
56
+ return 'unselected';
57
+ }
58
+ if (hasSelectedDescendant) {
59
+ return 'indeterminate';
60
+ }
61
+ return 'unselected';
62
+ });
20
63
  export const selectionSelectors = {
21
64
  /**
22
65
  * Gets the selected items as provided to the component.
@@ -45,11 +88,21 @@ export const selectionSelectors = {
45
88
  /**
46
89
  * Gets the selection propagation rules.
47
90
  */
48
- propagationRules: createSelector(state => state.selectionPropagation),
91
+ propagationRules: propagationRulesSelector,
49
92
  /**
50
93
  * Checks whether an item is selected.
51
94
  */
52
- isItemSelected: createSelector(selectedItemsMapSelector, (selectedItemsMap, itemId) => selectedItemsMap.has(itemId)),
95
+ isItemSelected: isItemSelectedSelector,
96
+ /**
97
+ * Gets the selection status of an item.
98
+ * An item that is not selected is `indeterminate` when some of its selectable descendants are selected.
99
+ * When `selectionPropagation.parents` is enabled, an item whose selectable descendants are all selected is `selected`.
100
+ */
101
+ itemSelectionStatus: itemSelectionStatusSelector,
102
+ /**
103
+ * Checks whether an item is indeterminate (it is not selected but some of its selectable descendants are).
104
+ */
105
+ isItemIndeterminate: createSelector(itemSelectionStatusSelector, (selectionStatus, _itemId) => selectionStatus === 'indeterminate'),
53
106
  /**
54
107
  * Checks whether the selection feature is enabled for an item.
55
108
  * Returns `true` when selection is enabled on the Tree View and the item is selectable (even if the item is disabled).
@@ -58,7 +111,7 @@ export const selectionSelectors = {
58
111
  /**
59
112
  * Checks whether an item can be selected (if selection is enabled, if the item is not disabled, and if the item is selectable).
60
113
  */
61
- canItemBeSelected: createSelector(itemsSelectors.isItemDisabled, isItemSelectableSelector, state => !state.disableSelection, (isItemDisabled, isItemSelectable, isSelectionEnabled, _itemId) => isSelectionEnabled && !isItemDisabled && isItemSelectable),
114
+ canItemBeSelected: canItemBeSelectedSelector,
62
115
  /**
63
116
  * Checks whether an item is selectable based on the `isItemSelectionDisabled` prop.
64
117
  */
@@ -11,4 +11,5 @@ export type TreeViewItemsReorderingAction = 'reorder-above' | 'reorder-below' |
11
11
  export interface TreeViewSelectionPropagation {
12
12
  descendants?: boolean;
13
13
  parents?: boolean;
14
- }
14
+ }
15
+ export type TreeViewItemSelectionStatus = 'selected' | 'indeterminate' | 'unselected';
package/models/items.d.ts CHANGED
@@ -11,4 +11,5 @@ export type TreeViewItemsReorderingAction = 'reorder-above' | 'reorder-below' |
11
11
  export interface TreeViewSelectionPropagation {
12
12
  descendants?: boolean;
13
13
  parents?: boolean;
14
- }
14
+ }
15
+ export type TreeViewItemSelectionStatus = 'selected' | 'indeterminate' | 'unselected';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/x-tree-view",
3
- "version": "9.10.1",
3
+ "version": "9.12.0",
4
4
  "author": "MUI Team",
5
5
  "description": "The community edition of the MUI X Tree View components.",
6
6
  "license": "MIT",
@@ -33,12 +33,12 @@
33
33
  "dependencies": {
34
34
  "@babel/runtime": "^7.29.7",
35
35
  "@base-ui/utils": "^0.3.1",
36
- "@mui/utils": "^9.2.0",
36
+ "@mui/utils": "^9.3.0",
37
37
  "@types/react-transition-group": "^4.4.12",
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": "^9.10.1"
41
+ "@mui/x-internals": "^9.12.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@emotion/react": "^11.9.0",
@@ -98,6 +98,10 @@ export interface UseTreeItemStatus {
98
98
  expanded: boolean;
99
99
  focused: boolean;
100
100
  selected: boolean;
101
+ /**
102
+ * `true` when the item is not selected but some of its selectable descendants are.
103
+ */
104
+ indeterminate?: boolean;
101
105
  disabled: boolean;
102
106
  editing: boolean;
103
107
  editable: boolean;
@@ -98,6 +98,10 @@ export interface UseTreeItemStatus {
98
98
  expanded: boolean;
99
99
  focused: boolean;
100
100
  selected: boolean;
101
+ /**
102
+ * `true` when the item is not selected but some of its selectable descendants are.
103
+ */
104
+ indeterminate?: boolean;
101
105
  disabled: boolean;
102
106
  editing: boolean;
103
107
  editable: boolean;