([]);
- const toggledItemRef = React.useRef<{ [itemId: string]: boolean }>({});
- const apiRef = useTreeViewApiRef();
-
- const handleItemSelectionToggle = (
- event: React.SyntheticEvent,
- itemId: string,
- isSelected: boolean,
- ) => {
- toggledItemRef.current[itemId] = isSelected;
- };
-
- const handleSelectedItemsChange = (
- event: React.SyntheticEvent,
- newSelectedItems: string[],
- ) => {
- setSelectedItems(newSelectedItems);
-
- // Select / unselect the children of the toggled item
- const itemsToSelect: string[] = [];
- const itemsToUnSelect: { [itemId: string]: boolean } = {};
- Object.entries(toggledItemRef.current).forEach(([itemId, isSelected]) => {
- const item = apiRef.current!.getItem(itemId);
- if (isSelected) {
- itemsToSelect.push(...getItemDescendantsIds(item));
- } else {
- getItemDescendantsIds(item).forEach((descendantId) => {
- itemsToUnSelect[descendantId] = true;
- });
- }
- });
-
- const newSelectedItemsWithChildren = Array.from(
- new Set(
- [...newSelectedItems, ...itemsToSelect].filter(
- (itemId) => !itemsToUnSelect[itemId],
- ),
- ),
- );
-
- setSelectedItems(newSelectedItemsWithChildren);
-
- toggledItemRef.current = {};
- };
-
- return (
-
-
-
- );
-}
diff --git a/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.tsx.preview b/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.tsx.preview
deleted file mode 100644
index 6fa38db9c231b..0000000000000
--- a/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.tsx.preview
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/docs/data/tree-view/rich-tree-view/selection/SelectionPropagation.js b/docs/data/tree-view/rich-tree-view/selection/SelectionPropagation.js
new file mode 100644
index 0000000000000..2c54a087f326b
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/selection/SelectionPropagation.js
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import Checkbox from '@mui/material/Checkbox';
+import Stack from '@mui/material/Stack';
+import Box from '@mui/material/Box';
+import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
+
+import { EMPLOYEES_DATASET } from '../../datasets/employees';
+
+export default function SelectionPropagation() {
+ const [selectionPropagation, setSelectionPropagation] = React.useState({
+ parents: true,
+ descendants: true,
+ });
+
+ return (
+
+
+
+ setSelectionPropagation((prev) => ({
+ ...prev,
+ descendants: event.target.checked,
+ }))
+ }
+ />
+ }
+ label="Auto select descendants"
+ />
+
+ setSelectionPropagation((prev) => ({
+ ...prev,
+ parents: event.target.checked,
+ }))
+ }
+ />
+ }
+ label="Auto select parents"
+ />
+
+
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/rich-tree-view/selection/SelectionPropagation.tsx b/docs/data/tree-view/rich-tree-view/selection/SelectionPropagation.tsx
new file mode 100644
index 0000000000000..c41780b8fb9cc
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/selection/SelectionPropagation.tsx
@@ -0,0 +1,60 @@
+import * as React from 'react';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import Checkbox from '@mui/material/Checkbox';
+import Stack from '@mui/material/Stack';
+import Box from '@mui/material/Box';
+import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
+import { TreeViewSelectionPropagation } from '@mui/x-tree-view/models';
+import { EMPLOYEES_DATASET } from '../../datasets/employees';
+
+export default function SelectionPropagation() {
+ const [selectionPropagation, setSelectionPropagation] =
+ React.useState({
+ parents: true,
+ descendants: true,
+ });
+
+ return (
+
+
+
+ setSelectionPropagation((prev) => ({
+ ...prev,
+ descendants: event.target.checked,
+ }))
+ }
+ />
+ }
+ label="Auto select descendants"
+ />
+
+ setSelectionPropagation((prev) => ({
+ ...prev,
+ parents: event.target.checked,
+ }))
+ }
+ />
+ }
+ label="Auto select parents"
+ />
+
+
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/rich-tree-view/selection/selection.md b/docs/data/tree-view/rich-tree-view/selection/selection.md
index 2175d7ad39787..6012a68e78d60 100644
--- a/docs/data/tree-view/rich-tree-view/selection/selection.md
+++ b/docs/data/tree-view/rich-tree-view/selection/selection.md
@@ -75,25 +75,36 @@ Use the `onItemSelectionToggle` prop if you want to react to an item selection c
{{"demo": "TrackItemSelectionToggle.js"}}
-## Parent / children selection relationship
+## Automatic parents and children selection
-Automatically select an item when all of its children are selected and automatically select all children when the parent is selected.
+By default, selecting a parent item does not select its children. You can override this behavior using the `selectionPropagation` prop.
-:::warning
-This feature isn't implemented yet. It's coming.
+Here's how it's structured:
-👍 Upvote [issue #4821](https://github.com/mui/mui-x/issues/4821) if you want to see it land faster.
+```ts
+type TreeViewSelectionPropagation = {
+ descendants?: boolean; // default: false
+ parents?: boolean; // default: false
+};
+```
-Don't hesitate to leave a comment on the same issue to influence what gets built.
-Especially if you already have a use case for this component,
-or if you are facing a pain point with your current solution.
-:::
+When `selectionPropagation.descendants` is set to `true`.
+
+- Selecting a parent selects all its descendants automatically.
+- Deselecting a parent deselects all its descendants automatically.
-If you cannot wait for the official implementation,
-you can create your own custom solution using the `selectedItems`,
-`onSelectedItemsChange` and `onItemSelectionToggle` props:
+When `selectionPropagation.parents` is set to `true`.
-{{"demo": "ParentChildrenSelectionRelationship.js"}}
+- Selecting all the descendants of a parent selects the parent automatically.
+- Deselecting a descendant of a selected parent deselects the parent automatically.
+
+The example below demonstrates the usage of the `selectionPropagation` prop.
+
+{{"demo": "SelectionPropagation.js", "defaultCodeOpen": false}}
+
+:::warning
+This feature only works when multi selection is enabled using `props.multiSelect`.
+:::
## Imperative API
diff --git a/docs/pages/x/api/tree-view/rich-tree-view-pro.json b/docs/pages/x/api/tree-view/rich-tree-view-pro.json
index fb4b208b9f1de..79efa85605209 100644
--- a/docs/pages/x/api/tree-view/rich-tree-view-pro.json
+++ b/docs/pages/x/api/tree-view/rich-tree-view-pro.json
@@ -134,6 +134,10 @@
}
},
"selectedItems": { "type": { "name": "any" } },
+ "selectionPropagation": {
+ "type": { "name": "shape", "description": "{ descendants?: bool, parents?: bool }" },
+ "default": "{ parents: false, descendants: false }"
+ },
"slotProps": { "type": { "name": "object" }, "default": "{}" },
"slots": {
"type": { "name": "object" },
diff --git a/docs/pages/x/api/tree-view/rich-tree-view.json b/docs/pages/x/api/tree-view/rich-tree-view.json
index 98191cec2fc07..3265653e606e3 100644
--- a/docs/pages/x/api/tree-view/rich-tree-view.json
+++ b/docs/pages/x/api/tree-view/rich-tree-view.json
@@ -109,6 +109,10 @@
}
},
"selectedItems": { "type": { "name": "any" } },
+ "selectionPropagation": {
+ "type": { "name": "shape", "description": "{ descendants?: bool, parents?: bool }" },
+ "default": "{ parents: false, descendants: false }"
+ },
"slotProps": { "type": { "name": "object" }, "default": "{}" },
"slots": {
"type": { "name": "object" },
diff --git a/docs/pages/x/api/tree-view/simple-tree-view.json b/docs/pages/x/api/tree-view/simple-tree-view.json
index b3507fdce74d7..bcf5153e9cb5e 100644
--- a/docs/pages/x/api/tree-view/simple-tree-view.json
+++ b/docs/pages/x/api/tree-view/simple-tree-view.json
@@ -73,6 +73,10 @@
}
},
"selectedItems": { "type": { "name": "any" } },
+ "selectionPropagation": {
+ "type": { "name": "shape", "description": "{ descendants?: bool, parents?: bool }" },
+ "default": "{ parents: false, descendants: false }"
+ },
"slotProps": { "type": { "name": "object" } },
"slots": { "type": { "name": "object" }, "additionalInfo": { "slotsApi": true } },
"sx": {
diff --git a/docs/pages/x/api/tree-view/tree-view.json b/docs/pages/x/api/tree-view/tree-view.json
index dc8fc5e8013fe..c9f0691c2b7b6 100644
--- a/docs/pages/x/api/tree-view/tree-view.json
+++ b/docs/pages/x/api/tree-view/tree-view.json
@@ -73,6 +73,10 @@
}
},
"selectedItems": { "type": { "name": "any" } },
+ "selectionPropagation": {
+ "type": { "name": "shape", "description": "{ descendants?: bool, parents?: bool }" },
+ "default": "{ parents: false, descendants: false }"
+ },
"slotProps": { "type": { "name": "object" } },
"slots": { "type": { "name": "object" }, "additionalInfo": { "slotsApi": true } },
"sx": {
diff --git a/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json b/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json
index d7edf9c18d6d7..b43dd9784328a 100644
--- a/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json
+++ b/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json
@@ -137,6 +137,9 @@
"selectedItems": {
"description": "Selected item ids. (Controlled) When multiSelect
is true this takes an array of strings; when false (default) a string."
},
+ "selectionPropagation": {
+ "description": "When selectionPropagation.descendants
is set to true
.
- Selecting a parent selects all its descendants automatically. - Deselecting a parent deselects all its descendants automatically.
When selectionPropagation.parents
is set to true
.
- Selecting all the descendants of a parent selects the parent automatically. - Deselecting a descendant of a selected parent deselects the parent automatically.
Only works when multiSelect
is true
. On the <SimpleTreeView />, only the expanded items are considered (since the collapsed item are not passed to the Tree View component at all)"
+ },
"slotProps": { "description": "The props used for each component slot." },
"slots": { "description": "Overridable component slots." },
"sx": {
diff --git a/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json b/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json
index feba556e087cb..a2fdbd15c255a 100644
--- a/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json
+++ b/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json
@@ -108,6 +108,9 @@
"selectedItems": {
"description": "Selected item ids. (Controlled) When multiSelect
is true this takes an array of strings; when false (default) a string."
},
+ "selectionPropagation": {
+ "description": "When selectionPropagation.descendants
is set to true
.
- Selecting a parent selects all its descendants automatically. - Deselecting a parent deselects all its descendants automatically.
When selectionPropagation.parents
is set to true
.
- Selecting all the descendants of a parent selects the parent automatically. - Deselecting a descendant of a selected parent deselects the parent automatically.
Only works when multiSelect
is true
. On the <SimpleTreeView />, only the expanded items are considered (since the collapsed item are not passed to the Tree View component at all)"
+ },
"slotProps": { "description": "The props used for each component slot." },
"slots": { "description": "Overridable component slots." },
"sx": {
diff --git a/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json b/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json
index 1d9142ee23a96..86afc50c65dbd 100644
--- a/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json
+++ b/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json
@@ -84,6 +84,9 @@
"selectedItems": {
"description": "Selected item ids. (Controlled) When multiSelect
is true this takes an array of strings; when false (default) a string."
},
+ "selectionPropagation": {
+ "description": "When selectionPropagation.descendants
is set to true
.
- Selecting a parent selects all its descendants automatically. - Deselecting a parent deselects all its descendants automatically.
When selectionPropagation.parents
is set to true
.
- Selecting all the descendants of a parent selects the parent automatically. - Deselecting a descendant of a selected parent deselects the parent automatically.
Only works when multiSelect
is true
. On the <SimpleTreeView />, only the expanded items are considered (since the collapsed item are not passed to the Tree View component at all)"
+ },
"slotProps": { "description": "The props used for each component slot." },
"slots": { "description": "Overridable component slots." },
"sx": {
diff --git a/docs/translations/api-docs/tree-view/tree-view/tree-view.json b/docs/translations/api-docs/tree-view/tree-view/tree-view.json
index 069ba6b9dbc67..7558c542dc419 100644
--- a/docs/translations/api-docs/tree-view/tree-view/tree-view.json
+++ b/docs/translations/api-docs/tree-view/tree-view/tree-view.json
@@ -84,6 +84,9 @@
"selectedItems": {
"description": "Selected item ids. (Controlled) When multiSelect
is true this takes an array of strings; when false (default) a string."
},
+ "selectionPropagation": {
+ "description": "When selectionPropagation.descendants
is set to true
.
- Selecting a parent selects all its descendants automatically. - Deselecting a parent deselects all its descendants automatically.
When selectionPropagation.parents
is set to true
.
- Selecting all the descendants of a parent selects the parent automatically. - Deselecting a descendant of a selected parent deselects the parent automatically.
Only works when multiSelect
is true
. On the <SimpleTreeView />, only the expanded items are considered (since the collapsed item are not passed to the Tree View component at all)"
+ },
"slotProps": { "description": "The props used for each component slot." },
"slots": { "description": "Overridable component slots." },
"sx": {
diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx
index 126919c04ba50..d1cf77859bda1 100644
--- a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx
+++ b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx
@@ -315,6 +315,26 @@ RichTreeViewPro.propTypes = {
* When `multiSelect` is true this takes an array of strings; when false (default) a string.
*/
selectedItems: PropTypes.any,
+ /**
+ * When `selectionPropagation.descendants` is set to `true`.
+ *
+ * - Selecting a parent selects all its descendants automatically.
+ * - Deselecting a parent deselects all its descendants automatically.
+ *
+ * When `selectionPropagation.parents` is set to `true`.
+ *
+ * - Selecting all the descendants of a parent selects the parent automatically.
+ * - Deselecting a descendant of a selected parent deselects the parent automatically.
+ *
+ * Only works when `multiSelect` is `true`.
+ * On the , only the expanded items are considered (since the collapsed item are not passed to the Tree View component at all)
+ *
+ * @default { parents: false, descendants: false }
+ */
+ selectionPropagation: PropTypes.shape({
+ descendants: PropTypes.bool,
+ parents: PropTypes.bool,
+ }),
/**
* The props used for each component slot.
* @default {}
diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx
index cf5ff069ef5b8..ccaccc5bcc78b 100644
--- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx
+++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx
@@ -275,6 +275,26 @@ RichTreeView.propTypes = {
* When `multiSelect` is true this takes an array of strings; when false (default) a string.
*/
selectedItems: PropTypes.any,
+ /**
+ * When `selectionPropagation.descendants` is set to `true`.
+ *
+ * - Selecting a parent selects all its descendants automatically.
+ * - Deselecting a parent deselects all its descendants automatically.
+ *
+ * When `selectionPropagation.parents` is set to `true`.
+ *
+ * - Selecting all the descendants of a parent selects the parent automatically.
+ * - Deselecting a descendant of a selected parent deselects the parent automatically.
+ *
+ * Only works when `multiSelect` is `true`.
+ * On the , only the expanded items are considered (since the collapsed item are not passed to the Tree View component at all)
+ *
+ * @default { parents: false, descendants: false }
+ */
+ selectionPropagation: PropTypes.shape({
+ descendants: PropTypes.bool,
+ parents: PropTypes.bool,
+ }),
/**
* The props used for each component slot.
* @default {}
diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx
index ff7ae43139351..5eb1c66c9cbbd 100644
--- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx
+++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx
@@ -230,6 +230,26 @@ SimpleTreeView.propTypes = {
* When `multiSelect` is true this takes an array of strings; when false (default) a string.
*/
selectedItems: PropTypes.any,
+ /**
+ * When `selectionPropagation.descendants` is set to `true`.
+ *
+ * - Selecting a parent selects all its descendants automatically.
+ * - Deselecting a parent deselects all its descendants automatically.
+ *
+ * When `selectionPropagation.parents` is set to `true`.
+ *
+ * - Selecting all the descendants of a parent selects the parent automatically.
+ * - Deselecting a descendant of a selected parent deselects the parent automatically.
+ *
+ * Only works when `multiSelect` is `true`.
+ * On the , only the expanded items are considered (since the collapsed item are not passed to the Tree View component at all)
+ *
+ * @default { parents: false, descendants: false }
+ */
+ selectionPropagation: PropTypes.shape({
+ descendants: PropTypes.bool,
+ parents: PropTypes.bool,
+ }),
/**
* The props used for each component slot.
*/
diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx
index 4204ae102656d..a53f1c1fd87bb 100644
--- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx
+++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx
@@ -235,6 +235,7 @@ export const TreeItem = React.forwardRef(function TreeItem(
handleExpansion,
handleCancelItemLabelEditing,
handleSaveItemLabel,
+ handleCheckboxSelection,
} = useTreeItemState(itemId);
if (process.env.NODE_ENV !== 'production') {
@@ -413,7 +414,8 @@ export const TreeItem = React.forwardRef(function TreeItem(
> = {
rootRefObject,
contentRefObject,
- interactions: { handleSaveItemLabel, handleCancelItemLabelEditing },
+ interactions: { handleSaveItemLabel, handleCancelItemLabelEditing, handleCheckboxSelection },
+ status: { selected, disabled },
};
const enhancedRootProps =
@@ -436,6 +438,11 @@ export const TreeItem = React.forwardRef(function TreeItem(
...sharedPropsEnhancerParams,
externalEventHandlers: {},
}) ?? {};
+ const { visible: isCheckboxVisible, ...enhancedCheckboxProps } =
+ propsEnhancers.checkbox?.({
+ ...sharedPropsEnhancerParams,
+ externalEventHandlers: {},
+ }) ?? {};
return (
@@ -495,6 +502,7 @@ export const TreeItem = React.forwardRef(function TreeItem(
{...((enhancedLabelInputProps as any).value == null
? {}
: { labelInputProps: enhancedLabelInputProps })}
+ {...(isCheckboxVisible ? { checkboxProps: enhancedCheckboxProps } : {})}
ref={handleContentRef}
/>
{children && (
diff --git a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx
index 15a0d73496799..cbbcaad73ae4e 100644
--- a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx
+++ b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
-import Checkbox from '@mui/material/Checkbox';
+import Checkbox, { CheckboxProps } from '@mui/material/Checkbox';
import { useTreeItemState } from './useTreeItemState';
import {
TreeItem2DragAndDropOverlay,
@@ -61,6 +61,7 @@ export interface TreeItemContentProps extends React.HTMLAttributes
displayIcon?: React.ReactNode;
dragAndDropOverlayProps?: TreeItem2DragAndDropOverlayProps;
labelInputProps?: TreeItem2LabelInputProps;
+ checkboxProps?: CheckboxProps & { visible?: boolean };
}
export type TreeItemContentClassKey = keyof NonNullable;
@@ -84,6 +85,7 @@ const TreeItemContent = React.forwardRef(function TreeItemContent(
onMouseDown,
dragAndDropOverlayProps,
labelInputProps,
+ checkboxProps,
...other
} = props;
@@ -94,11 +96,9 @@ const TreeItemContent = React.forwardRef(function TreeItemContent(
focused,
editing,
editable,
- disableSelection,
checkboxSelection,
handleExpansion,
handleSelection,
- handleCheckboxSelection,
handleContentClick,
preventSelection,
expansionTrigger,
@@ -164,17 +164,9 @@ const TreeItemContent = React.forwardRef(function TreeItemContent(
ref={ref}
>
{icon}
- {checkboxSelection && (
-
+ {checkboxProps && (
+
)}
-
{editing ? (
) : (
@@ -193,6 +185,7 @@ TreeItemContent.propTypes = {
// | These PropTypes are generated from the TypeScript type definitions |
// | To update them edit the TypeScript types and run "pnpm proptypes" |
// ----------------------------------------------------------------------
+ checkboxProps: PropTypes.object,
/**
* Override or extend the styles applied to the component.
*/
diff --git a/packages/x-tree-view/src/TreeItem2/TreeItem2.test.tsx b/packages/x-tree-view/src/TreeItem2/TreeItem2.test.tsx
index 524b3aca824fc..36317d592c2c0 100644
--- a/packages/x-tree-view/src/TreeItem2/TreeItem2.test.tsx
+++ b/packages/x-tree-view/src/TreeItem2/TreeItem2.test.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import { createRenderer } from '@mui/internal-test-utils';
+import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
import { TreeItem2 } from '@mui/x-tree-view/TreeItem2';
import { treeItemClasses as classes } from '@mui/x-tree-view/TreeItem';
import { TreeViewContext } from '@mui/x-tree-view/internals/TreeViewProvider/TreeViewContext';
@@ -26,11 +27,9 @@ describe('', () => {
describeSlotsConformance({
render,
getElement: ({ props, slotName }) => (
-
+
-
+
),
slots: {
label: { className: classes.label },
diff --git a/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx b/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx
index 86a398dee5d4f..4046bed32731d 100644
--- a/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx
+++ b/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx
@@ -170,7 +170,7 @@ export const TreeItem2GroupTransition = styled(Collapse, {
export const TreeItem2Checkbox = styled(
React.forwardRef(
- (props: CheckboxProps & { visible: boolean }, ref: React.Ref) => {
+ (props: CheckboxProps & { visible?: boolean }, ref: React.Ref) => {
const { visible, ...other } = props;
if (!visible) {
return null;
diff --git a/packages/x-tree-view/src/TreeView/TreeView.tsx b/packages/x-tree-view/src/TreeView/TreeView.tsx
index efe8947e4c943..06be3ecba63ab 100644
--- a/packages/x-tree-view/src/TreeView/TreeView.tsx
+++ b/packages/x-tree-view/src/TreeView/TreeView.tsx
@@ -217,6 +217,26 @@ TreeView.propTypes = {
* When `multiSelect` is true this takes an array of strings; when false (default) a string.
*/
selectedItems: PropTypes.any,
+ /**
+ * When `selectionPropagation.descendants` is set to `true`.
+ *
+ * - Selecting a parent selects all its descendants automatically.
+ * - Deselecting a parent deselects all its descendants automatically.
+ *
+ * When `selectionPropagation.parents` is set to `true`.
+ *
+ * - Selecting all the descendants of a parent selects the parent automatically.
+ * - Deselecting a descendant of a selected parent deselects the parent automatically.
+ *
+ * Only works when `multiSelect` is `true`.
+ * On the , only the expanded items are considered (since the collapsed item are not passed to the Tree View component at all)
+ *
+ * @default { parents: false, descendants: false }
+ */
+ selectionPropagation: PropTypes.shape({
+ descendants: PropTypes.bool,
+ parents: PropTypes.bool,
+ }),
/**
* The props used for each component slot.
*/
diff --git a/packages/x-tree-view/src/internals/models/itemPlugin.ts b/packages/x-tree-view/src/internals/models/itemPlugin.ts
index c0858f84d9604..017fa155f28fb 100644
--- a/packages/x-tree-view/src/internals/models/itemPlugin.ts
+++ b/packages/x-tree-view/src/internals/models/itemPlugin.ts
@@ -1,10 +1,12 @@
import * as React from 'react';
import { EventHandlers } from '@mui/utils';
import type {
+ UseTreeItem2CheckboxSlotOwnProps,
UseTreeItem2ContentSlotOwnProps,
UseTreeItem2DragAndDropOverlaySlotOwnProps,
UseTreeItem2LabelInputSlotOwnProps,
UseTreeItem2RootSlotOwnProps,
+ UseTreeItem2Status,
} from '../../useTreeItem2';
import type { UseTreeItem2Interactions } from '../../hooks/useTreeItem2Utils/useTreeItem2Utils';
@@ -12,11 +14,13 @@ export interface TreeViewItemPluginSlotPropsEnhancerParams {
rootRefObject: React.MutableRefObject;
contentRefObject: React.MutableRefObject;
externalEventHandlers: EventHandlers;
- // TODO v9: Remove "Pick" once the old TreeItem is removed.
+ // TODO v8: Remove "Pick" once the old TreeItem is removed.
interactions: Pick<
UseTreeItem2Interactions,
- 'handleSaveItemLabel' | 'handleCancelItemLabelEditing'
+ 'handleSaveItemLabel' | 'handleCancelItemLabelEditing' | 'handleCheckboxSelection'
>;
+ // TODO v8: Remove "Pick" once the old TreeItem is removed.
+ status: Pick;
}
type TreeViewItemPluginSlotPropsEnhancer = (
@@ -28,6 +32,7 @@ export interface TreeViewItemPluginSlotPropsEnhancers {
content?: TreeViewItemPluginSlotPropsEnhancer;
dragAndDropOverlay?: TreeViewItemPluginSlotPropsEnhancer;
labelInput?: TreeViewItemPluginSlotPropsEnhancer;
+ checkbox?: TreeViewItemPluginSlotPropsEnhancer;
}
export interface TreeViewItemPluginResponse {
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.itemPlugin.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.itemPlugin.ts
new file mode 100644
index 0000000000000..b5e8dfed978d7
--- /dev/null
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.itemPlugin.ts
@@ -0,0 +1,115 @@
+import * as React from 'react';
+import type { TreeItem2Props } from '../../../TreeItem2';
+import type { TreeItemProps } from '../../../TreeItem';
+import {
+ TreeViewItemId,
+ TreeViewSelectionPropagation,
+ TreeViewCancellableEvent,
+} from '../../../models';
+import { useTreeViewContext } from '../../TreeViewProvider';
+import { TreeViewInstance, TreeViewItemPlugin } from '../../models';
+import {
+ UseTreeItem2CheckboxSlotPropsFromSelection,
+ UseTreeViewSelectionSignature,
+} from './useTreeViewSelection.types';
+import { UseTreeViewItemsSignature } from '../useTreeViewItems';
+
+function getCheckboxStatus({
+ itemId,
+ instance,
+ selectionPropagation,
+ selected,
+}: {
+ itemId: TreeViewItemId;
+ instance: TreeViewInstance<[UseTreeViewItemsSignature, UseTreeViewSelectionSignature]>;
+ selectionPropagation: TreeViewSelectionPropagation;
+ selected: boolean;
+}) {
+ if (selected) {
+ return {
+ indeterminate: false,
+ checked: true,
+ };
+ }
+
+ const children = instance.getItemOrderedChildrenIds(itemId);
+ if (children.length === 0) {
+ return {
+ indeterminate: false,
+ checked: false,
+ };
+ }
+
+ let hasSelectedDescendant = false;
+ let hasUnSelectedDescendant = false;
+
+ const traverseDescendants = (itemToTraverseId: TreeViewItemId) => {
+ if (itemToTraverseId !== itemId) {
+ if (instance.isItemSelected(itemToTraverseId)) {
+ hasSelectedDescendant = true;
+ } else {
+ hasUnSelectedDescendant = true;
+ }
+ }
+
+ instance.getItemOrderedChildrenIds(itemToTraverseId).forEach(traverseDescendants);
+ };
+
+ traverseDescendants(itemId);
+
+ return {
+ indeterminate:
+ (hasSelectedDescendant && hasUnSelectedDescendant) || (!hasUnSelectedDescendant && !selected),
+ checked: selectionPropagation.parents ? hasSelectedDescendant : selected,
+ };
+}
+
+export const useTreeViewSelectionItemPlugin: TreeViewItemPlugin = ({
+ props,
+}) => {
+ const { itemId } = props;
+
+ const {
+ instance,
+ selection: { disableSelection, checkboxSelection, selectionPropagation },
+ } = useTreeViewContext<[UseTreeViewItemsSignature, UseTreeViewSelectionSignature]>();
+ return {
+ propsEnhancers: {
+ checkbox: ({
+ externalEventHandlers,
+ interactions,
+ status,
+ }): UseTreeItem2CheckboxSlotPropsFromSelection => {
+ const handleChange = (
+ event: React.ChangeEvent & TreeViewCancellableEvent,
+ ) => {
+ externalEventHandlers.onChange?.(event);
+ if (event.defaultMuiPrevented) {
+ return;
+ }
+
+ if (disableSelection || status.disabled) {
+ return;
+ }
+
+ interactions.handleCheckboxSelection(event);
+ };
+
+ const checkboxStatus = getCheckboxStatus({
+ instance,
+ itemId,
+ selectionPropagation,
+ selected: status.selected,
+ });
+
+ return {
+ visible: checkboxSelection,
+ disabled: disableSelection || status.disabled,
+ tabIndex: -1,
+ onChange: handleChange,
+ ...checkboxStatus,
+ };
+ },
+ },
+ };
+};
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx
index 448178c94601e..c9cc2f20d28ca 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx
@@ -689,6 +689,128 @@ describeTreeView<[UseTreeViewSelectionSignature, UseTreeViewExpansionSignature]>
fireEvent.click(view.getItemCheckboxInput('3'), { shiftKey: true });
expect(view.getSelectedTreeItems()).to.deep.equal(['1', '3']);
});
+
+ it('should not select the parent when selecting all the children', () => {
+ const view = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1', children: [{ id: '1.1' }, { id: '1.2' }] }, { id: '2' }],
+ defaultSelectedItems: ['1.2'],
+ defaultExpandedItems: ['1'],
+ });
+
+ fireEvent.click(view.getItemCheckboxInput('1.1'));
+ expect(view.getSelectedTreeItems()).to.deep.equal(['1.1', '1.2']);
+ });
+
+ it('should set the parent checkbox as indeterminate when some children are selected but the parent is not', () => {
+ const view = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1', children: [{ id: '1.1' }, { id: '1.2' }] }, { id: '2' }],
+ defaultSelectedItems: ['1.1'],
+ defaultExpandedItems: ['1'],
+ });
+
+ expect(view.getItemCheckboxInput('1').dataset.indeterminate).to.equal('true');
+ });
+
+ it('should not set the parent checkbox as indeterminate when no child is selected and the parent is not either', () => {
+ const view = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1', children: [{ id: '1.1' }, { id: '1.2' }] }, { id: '2' }],
+ defaultExpandedItems: ['1'],
+ });
+
+ expect(view.getItemCheckboxInput('1').dataset.indeterminate).to.equal('false');
+ });
+ });
+
+ describe('multi selection with selectionPropagation.descendants = true', () => {
+ it('should select all the children when selecting a parent', () => {
+ const view = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1', children: [{ id: '1.1' }, { id: '1.2' }] }],
+ defaultExpandedItems: ['1'],
+ selectionPropagation: { descendants: true },
+ });
+
+ fireEvent.click(view.getItemCheckboxInput('1'));
+ expect(view.getSelectedTreeItems()).to.deep.equal(['1', '1.1', '1.2']);
+ });
+
+ it('should deselect all the children when deselecting a parent', () => {
+ const view = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1', children: [{ id: '1.1' }, { id: '1.2' }] }],
+ defaultSelectedItems: ['1', '1.1', '1.2'],
+ defaultExpandedItems: ['1'],
+ selectionPropagation: { descendants: true },
+ });
+
+ fireEvent.click(view.getItemCheckboxInput('1'));
+ expect(view.getSelectedTreeItems()).to.deep.equal([]);
+ });
+
+ it('should not select the parent when selecting all the children', () => {
+ const view = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1', children: [{ id: '1.1' }, { id: '1.2' }] }],
+ defaultSelectedItems: ['1.2'],
+ defaultExpandedItems: ['1'],
+ selectionPropagation: { descendants: true },
+ });
+
+ fireEvent.click(view.getItemCheckboxInput('1.1'));
+ expect(view.getSelectedTreeItems()).to.deep.equal(['1.1', '1.2']);
+ });
+
+ it('should not unselect the parent when unselecting a children', () => {
+ const view = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1', children: [{ id: '1.1' }, { id: '1.2' }] }],
+ defaultSelectedItems: ['1', '1.1', '1.2'],
+ defaultExpandedItems: ['1'],
+ selectionPropagation: { descendants: true },
+ });
+
+ fireEvent.click(view.getItemCheckboxInput('1.1'));
+ expect(view.getSelectedTreeItems()).to.deep.equal(['1', '1.2']);
+ });
+ });
+
+ describe('multi selection with selectionPropagation.parents = true', () => {
+ it('should select all the parents when selecting a child', () => {
+ const view = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1', children: [{ id: '1.1', children: [{ id: '1.1.1' }] }] }],
+ defaultExpandedItems: ['1', '1.1'],
+ selectionPropagation: { parents: true },
+ });
+
+ fireEvent.click(view.getItemCheckboxInput('1.1.1'));
+ expect(view.getSelectedTreeItems()).to.deep.equal(['1', '1.1', '1.1.1']);
+ });
+
+ it('should deselect all the parents when deselecting a child ', () => {
+ const view = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1', children: [{ id: '1.1', children: [{ id: '1.1.1' }] }] }],
+ defaultSelectedItems: ['1', '1.1', '1.1.1'],
+ defaultExpandedItems: ['1', '1.1'],
+ selectionPropagation: { parents: true },
+ });
+
+ fireEvent.click(view.getItemCheckboxInput('1.1.1'));
+ expect(view.getSelectedTreeItems()).to.deep.equal([]);
+ });
});
});
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts
index c24fad0b6bbe5..e2225d707777b 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts
@@ -12,7 +12,13 @@ import {
UseTreeViewSelectionInstance,
UseTreeViewSelectionSignature,
} from './useTreeViewSelection.types';
-import { convertSelectedItemsToArray, getLookupFromArray } from './useTreeViewSelection.utils';
+import {
+ convertSelectedItemsToArray,
+ propagateSelection,
+ getAddedAndRemovedItems,
+ getLookupFromArray,
+} from './useTreeViewSelection.utils';
+import { useTreeViewSelectionItemPlugin } from './useTreeViewSelection.itemPlugin';
export const useTreeViewSelection: TreeViewPlugin = ({
instance,
@@ -37,39 +43,58 @@ export const useTreeViewSelection: TreeViewPlugin
const setSelectedItems = (
event: React.SyntheticEvent,
- newSelectedItems: typeof params.defaultSelectedItems,
+ newModel: typeof params.defaultSelectedItems,
+ additionalItemsToPropagate?: TreeViewItemId[],
) => {
+ let cleanModel: typeof newModel;
+
+ if (
+ params.multiSelect &&
+ (params.selectionPropagation.descendants || params.selectionPropagation.parents)
+ ) {
+ cleanModel = propagateSelection({
+ instance,
+ selectionPropagation: params.selectionPropagation,
+ newModel: newModel as string[],
+ oldModel: models.selectedItems.value as string[],
+ additionalItemsToPropagate,
+ });
+ } else {
+ cleanModel = newModel;
+ }
+
if (params.onItemSelectionToggle) {
if (params.multiSelect) {
- const addedItems = (newSelectedItems as string[]).filter(
- (itemId) => !instance.isItemSelected(itemId),
- );
- const removedItems = (models.selectedItems.value as string[]).filter(
- (itemId) => !(newSelectedItems as string[]).includes(itemId),
- );
-
- addedItems.forEach((itemId) => {
- params.onItemSelectionToggle!(event, itemId, true);
+ const changes = getAddedAndRemovedItems({
+ instance,
+ newModel: cleanModel as string[],
+ oldModel: models.selectedItems.value as string[],
});
- removedItems.forEach((itemId) => {
- params.onItemSelectionToggle!(event, itemId, false);
- });
- } else if (newSelectedItems !== models.selectedItems.value) {
+ if (params.onItemSelectionToggle) {
+ changes.added.forEach((itemId) => {
+ params.onItemSelectionToggle!(event, itemId, true);
+ });
+
+ changes.removed.forEach((itemId) => {
+ params.onItemSelectionToggle!(event, itemId, false);
+ });
+ }
+ } else if (params.onItemSelectionToggle && cleanModel !== models.selectedItems.value) {
if (models.selectedItems.value != null) {
params.onItemSelectionToggle(event, models.selectedItems.value as string, false);
}
- if (newSelectedItems != null) {
- params.onItemSelectionToggle(event, newSelectedItems as string, true);
+ if (cleanModel != null) {
+ params.onItemSelectionToggle(event, cleanModel as string, true);
}
}
}
if (params.onSelectedItemsChange) {
- params.onSelectedItemsChange(event, newSelectedItems);
+ params.onSelectedItemsChange(event, cleanModel);
}
- models.selectedItems.setControlledValue(newSelectedItems);
+ models.selectedItems.setControlledValue(cleanModel);
};
const isItemSelected = (itemId: string) => selectedItemsMap.has(itemId);
@@ -107,7 +132,13 @@ export const useTreeViewSelection: TreeViewPlugin
}
}
- setSelectedItems(event, newSelected);
+ setSelectedItems(
+ event,
+ newSelected,
+ // If shouldBeSelected === instance.isItemSelect(itemId), we still want to propagate the select.
+ // This is useful when the element is in an indeterminate state.
+ [itemId],
+ );
lastSelectedItem.current = itemId;
lastSelectedRange.current = {};
};
@@ -213,11 +244,14 @@ export const useTreeViewSelection: TreeViewPlugin
multiSelect: params.multiSelect,
checkboxSelection: params.checkboxSelection,
disableSelection: params.disableSelection,
+ selectionPropagation: params.selectionPropagation,
},
},
};
};
+useTreeViewSelection.itemPlugin = useTreeViewSelectionItemPlugin;
+
useTreeViewSelection.models = {
selectedItems: {
getDefaultValue: (params) => params.defaultSelectedItems,
@@ -233,6 +267,7 @@ useTreeViewSelection.getDefaultizedParams = ({ params }) => ({
checkboxSelection: params.checkboxSelection ?? false,
defaultSelectedItems:
params.defaultSelectedItems ?? (params.multiSelect ? DEFAULT_SELECTED_ITEMS : null),
+ selectionPropagation: params.selectionPropagation ?? {},
});
useTreeViewSelection.params = {
@@ -243,4 +278,5 @@ useTreeViewSelection.params = {
selectedItems: true,
onSelectedItemsChange: true,
onItemSelectionToggle: true,
+ selectionPropagation: true,
};
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts
index d70f440eec262..bb573c827b220 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts
@@ -2,6 +2,7 @@ import * as React from 'react';
import type { DefaultizedProps, TreeViewPluginSignature } from '../../models';
import { UseTreeViewItemsSignature } from '../useTreeViewItems';
import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion';
+import { TreeViewSelectionPropagation, TreeViewCancellableEventHandler } from '../../../models';
export interface UseTreeViewSelectionPublicAPI {
/**
@@ -94,6 +95,23 @@ export interface UseTreeViewSelectionParameters, only the expanded items are considered (since the collapsed item are not passed to the Tree View component at all)
+ *
+ * @default { parents: false, descendants: false }
+ */
+ selectionPropagation?: TreeViewSelectionPropagation;
/**
* Callback fired when Tree Items are selected/deselected.
* @param {React.SyntheticEvent} event The DOM event that triggered the change.
@@ -119,13 +137,17 @@ export interface UseTreeViewSelectionParameters = DefaultizedProps<
UseTreeViewSelectionParameters,
- 'disableSelection' | 'defaultSelectedItems' | 'multiSelect' | 'checkboxSelection'
+ | 'disableSelection'
+ | 'defaultSelectedItems'
+ | 'multiSelect'
+ | 'checkboxSelection'
+ | 'selectionPropagation'
>;
interface UseTreeViewSelectionContextValue {
selection: Pick<
UseTreeViewSelectionDefaultizedParameters,
- 'multiSelect' | 'checkboxSelection' | 'disableSelection'
+ 'multiSelect' | 'checkboxSelection' | 'disableSelection' | 'selectionPropagation'
>;
}
@@ -142,3 +164,15 @@ export type UseTreeViewSelectionSignature = TreeViewPluginSignature<{
UseTreeViewItemsSignature,
];
}>;
+
+export interface UseTreeItem2CheckboxSlotPropsFromSelection {
+ visible?: boolean;
+ checked?: boolean;
+ disabled?: boolean;
+ tabIndex?: -1;
+ onChange?: TreeViewCancellableEventHandler>;
+}
+
+declare module '@mui/x-tree-view/useTreeItem2' {
+ interface UseTreeItem2CheckboxSlotOwnProps extends UseTreeItem2CheckboxSlotPropsFromSelection {}
+}
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts
index bb022e13338c6..29913562a4b69 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts
@@ -1,3 +1,8 @@
+import { TreeViewItemId, TreeViewSelectionPropagation } from '../../../models';
+import { TreeViewInstance } from '../../models';
+import { UseTreeViewItemsSignature } from '../useTreeViewItems';
+import { UseTreeViewSelectionSignature } from './useTreeViewSelection.types';
+
/**
* Transform the `selectedItems` model to be an array if it was a string or null.
* @param {string[] | string | null} model The raw model.
@@ -16,9 +21,133 @@ export const convertSelectedItemsToArray = (model: string[] | string | null): st
};
export const getLookupFromArray = (array: string[]) => {
- const lookup: { [itemId: string]: boolean } = {};
+ const lookup: { [itemId: string]: true } = {};
array.forEach((itemId) => {
lookup[itemId] = true;
});
return lookup;
};
+
+export const getAddedAndRemovedItems = ({
+ instance,
+ oldModel,
+ newModel,
+}: {
+ instance: TreeViewInstance<[UseTreeViewSelectionSignature]>;
+ oldModel: TreeViewItemId[];
+ newModel: TreeViewItemId[];
+}) => {
+ const newModelLookup = getLookupFromArray(newModel);
+
+ return {
+ added: newModel.filter((itemId) => !instance.isItemSelected(itemId)),
+ removed: oldModel.filter((itemId) => !newModelLookup[itemId]),
+ };
+};
+
+export const propagateSelection = ({
+ instance,
+ selectionPropagation,
+ newModel,
+ oldModel,
+ additionalItemsToPropagate,
+}: {
+ instance: TreeViewInstance<[UseTreeViewItemsSignature, UseTreeViewSelectionSignature]>;
+ selectionPropagation: TreeViewSelectionPropagation;
+ newModel: TreeViewItemId[];
+ oldModel: TreeViewItemId[];
+ additionalItemsToPropagate?: TreeViewItemId[];
+}): string[] => {
+ if (!selectionPropagation.descendants && !selectionPropagation.parents) {
+ return newModel;
+ }
+
+ let shouldRegenerateModel = false;
+ const newModelLookup = getLookupFromArray(newModel);
+
+ const changes = getAddedAndRemovedItems({
+ instance,
+ newModel,
+ oldModel,
+ });
+
+ additionalItemsToPropagate?.forEach((itemId) => {
+ if (newModelLookup[itemId]) {
+ if (!changes.added.includes(itemId)) {
+ changes.added.push(itemId);
+ }
+ } else if (!changes.removed.includes(itemId)) {
+ changes.removed.push(itemId);
+ }
+ });
+
+ changes.added.forEach((addedItemId) => {
+ if (selectionPropagation.descendants) {
+ const selectDescendants = (itemId: TreeViewItemId) => {
+ if (itemId !== addedItemId) {
+ shouldRegenerateModel = true;
+ newModelLookup[itemId] = true;
+ }
+
+ instance.getItemOrderedChildrenIds(itemId).forEach(selectDescendants);
+ };
+
+ selectDescendants(addedItemId);
+ }
+
+ if (selectionPropagation.parents) {
+ const checkAllDescendantsSelected = (itemId: TreeViewItemId): boolean => {
+ if (!newModelLookup[itemId]) {
+ return false;
+ }
+
+ const children = instance.getItemOrderedChildrenIds(itemId);
+ return children.every(checkAllDescendantsSelected);
+ };
+
+ const selectParents = (itemId: TreeViewItemId) => {
+ const parentId = instance.getItemMeta(itemId).parentId;
+ if (parentId == null) {
+ return;
+ }
+
+ const siblings = instance.getItemOrderedChildrenIds(parentId);
+ if (siblings.every(checkAllDescendantsSelected)) {
+ shouldRegenerateModel = true;
+ newModelLookup[parentId] = true;
+ selectParents(parentId);
+ }
+ };
+ selectParents(addedItemId);
+ }
+ });
+
+ changes.removed.forEach((removedItemId) => {
+ if (selectionPropagation.parents) {
+ let parentId = instance.getItemMeta(removedItemId).parentId;
+ while (parentId != null) {
+ if (newModelLookup[parentId]) {
+ shouldRegenerateModel = true;
+ delete newModelLookup[parentId];
+ }
+
+ parentId = instance.getItemMeta(parentId).parentId;
+ }
+ }
+
+ if (selectionPropagation.descendants) {
+ const deSelectDescendants = (itemId: TreeViewItemId) => {
+ if (itemId !== removedItemId) {
+ shouldRegenerateModel = true;
+ delete newModelLookup[itemId];
+ }
+
+ instance.getItemOrderedChildrenIds(itemId).forEach(deSelectDescendants);
+ };
+
+ deSelectDescendants(removedItemId);
+ }
+ });
+
+ return shouldRegenerateModel ? Object.keys(newModelLookup) : newModel;
+};
diff --git a/packages/x-tree-view/src/models/items.ts b/packages/x-tree-view/src/models/items.ts
index f1ef54da13b9c..0d15e359d2d67 100644
--- a/packages/x-tree-view/src/models/items.ts
+++ b/packages/x-tree-view/src/models/items.ts
@@ -10,3 +10,8 @@ export type TreeViewItemsReorderingAction =
| 'reorder-below'
| 'make-child'
| 'move-to-parent';
+
+export interface TreeViewSelectionPropagation {
+ descendants?: boolean;
+ parents?: boolean;
+}
diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts
index 6eb05172543f9..db8934d6300e9 100644
--- a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts
+++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts
@@ -59,7 +59,7 @@ export const useTreeItem2 = <
const sharedPropsEnhancerParams: Omit<
TreeViewItemPluginSlotPropsEnhancerParams,
'externalEventHandlers'
- > = { rootRefObject, contentRefObject, interactions };
+ > = { rootRefObject, contentRefObject, interactions, status };
const createRootHandleFocus =
(otherHandlers: EventHandlers) =>
@@ -158,21 +158,6 @@ export const useTreeItem2 = <
}
};
- const createCheckboxHandleChange =
- (otherHandlers: EventHandlers) =>
- (event: React.ChangeEvent & TreeViewCancellableEvent) => {
- otherHandlers.onChange?.(event);
- if (event.defaultMuiPrevented) {
- return;
- }
-
- if (disableSelection || status.disabled) {
- return;
- }
-
- interactions.handleCheckboxSelection(event);
- };
-
const createIconContainerHandleClick =
(otherHandlers: EventHandlers) => (event: React.MouseEvent & TreeViewCancellableEvent) => {
otherHandlers.onClick?.(event);
@@ -268,15 +253,21 @@ export const useTreeItem2 = <
): UseTreeItem2CheckboxSlotProps => {
const externalEventHandlers = extractEventHandlers(externalProps);
- return {
+ const props = {
...externalEventHandlers,
- visible: checkboxSelection,
ref: checkboxRef,
- checked: status.selected,
- disabled: disableSelection || status.disabled,
- tabIndex: -1,
...externalProps,
- onChange: createCheckboxHandleChange(externalEventHandlers),
+ };
+
+ const enhancedCheckboxProps =
+ propsEnhancers.checkbox?.({
+ ...sharedPropsEnhancerParams,
+ externalEventHandlers,
+ }) ?? {};
+
+ return {
+ ...props,
+ ...enhancedCheckboxProps,
};
};
@@ -308,10 +299,8 @@ export const useTreeItem2 = <
const enhancedLabelInputProps =
propsEnhancers.labelInput?.({
- rootRefObject,
- contentRefObject,
+ ...sharedPropsEnhancerParams,
externalEventHandlers,
- interactions,
}) ?? {};
return {
diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts
index 3d711b079230f..d154c9d037efb 100644
--- a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts
+++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts
@@ -97,15 +97,13 @@ export interface UseTreeItem2LabelInputSlotOwnProps {}
export type UseTreeItem2LabelInputSlotProps = ExternalProps &
UseTreeItem2LabelInputSlotOwnProps;
-export interface UseTreeItem2CheckboxSlotOwnProps {
- visible: boolean;
- checked: boolean;
- onChange: TreeViewCancellableEventHandler>;
- disabled: boolean;
+export interface UseTreeItem2CheckboxSlotPropsFromUseTreeItem {
ref: React.RefObject;
- tabIndex: -1;
}
+export interface UseTreeItem2CheckboxSlotOwnProps
+ extends UseTreeItem2CheckboxSlotPropsFromUseTreeItem {}
+
export type UseTreeItem2CheckboxSlotProps = ExternalProps &
UseTreeItem2CheckboxSlotOwnProps;
diff --git a/scripts/x-tree-view-pro.exports.json b/scripts/x-tree-view-pro.exports.json
index decec771aa7fd..16fcc0d632504 100644
--- a/scripts/x-tree-view-pro.exports.json
+++ b/scripts/x-tree-view-pro.exports.json
@@ -61,6 +61,7 @@
{ "name": "TreeViewItemId", "kind": "TypeAlias" },
{ "name": "TreeViewItemsReorderingAction", "kind": "TypeAlias" },
{ "name": "TreeViewProps", "kind": "Interface" },
+ { "name": "TreeViewSelectionPropagation", "kind": "Interface" },
{ "name": "TreeViewSlotProps", "kind": "Interface" },
{ "name": "TreeViewSlots", "kind": "Interface" },
{ "name": "unstable_resetCleanupTracking", "kind": "Variable" },
diff --git a/scripts/x-tree-view.exports.json b/scripts/x-tree-view.exports.json
index 67797b594ce19..00c255bc05f51 100644
--- a/scripts/x-tree-view.exports.json
+++ b/scripts/x-tree-view.exports.json
@@ -65,6 +65,7 @@
{ "name": "TreeViewItemId", "kind": "TypeAlias" },
{ "name": "TreeViewItemsReorderingAction", "kind": "TypeAlias" },
{ "name": "TreeViewProps", "kind": "Interface" },
+ { "name": "TreeViewSelectionPropagation", "kind": "Interface" },
{ "name": "TreeViewSlotProps", "kind": "Interface" },
{ "name": "TreeViewSlots", "kind": "Interface" },
{ "name": "unstable_resetCleanupTracking", "kind": "Variable" },
diff --git a/test/utils/tree-view/fakeContextValue.ts b/test/utils/tree-view/fakeContextValue.ts
index eabcda86ac5d4..649fb121c4b2a 100644
--- a/test/utils/tree-view/fakeContextValue.ts
+++ b/test/utils/tree-view/fakeContextValue.ts
@@ -41,6 +41,7 @@ export const getFakeContextValue = (
multiSelect: false,
checkboxSelection: features.checkboxSelection ?? false,
disableSelection: false,
+ selectionPropagation: {},
},
treeId: 'mui-tree-view-1',
rootRef: {