@mui/x-tree-view 9.10.0 → 9.11.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 +224 -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 +2 -1
  14. package/internals/MinimalTreeViewStore/MinimalTreeViewStore.d.ts +2 -1
  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 +66 -17
  30. package/internals/plugins/selection/TreeViewSelectionPlugin.mjs +66 -17
  31. package/internals/plugins/selection/itemPlugin.js +13 -36
  32. package/internals/plugins/selection/itemPlugin.mjs +14 -37
  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
@@ -41,10 +41,16 @@ class TreeViewSelectionPlugin {
41
41
  } else {
42
42
  cleanModel = newModel;
43
43
  }
44
+
45
+ // The store is updated before the callbacks are fired,
46
+ // so that the selection selectors and the `getItemSelection` API method
47
+ // return the new selection status when called from `onItemSelectionToggle` or `onSelectedItemsChange`.
48
+ if (selectedItems === undefined) {
49
+ this.store.set('selectedItems', cleanModel);
50
+ }
44
51
  if (onItemSelectionToggle) {
45
52
  if (isMultiSelectEnabled) {
46
53
  const changes = getAddedAndRemovedItems({
47
- store: this.store,
48
54
  newModel: cleanModel,
49
55
  oldModel: oldModel
50
56
  });
@@ -65,9 +71,6 @@ class TreeViewSelectionPlugin {
65
71
  }
66
72
  }
67
73
  }
68
- if (selectedItems === undefined) {
69
- this.store.set('selectedItems', cleanModel);
70
- }
71
74
  onSelectedItemsChange?.(event, cleanModel);
72
75
  };
73
76
  selectRange = (event, [start, end]) => {
@@ -91,12 +94,39 @@ class TreeViewSelectionPlugin {
91
94
  this.setSelectedItems(event, newSelectedItems);
92
95
  this.lastSelectedRange = getLookupFromArray(range);
93
96
  };
97
+
98
+ /**
99
+ * Get the selection status of an item.
100
+ * An item that is not selected is `indeterminate` when some of its selectable descendants are selected.
101
+ * @param {TreeViewItemId} itemId The id of the item to get the selection status of.
102
+ * @returns {TreeViewItemSelectionStatus} The selection status of the item.
103
+ */
104
+ getItemSelection = itemId => _selectors.selectionSelectors.itemSelectionStatus(this.store.state, itemId);
94
105
  buildPublicAPI = () => {
95
106
  return {
107
+ getItemSelection: this.getItemSelection,
96
108
  setItemSelection: this.setItemSelection
97
109
  };
98
110
  };
99
111
 
112
+ /**
113
+ * Select the items added below a selected parent when the selection propagates to the descendants.
114
+ * @param {TreeViewItemId | null} parentId The id of the item the new items were added to.
115
+ * @param {TreeViewItemId[]} newItemIds The ids of the items that were just added.
116
+ */
117
+ propagateSelectionToNewItems = (parentId, newItemIds) => {
118
+ const {
119
+ selectionPropagation = _empty.EMPTY_OBJECT
120
+ } = this.store.parameters;
121
+ if (parentId == null || newItemIds.length === 0 || !selectionPropagation.descendants || !_selectors.selectionSelectors.isMultiSelectEnabled(this.store.state) || !_selectors.selectionSelectors.isItemSelected(this.store.state, parentId)) {
122
+ return;
123
+ }
124
+
125
+ // Only propagate to the new items, the rest of the parent's subtree is already up to date.
126
+ const newModel = _selectors.selectionSelectors.selectedItems(this.store.state).concat(newItemIds);
127
+ this.setSelectedItems(null, newModel, newItemIds);
128
+ };
129
+
100
130
  /**
101
131
  * Select or deselect an item.
102
132
  * @param {object} parameters The parameters of the method.
@@ -116,7 +146,7 @@ class TreeViewSelectionPlugin {
116
146
  }
117
147
  let newSelected;
118
148
  const isMultiSelectEnabled = _selectors.selectionSelectors.isMultiSelectEnabled(this.store.state);
119
- if (keepExistingSelection) {
149
+ if (keepExistingSelection && isMultiSelectEnabled) {
120
150
  const oldSelected = _selectors.selectionSelectors.selectedItems(this.store.state);
121
151
  const isSelectedBefore = _selectors.selectionSelectors.isItemSelected(this.store.state, itemId);
122
152
  if (isSelectedBefore && (shouldBeSelected === false || shouldBeSelected == null)) {
@@ -199,7 +229,9 @@ class TreeViewSelectionPlugin {
199
229
  }
200
230
  let newSelectedItems = _selectors.selectionSelectors.selectedItems(this.store.state).slice();
201
231
  if (Object.keys(this.lastSelectedRange).length === 0) {
202
- newSelectedItems.push(nextItem);
232
+ if (!_selectors.selectionSelectors.isItemSelected(this.store.state, nextItem)) {
233
+ newSelectedItems.push(nextItem);
234
+ }
203
235
  this.lastSelectedRange = {
204
236
  [currentItem]: true,
205
237
  [nextItem]: true
@@ -212,7 +244,9 @@ class TreeViewSelectionPlugin {
212
244
  newSelectedItems = newSelectedItems.filter(id => id !== currentItem);
213
245
  delete this.lastSelectedRange[currentItem];
214
246
  } else {
215
- newSelectedItems.push(nextItem);
247
+ if (!_selectors.selectionSelectors.isItemSelected(this.store.state, nextItem)) {
248
+ newSelectedItems.push(nextItem);
249
+ }
216
250
  this.lastSelectedRange[nextItem] = true;
217
251
  }
218
252
  }
@@ -233,7 +267,6 @@ function propagateSelection({
233
267
  let shouldRegenerateModel = false;
234
268
  const newModelLookup = getLookupFromArray(newModel);
235
269
  const changes = getAddedAndRemovedItems({
236
- store,
237
270
  newModel,
238
271
  oldModel
239
272
  });
@@ -250,20 +283,27 @@ function propagateSelection({
250
283
  if (selectionPropagation.descendants) {
251
284
  const selectDescendants = itemId => {
252
285
  if (itemId !== addedItemId) {
253
- shouldRegenerateModel = true;
254
- newModelLookup[itemId] = true;
286
+ if (_selectors.selectionSelectors.canItemBeSelected(store.state, itemId)) {
287
+ shouldRegenerateModel = true;
288
+ newModelLookup[itemId] = true;
289
+ }
255
290
  }
256
291
  _items.itemsSelectors.itemOrderedChildrenIds(store.state, itemId).forEach(selectDescendants);
257
292
  };
258
293
  selectDescendants(addedItemId);
259
294
  }
260
295
  if (selectionPropagation.parents) {
261
- const checkAllDescendantsSelected = itemId => {
296
+ const checkAllSelectableDescendantsSelected = itemId => {
297
+ if (!_selectors.selectionSelectors.canItemBeSelected(store.state, itemId)) {
298
+ // Non-selectable items don't count; still recurse for isItemSelectionDisabled case
299
+ const children = _items.itemsSelectors.itemOrderedChildrenIds(store.state, itemId);
300
+ return children.every(checkAllSelectableDescendantsSelected);
301
+ }
262
302
  if (!newModelLookup[itemId]) {
263
303
  return false;
264
304
  }
265
305
  const children = _items.itemsSelectors.itemOrderedChildrenIds(store.state, itemId);
266
- return children.every(checkAllDescendantsSelected);
306
+ return children.every(checkAllSelectableDescendantsSelected);
267
307
  };
268
308
  const selectParents = itemId => {
269
309
  const parentId = _items.itemsSelectors.itemParentId(store.state, itemId);
@@ -271,9 +311,11 @@ function propagateSelection({
271
311
  return;
272
312
  }
273
313
  const siblings = _items.itemsSelectors.itemOrderedChildrenIds(store.state, parentId);
274
- if (siblings.every(checkAllDescendantsSelected)) {
275
- shouldRegenerateModel = true;
276
- newModelLookup[parentId] = true;
314
+ if (siblings.every(checkAllSelectableDescendantsSelected)) {
315
+ if (_selectors.selectionSelectors.canItemBeSelected(store.state, parentId)) {
316
+ shouldRegenerateModel = true;
317
+ newModelLookup[parentId] = true;
318
+ }
277
319
  selectParents(parentId);
278
320
  }
279
321
  };
@@ -304,8 +346,11 @@ function propagateSelection({
304
346
  });
305
347
  return shouldRegenerateModel ? Object.keys(newModelLookup) : newModel;
306
348
  }
349
+
350
+ // This method only diffs the two models it receives,
351
+ // it must not read the selection from the store,
352
+ // otherwise the result would depend on whether the store has already been updated or not.
307
353
  function getAddedAndRemovedItems({
308
- store,
309
354
  oldModel,
310
355
  newModel
311
356
  }) {
@@ -313,8 +358,12 @@ function getAddedAndRemovedItems({
313
358
  newModel.forEach(id => {
314
359
  newModelMap.set(id, true);
315
360
  });
361
+ const oldModelMap = new Map();
362
+ oldModel.forEach(id => {
363
+ oldModelMap.set(id, true);
364
+ });
316
365
  return {
317
- added: newModel.filter(itemId => !_selectors.selectionSelectors.isItemSelected(store.state, itemId)),
366
+ added: newModel.filter(itemId => !oldModelMap.has(itemId)),
318
367
  removed: oldModel.filter(itemId => !newModelMap.has(itemId))
319
368
  };
320
369
  }
@@ -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
  });
@@ -242,20 +275,27 @@ function propagateSelection({
242
275
  if (selectionPropagation.descendants) {
243
276
  const selectDescendants = itemId => {
244
277
  if (itemId !== addedItemId) {
245
- shouldRegenerateModel = true;
246
- newModelLookup[itemId] = true;
278
+ if (selectionSelectors.canItemBeSelected(store.state, itemId)) {
279
+ shouldRegenerateModel = true;
280
+ newModelLookup[itemId] = true;
281
+ }
247
282
  }
248
283
  itemsSelectors.itemOrderedChildrenIds(store.state, itemId).forEach(selectDescendants);
249
284
  };
250
285
  selectDescendants(addedItemId);
251
286
  }
252
287
  if (selectionPropagation.parents) {
253
- const checkAllDescendantsSelected = itemId => {
288
+ const checkAllSelectableDescendantsSelected = itemId => {
289
+ if (!selectionSelectors.canItemBeSelected(store.state, itemId)) {
290
+ // Non-selectable items don't count; still recurse for isItemSelectionDisabled case
291
+ const children = itemsSelectors.itemOrderedChildrenIds(store.state, itemId);
292
+ return children.every(checkAllSelectableDescendantsSelected);
293
+ }
254
294
  if (!newModelLookup[itemId]) {
255
295
  return false;
256
296
  }
257
297
  const children = itemsSelectors.itemOrderedChildrenIds(store.state, itemId);
258
- return children.every(checkAllDescendantsSelected);
298
+ return children.every(checkAllSelectableDescendantsSelected);
259
299
  };
260
300
  const selectParents = itemId => {
261
301
  const parentId = itemsSelectors.itemParentId(store.state, itemId);
@@ -263,9 +303,11 @@ function propagateSelection({
263
303
  return;
264
304
  }
265
305
  const siblings = itemsSelectors.itemOrderedChildrenIds(store.state, parentId);
266
- if (siblings.every(checkAllDescendantsSelected)) {
267
- shouldRegenerateModel = true;
268
- newModelLookup[parentId] = true;
306
+ if (siblings.every(checkAllSelectableDescendantsSelected)) {
307
+ if (selectionSelectors.canItemBeSelected(store.state, parentId)) {
308
+ shouldRegenerateModel = true;
309
+ newModelLookup[parentId] = true;
310
+ }
269
311
  selectParents(parentId);
270
312
  }
271
313
  };
@@ -296,8 +338,11 @@ function propagateSelection({
296
338
  });
297
339
  return shouldRegenerateModel ? Object.keys(newModelLookup) : newModel;
298
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.
299
345
  function getAddedAndRemovedItems({
300
- store,
301
346
  oldModel,
302
347
  newModel
303
348
  }) {
@@ -305,8 +350,12 @@ function getAddedAndRemovedItems({
305
350
  newModel.forEach(id => {
306
351
  newModelMap.set(id, true);
307
352
  });
353
+ const oldModelMap = new Map();
354
+ oldModel.forEach(id => {
355
+ oldModelMap.set(id, true);
356
+ });
308
357
  return {
309
- added: newModel.filter(itemId => !selectionSelectors.isItemSelected(store.state, itemId)),
358
+ added: newModel.filter(itemId => !oldModelMap.has(itemId)),
310
359
  removed: oldModel.filter(itemId => !newModelMap.has(itemId))
311
360
  };
312
361
  }
@@ -8,38 +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.isItemSelected(state, itemToTraverseId)) {
20
- hasSelectedDescendant = true;
21
- } else {
22
- hasUnSelectedDescendant = true;
23
- }
24
- }
25
- _selectors.itemsSelectors.itemOrderedChildrenIds(state, itemToTraverseId).forEach(traverseDescendants);
26
- };
27
- traverseDescendants(itemId);
28
- const shouldSelectBasedOnDescendants = _selectors2.selectionSelectors.propagationRules(state).parents;
29
- if (shouldSelectBasedOnDescendants) {
30
- if (hasSelectedDescendant && hasUnSelectedDescendant) {
31
- return 'indeterminate';
32
- }
33
- if (hasSelectedDescendant && !hasUnSelectedDescendant) {
34
- return 'checked';
35
- }
36
- return 'empty';
37
- }
38
- if (hasSelectedDescendant) {
39
- return 'indeterminate';
40
- }
41
- return 'empty';
42
- });
43
11
  const useSelectionItemPlugin = ({
44
12
  props
45
13
  }) => {
@@ -52,19 +20,28 @@ const useSelectionItemPlugin = ({
52
20
  const isCheckboxSelectionEnabled = (0, _store.useStore)(store, _selectors2.selectionSelectors.isCheckboxSelectionEnabled);
53
21
  const isFeatureEnabledForItem = (0, _store.useStore)(store, _selectors2.selectionSelectors.isFeatureEnabledForItem, itemId);
54
22
  const canItemBeSelected = (0, _store.useStore)(store, _selectors2.selectionSelectors.canItemBeSelected, itemId);
55
- const selectionStatus = (0, _store.useStore)(store, selectorCheckboxSelectionStatus, itemId);
23
+ const isItemDisabled = (0, _store.useStore)(store, _selectors.itemsSelectors.isItemDisabled, itemId);
24
+ const isItemSelectable = (0, _store.useStore)(store, _selectors2.selectionSelectors.isItemSelectable, itemId);
25
+ const selectionStatus = (0, _store.useStore)(store, _selectors2.selectionSelectors.itemSelectionStatus, itemId);
26
+
27
+ // An item is "inherently not selectable" when disabled or excluded via isItemSelectionDisabled,
28
+ // regardless of the global disableSelection flag. Such items must not have aria-checked.
29
+ const isItemInherentlyNotSelectable = isItemDisabled || !isItemSelectable;
56
30
  return {
57
31
  propsEnhancers: {
58
32
  root: () => {
59
33
  // https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
60
34
  let ariaChecked;
61
- if (selectionStatus === 'checked') {
35
+ if (isItemInherentlyNotSelectable) {
36
+ // - if the tree contains nodes that are not selectable, aria-checked is not present on those nodes.
37
+ ariaChecked = undefined;
38
+ } else if (selectionStatus === 'selected') {
62
39
  // - each selected node has aria-checked set to true.
63
40
  ariaChecked = true;
64
41
  } else if (selectionStatus === 'indeterminate') {
65
42
  ariaChecked = 'mixed';
66
43
  } else if (!canItemBeSelected) {
67
- // - if the tree contains nodes that are not selectable, aria-checked is not present on those nodes.
44
+ // disableSelection=true with an unselected item: aria-checked is not present.
68
45
  ariaChecked = undefined;
69
46
  } else {
70
47
  // - all nodes that are selectable but not selected have aria-checked set to false.
@@ -93,7 +70,7 @@ const useSelectionItemPlugin = ({
93
70
  onChange: handleChange,
94
71
  visible: isCheckboxSelectionEnabled && isFeatureEnabledForItem,
95
72
  disabled: !canItemBeSelected,
96
- checked: selectionStatus === 'checked',
73
+ checked: selectionStatus === 'selected',
97
74
  indeterminate: selectionStatus === 'indeterminate'
98
75
  };
99
76
  }
@@ -1,39 +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.isItemSelected(state, itemToTraverseId)) {
14
- hasSelectedDescendant = true;
15
- } else {
16
- hasUnSelectedDescendant = true;
17
- }
18
- }
19
- itemsSelectors.itemOrderedChildrenIds(state, itemToTraverseId).forEach(traverseDescendants);
20
- };
21
- traverseDescendants(itemId);
22
- const shouldSelectBasedOnDescendants = selectionSelectors.propagationRules(state).parents;
23
- if (shouldSelectBasedOnDescendants) {
24
- if (hasSelectedDescendant && hasUnSelectedDescendant) {
25
- return 'indeterminate';
26
- }
27
- if (hasSelectedDescendant && !hasUnSelectedDescendant) {
28
- return 'checked';
29
- }
30
- return 'empty';
31
- }
32
- if (hasSelectedDescendant) {
33
- return 'indeterminate';
34
- }
35
- return 'empty';
36
- });
37
5
  export const useSelectionItemPlugin = ({
38
6
  props
39
7
  }) => {
@@ -46,19 +14,28 @@ export const useSelectionItemPlugin = ({
46
14
  const isCheckboxSelectionEnabled = useStore(store, selectionSelectors.isCheckboxSelectionEnabled);
47
15
  const isFeatureEnabledForItem = useStore(store, selectionSelectors.isFeatureEnabledForItem, itemId);
48
16
  const canItemBeSelected = useStore(store, selectionSelectors.canItemBeSelected, itemId);
49
- const selectionStatus = useStore(store, selectorCheckboxSelectionStatus, itemId);
17
+ const isItemDisabled = useStore(store, itemsSelectors.isItemDisabled, itemId);
18
+ const isItemSelectable = useStore(store, selectionSelectors.isItemSelectable, itemId);
19
+ const selectionStatus = useStore(store, selectionSelectors.itemSelectionStatus, itemId);
20
+
21
+ // An item is "inherently not selectable" when disabled or excluded via isItemSelectionDisabled,
22
+ // regardless of the global disableSelection flag. Such items must not have aria-checked.
23
+ const isItemInherentlyNotSelectable = isItemDisabled || !isItemSelectable;
50
24
  return {
51
25
  propsEnhancers: {
52
26
  root: () => {
53
27
  // https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
54
28
  let ariaChecked;
55
- if (selectionStatus === 'checked') {
29
+ if (isItemInherentlyNotSelectable) {
30
+ // - if the tree contains nodes that are not selectable, aria-checked is not present on those nodes.
31
+ ariaChecked = undefined;
32
+ } else if (selectionStatus === 'selected') {
56
33
  // - each selected node has aria-checked set to true.
57
34
  ariaChecked = true;
58
35
  } else if (selectionStatus === 'indeterminate') {
59
36
  ariaChecked = 'mixed';
60
37
  } else if (!canItemBeSelected) {
61
- // - if the tree contains nodes that are not selectable, aria-checked is not present on those nodes.
38
+ // disableSelection=true with an unselected item: aria-checked is not present.
62
39
  ariaChecked = undefined;
63
40
  } else {
64
41
  // - all nodes that are selectable but not selected have aria-checked set to false.
@@ -87,7 +64,7 @@ export const useSelectionItemPlugin = ({
87
64
  onChange: handleChange,
88
65
  visible: isCheckboxSelectionEnabled && isFeatureEnabledForItem,
89
66
  disabled: !canItemBeSelected,
90
- checked: selectionStatus === 'checked',
67
+ checked: selectionStatus === 'selected',
91
68
  indeterminate: selectionStatus === 'indeterminate'
92
69
  };
93
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
  */