diff --git a/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.js b/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.js
index d024ef094cc15..24ddeb962f48d 100644
--- a/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.js
+++ b/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.js
@@ -11,6 +11,7 @@ import {
TreeItem2GroupTransition,
TreeItem2Label,
TreeItem2Root,
+ TreeItem2Checkbox,
} from '@mui/x-tree-view/TreeItem2';
import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon';
import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider';
@@ -46,6 +47,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) {
getRootProps,
getContentProps,
getIconContainerProps,
+ getCheckboxProps,
getLabelProps,
getGroupTransitionProps,
status,
@@ -69,6 +71,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) {
>
{label[0]}
+
diff --git a/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.tsx b/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.tsx
index 7651bb7ed31ef..0caea393d6977 100644
--- a/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.tsx
+++ b/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.tsx
@@ -14,6 +14,7 @@ import {
TreeItem2GroupTransition,
TreeItem2Label,
TreeItem2Root,
+ TreeItem2Checkbox,
} from '@mui/x-tree-view/TreeItem2';
import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon';
import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider';
@@ -56,6 +57,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(
getRootProps,
getContentProps,
getIconContainerProps,
+ getCheckboxProps,
getLabelProps,
getGroupTransitionProps,
status,
@@ -79,6 +81,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(
>
{(label as string)[0]}
+
diff --git a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js
index 2ac57640beea8..78192d2b26c21 100644
--- a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js
+++ b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js
@@ -17,6 +17,7 @@ import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
import { treeItemClasses } from '@mui/x-tree-view/TreeItem';
import { unstable_useTreeItem2 as useTreeItem2 } from '@mui/x-tree-view/useTreeItem2';
import {
+ TreeItem2Checkbox,
TreeItem2Content,
TreeItem2IconContainer,
TreeItem2Label,
@@ -211,6 +212,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) {
getRootProps,
getContentProps,
getIconContainerProps,
+ getCheckboxProps,
getLabelProps,
getGroupTransitionProps,
status,
@@ -242,7 +244,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) {
-
+
diff --git a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx
index d12cf981003ac..ef5e8e5384abc 100644
--- a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx
+++ b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx
@@ -20,6 +20,7 @@ import {
UseTreeItem2Parameters,
} from '@mui/x-tree-view/useTreeItem2';
import {
+ TreeItem2Checkbox,
TreeItem2Content,
TreeItem2IconContainer,
TreeItem2Label,
@@ -247,6 +248,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(
getRootProps,
getContentProps,
getIconContainerProps,
+ getCheckboxProps,
getLabelProps,
getGroupTransitionProps,
status,
@@ -278,7 +280,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(
-
+
diff --git a/docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.js b/docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.js
new file mode 100644
index 0000000000000..2ee4cd92bb214
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.js
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import { RichTreeView } from '@mui/x-tree-view';
+
+const MUI_X_PRODUCTS = [
+ {
+ id: 'grid',
+ label: 'Data Grid',
+ children: [
+ { id: 'grid-community', label: '@mui/x-data-grid' },
+ { id: 'grid-pro', label: '@mui/x-data-grid-pro' },
+ { id: 'grid-premium', label: '@mui/x-data-grid-premium' },
+ ],
+ },
+ {
+ id: 'pickers',
+ label: 'Date and Time Pickers',
+ children: [
+ { id: 'pickers-community', label: '@mui/x-date-pickers' },
+ { id: 'pickers-pro', label: '@mui/x-date-pickers-pro' },
+ ],
+ },
+ {
+ id: 'charts',
+ label: 'Charts',
+ children: [{ id: 'charts-community', label: '@mui/x-charts' }],
+ },
+ {
+ id: 'tree-view',
+ label: 'Tree View',
+ children: [{ id: 'tree-view-community', label: '@mui/x-tree-view' }],
+ },
+];
+
+export default function CheckboxMultiSelection() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.tsx b/docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.tsx
new file mode 100644
index 0000000000000..c8803fc153b3c
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.tsx
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import { RichTreeView, TreeViewBaseItem } from '@mui/x-tree-view';
+
+const MUI_X_PRODUCTS: TreeViewBaseItem[] = [
+ {
+ id: 'grid',
+ label: 'Data Grid',
+ children: [
+ { id: 'grid-community', label: '@mui/x-data-grid' },
+ { id: 'grid-pro', label: '@mui/x-data-grid-pro' },
+ { id: 'grid-premium', label: '@mui/x-data-grid-premium' },
+ ],
+ },
+ {
+ id: 'pickers',
+ label: 'Date and Time Pickers',
+ children: [
+ { id: 'pickers-community', label: '@mui/x-date-pickers' },
+ { id: 'pickers-pro', label: '@mui/x-date-pickers-pro' },
+ ],
+ },
+ {
+ id: 'charts',
+ label: 'Charts',
+ children: [{ id: 'charts-community', label: '@mui/x-charts' }],
+ },
+ {
+ id: 'tree-view',
+ label: 'Tree View',
+ children: [{ id: 'tree-view-community', label: '@mui/x-tree-view' }],
+ },
+];
+
+export default function CheckboxMultiSelection() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.tsx.preview b/docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.tsx.preview
new file mode 100644
index 0000000000000..7d1afb61bd4c0
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.tsx.preview
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.js b/docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.js
new file mode 100644
index 0000000000000..17a520a2a7756
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.js
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import { RichTreeView } from '@mui/x-tree-view';
+
+const MUI_X_PRODUCTS = [
+ {
+ id: 'grid',
+ label: 'Data Grid',
+ children: [
+ { id: 'grid-community', label: '@mui/x-data-grid' },
+ { id: 'grid-pro', label: '@mui/x-data-grid-pro' },
+ { id: 'grid-premium', label: '@mui/x-data-grid-premium' },
+ ],
+ },
+ {
+ id: 'pickers',
+ label: 'Date and Time Pickers',
+ children: [
+ { id: 'pickers-community', label: '@mui/x-date-pickers' },
+ { id: 'pickers-pro', label: '@mui/x-date-pickers-pro' },
+ ],
+ },
+ {
+ id: 'charts',
+ label: 'Charts',
+ children: [{ id: 'charts-community', label: '@mui/x-charts' }],
+ },
+ {
+ id: 'tree-view',
+ label: 'Tree View',
+ children: [{ id: 'tree-view-community', label: '@mui/x-tree-view' }],
+ },
+];
+
+export default function CheckboxSelection() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.tsx b/docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.tsx
new file mode 100644
index 0000000000000..28bda421ad18e
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.tsx
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import { RichTreeView, TreeViewBaseItem } from '@mui/x-tree-view';
+
+const MUI_X_PRODUCTS: TreeViewBaseItem[] = [
+ {
+ id: 'grid',
+ label: 'Data Grid',
+ children: [
+ { id: 'grid-community', label: '@mui/x-data-grid' },
+ { id: 'grid-pro', label: '@mui/x-data-grid-pro' },
+ { id: 'grid-premium', label: '@mui/x-data-grid-premium' },
+ ],
+ },
+ {
+ id: 'pickers',
+ label: 'Date and Time Pickers',
+ children: [
+ { id: 'pickers-community', label: '@mui/x-date-pickers' },
+ { id: 'pickers-pro', label: '@mui/x-date-pickers-pro' },
+ ],
+ },
+ {
+ id: 'charts',
+ label: 'Charts',
+ children: [{ id: 'charts-community', label: '@mui/x-charts' }],
+ },
+ {
+ id: 'tree-view',
+ label: 'Tree View',
+ children: [{ id: 'tree-view-community', label: '@mui/x-tree-view' }],
+ },
+];
+
+export default function CheckboxSelection() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.tsx.preview b/docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.tsx.preview
new file mode 100644
index 0000000000000..5498dc5442391
--- /dev/null
+++ b/docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.tsx.preview
@@ -0,0 +1 @@
+
\ No newline at end of file
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 90e60c40dcc41..929d048d53776 100644
--- a/docs/data/tree-view/rich-tree-view/selection/selection.md
+++ b/docs/data/tree-view/rich-tree-view/selection/selection.md
@@ -23,6 +23,16 @@ Use the `disableSelection` prop if you don't want your items to be selectable:
{{"demo": "DisableSelection.js"}}
+## Checkbox selection
+
+To activate checkbox selection set `checkboxSelection={true}`:
+
+{{"demo": "CheckboxSelection.js"}}
+
+This is also compatible with multi selection:
+
+{{"demo": "CheckboxMultiSelection.js"}}
+
## Controlled selection
Use the `selectedItems` prop to control the selected items.
diff --git a/docs/data/tree-view/simple-tree-view/customization/CustomContentTreeView.js b/docs/data/tree-view/simple-tree-view/customization/CustomContentTreeView.js
index 2076a1e87f939..c16dc8ac924a5 100644
--- a/docs/data/tree-view/simple-tree-view/customization/CustomContentTreeView.js
+++ b/docs/data/tree-view/simple-tree-view/customization/CustomContentTreeView.js
@@ -10,6 +10,7 @@ import {
TreeItem2GroupTransition,
TreeItem2Label,
TreeItem2Root,
+ TreeItem2Checkbox,
} from '@mui/x-tree-view/TreeItem2';
import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon';
import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider';
@@ -25,6 +26,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) {
getRootProps,
getContentProps,
getIconContainerProps,
+ getCheckboxProps,
getLabelProps,
getGroupTransitionProps,
status,
@@ -37,6 +39,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) {
+
({
diff --git a/docs/data/tree-view/simple-tree-view/customization/CustomContentTreeView.tsx b/docs/data/tree-view/simple-tree-view/customization/CustomContentTreeView.tsx
index a7e41eb533c37..c4d333d40f08a 100644
--- a/docs/data/tree-view/simple-tree-view/customization/CustomContentTreeView.tsx
+++ b/docs/data/tree-view/simple-tree-view/customization/CustomContentTreeView.tsx
@@ -13,6 +13,7 @@ import {
TreeItem2GroupTransition,
TreeItem2Label,
TreeItem2Root,
+ TreeItem2Checkbox,
} from '@mui/x-tree-view/TreeItem2';
import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon';
import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider';
@@ -35,6 +36,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(
getRootProps,
getContentProps,
getIconContainerProps,
+ getCheckboxProps,
getLabelProps,
getGroupTransitionProps,
status,
@@ -47,6 +49,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(
+
({
diff --git a/docs/data/tree-view/simple-tree-view/selection/CheckboxMultiSelection.js b/docs/data/tree-view/simple-tree-view/selection/CheckboxMultiSelection.js
new file mode 100644
index 0000000000000..4c5cc2874d520
--- /dev/null
+++ b/docs/data/tree-view/simple-tree-view/selection/CheckboxMultiSelection.js
@@ -0,0 +1,28 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
+import { TreeItem } from '@mui/x-tree-view/TreeItem';
+
+export default function CheckboxMultiSelection() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/simple-tree-view/selection/CheckboxMultiSelection.tsx b/docs/data/tree-view/simple-tree-view/selection/CheckboxMultiSelection.tsx
new file mode 100644
index 0000000000000..4c5cc2874d520
--- /dev/null
+++ b/docs/data/tree-view/simple-tree-view/selection/CheckboxMultiSelection.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
+import { TreeItem } from '@mui/x-tree-view/TreeItem';
+
+export default function CheckboxMultiSelection() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/simple-tree-view/selection/CheckboxSelection.js b/docs/data/tree-view/simple-tree-view/selection/CheckboxSelection.js
new file mode 100644
index 0000000000000..1b1d057131b7d
--- /dev/null
+++ b/docs/data/tree-view/simple-tree-view/selection/CheckboxSelection.js
@@ -0,0 +1,28 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
+import { TreeItem } from '@mui/x-tree-view/TreeItem';
+
+export default function CheckboxSelection() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/simple-tree-view/selection/CheckboxSelection.tsx b/docs/data/tree-view/simple-tree-view/selection/CheckboxSelection.tsx
new file mode 100644
index 0000000000000..1b1d057131b7d
--- /dev/null
+++ b/docs/data/tree-view/simple-tree-view/selection/CheckboxSelection.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
+import { TreeItem } from '@mui/x-tree-view/TreeItem';
+
+export default function CheckboxSelection() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/data/tree-view/simple-tree-view/selection/selection.md b/docs/data/tree-view/simple-tree-view/selection/selection.md
index da1aabecfa40b..18337196bec76 100644
--- a/docs/data/tree-view/simple-tree-view/selection/selection.md
+++ b/docs/data/tree-view/simple-tree-view/selection/selection.md
@@ -23,6 +23,16 @@ Use the `disableSelection` prop if you don't want your items to be selectable:
{{"demo": "DisableSelection.js"}}
+## Checkbox selection
+
+To activate checkbox selection set `checkboxSelection={true}`:
+
+{{"demo": "CheckboxSelection.js"}}
+
+This is also compatible with multi selection:
+
+{{"demo": "CheckboxMultiSelection.js"}}
+
## Controlled selection
Use the `selectedItems` prop to control selected Tree View items.
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 fe0bf95c7f4cc..c80f7ce68570a 100644
--- a/docs/pages/x/api/tree-view/rich-tree-view.json
+++ b/docs/pages/x/api/tree-view/rich-tree-view.json
@@ -6,6 +6,7 @@
"description": "{ current?: { focusItem: func, getItem: func, setItemExpansion: func } }"
}
},
+ "checkboxSelection": { "type": { "name": "bool" }, "default": "false" },
"classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
"defaultExpandedItems": {
"type": { "name": "arrayOf", "description": "Array<string>" },
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 8bdc52ded8fa7..f346fcd7a7004 100644
--- a/docs/pages/x/api/tree-view/simple-tree-view.json
+++ b/docs/pages/x/api/tree-view/simple-tree-view.json
@@ -6,6 +6,7 @@
"description": "{ current?: { focusItem: func, getItem: func, setItemExpansion: func } }"
}
},
+ "checkboxSelection": { "type": { "name": "bool" }, "default": "false" },
"children": { "type": { "name": "node" } },
"classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
"defaultExpandedItems": {
diff --git a/docs/pages/x/api/tree-view/tree-item-2.json b/docs/pages/x/api/tree-view/tree-item-2.json
index e15c84a4b0efc..6b9f00accb0b8 100644
--- a/docs/pages/x/api/tree-view/tree-item-2.json
+++ b/docs/pages/x/api/tree-view/tree-item-2.json
@@ -44,6 +44,12 @@
"default": "TreeItem2IconContainer",
"class": "MuiTreeItem2-iconContainer"
},
+ {
+ "name": "checkbox",
+ "description": "The component that renders the item checkbox for selection.",
+ "default": "TreeItem2Checkbox",
+ "class": "MuiTreeItem2-checkbox"
+ },
{
"name": "label",
"description": "The component that renders the item label.",
diff --git a/docs/pages/x/api/tree-view/tree-item.json b/docs/pages/x/api/tree-view/tree-item.json
index 58a3c04e4ff85..b25b49c3e6515 100644
--- a/docs/pages/x/api/tree-view/tree-item.json
+++ b/docs/pages/x/api/tree-view/tree-item.json
@@ -47,6 +47,12 @@
}
],
"classes": [
+ {
+ "key": "checkbox",
+ "className": "MuiTreeItem-checkbox",
+ "description": "Styles applied to the checkbox element.",
+ "isGlobal": false
+ },
{
"key": "content",
"className": "MuiTreeItem-content",
diff --git a/docs/pages/x/api/tree-view/tree-view.json b/docs/pages/x/api/tree-view/tree-view.json
index 40a19d4f2817b..4fd0587a6b119 100644
--- a/docs/pages/x/api/tree-view/tree-view.json
+++ b/docs/pages/x/api/tree-view/tree-view.json
@@ -6,6 +6,7 @@
"description": "{ current?: { focusItem: func, getItem: func, setItemExpansion: func } }"
}
},
+ "checkboxSelection": { "type": { "name": "bool" }, "default": "false" },
"children": { "type": { "name": "node" } },
"classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
"defaultExpandedItems": {
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 8e73109f8685b..1dc23fbbce097 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
@@ -4,6 +4,9 @@
"apiRef": {
"description": "The ref object that allows Tree View manipulation. Can be instantiated with useTreeViewApiRef()
."
},
+ "checkboxSelection": {
+ "description": "If true
, the tree view renders a checkbox at the left of its label that allows selecting it."
+ },
"classes": { "description": "Override or extend the styles applied to the component." },
"defaultExpandedItems": {
"description": "Expanded item ids. Used when the item's expansion is not controlled."
@@ -37,7 +40,7 @@
}
},
"multiSelect": {
- "description": "If true ctrl
and shift
will trigger multiselect."
+ "description": "If true
, ctrl
and shift
will trigger multiselect."
},
"onExpandedItemsChange": {
"description": "Callback fired when tree items are expanded/collapsed.",
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 4a7aca190f6eb..637ca728dab07 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
@@ -4,6 +4,9 @@
"apiRef": {
"description": "The ref object that allows Tree View manipulation. Can be instantiated with useTreeViewApiRef()
."
},
+ "checkboxSelection": {
+ "description": "If true
, the tree view renders a checkbox at the left of its label that allows selecting it."
+ },
"children": { "description": "The content of the component." },
"classes": { "description": "Override or extend the styles applied to the component." },
"defaultExpandedItems": {
@@ -23,7 +26,7 @@
"description": "This prop is used to help implement the accessibility logic. If you don't provide this prop. It falls back to a randomly generated id."
},
"multiSelect": {
- "description": "If true ctrl
and shift
will trigger multiselect."
+ "description": "If true
, ctrl
and shift
will trigger multiselect."
},
"onExpandedItemsChange": {
"description": "Callback fired when tree items are expanded/collapsed.",
diff --git a/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json b/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json
index 43ff882ed936a..47b99c2cf4d8c 100644
--- a/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json
+++ b/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json
@@ -36,6 +36,7 @@
}
},
"slotDescriptions": {
+ "checkbox": "The component that renders the item checkbox for selection.",
"collapseIcon": "The icon used to collapse the item.",
"content": "The component that renders the content of the item. (e.g.: everything related to this item, not to its children).",
"endIcon": "The icon displayed next to an end item.",
diff --git a/docs/translations/api-docs/tree-view/tree-item/tree-item.json b/docs/translations/api-docs/tree-view/tree-item/tree-item.json
index edf4ebca118f6..88c3575d3fb96 100644
--- a/docs/translations/api-docs/tree-view/tree-item/tree-item.json
+++ b/docs/translations/api-docs/tree-view/tree-item/tree-item.json
@@ -21,6 +21,10 @@
}
},
"classDescriptions": {
+ "checkbox": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the checkbox element"
+ },
"content": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the content element"
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 d96dc4b910ffd..907ed9df8c7c3 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
@@ -4,6 +4,9 @@
"apiRef": {
"description": "The ref object that allows Tree View manipulation. Can be instantiated with useTreeViewApiRef()
."
},
+ "checkboxSelection": {
+ "description": "If true
, the tree view renders a checkbox at the left of its label that allows selecting it."
+ },
"children": { "description": "The content of the component." },
"classes": { "description": "Override or extend the styles applied to the component." },
"defaultExpandedItems": {
@@ -23,7 +26,7 @@
"description": "This prop is used to help implement the accessibility logic. If you don't provide this prop. It falls back to a randomly generated id."
},
"multiSelect": {
- "description": "If true ctrl
and shift
will trigger multiselect."
+ "description": "If true
, ctrl
and shift
will trigger multiselect."
},
"onExpandedItemsChange": {
"description": "Callback fired when tree items are expanded/collapsed.",
diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx
index 20320b2a765c7..3e46711dd63e4 100644
--- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx
+++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx
@@ -157,6 +157,11 @@ RichTreeView.propTypes = {
setItemExpansion: PropTypes.func.isRequired,
}),
}),
+ /**
+ * If `true`, the tree view renders a checkbox at the left of its label that allows selecting it.
+ * @default false
+ */
+ checkboxSelection: PropTypes.bool,
/**
* Override or extend the styles applied to the component.
*/
@@ -221,7 +226,7 @@ RichTreeView.propTypes = {
isItemDisabled: PropTypes.func,
items: PropTypes.array.isRequired,
/**
- * If true `ctrl` and `shift` will trigger multiselect.
+ * If `true`, `ctrl` and `shift` will trigger multiselect.
* @default false
*/
multiSelect: PropTypes.bool,
diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx
index 2d8a9ea47c93c..f25515f22472e 100644
--- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx
+++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx
@@ -120,6 +120,11 @@ SimpleTreeView.propTypes = {
setItemExpansion: PropTypes.func.isRequired,
}),
}),
+ /**
+ * If `true`, the tree view renders a checkbox at the left of its label that allows selecting it.
+ * @default false
+ */
+ checkboxSelection: PropTypes.bool,
/**
* The content of the component.
*/
@@ -162,7 +167,7 @@ SimpleTreeView.propTypes = {
*/
id: PropTypes.string,
/**
- * If true `ctrl` and `shift` will trigger multiselect.
+ * If `true`, `ctrl` and `shift` will trigger multiselect.
* @default false
*/
multiSelect: PropTypes.bool,
diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx
index 857b8866ae147..6f28000d20922 100644
--- a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx
+++ b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx
@@ -45,6 +45,8 @@ const TEST_TREE_VIEW_CONTEXT_VALUE: TreeViewContextValue
},
selection: {
multiSelect: false,
+ checkboxSelection: false,
+ disableSelection: false,
},
rootRef: {
current: null,
diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx
index 20781e11f033c..162661e9440ec 100644
--- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx
+++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx
@@ -28,6 +28,7 @@ const useUtilityClasses = (ownerState: TreeItemOwnerState) => {
focused: ['focused'],
disabled: ['disabled'],
iconContainer: ['iconContainer'],
+ checkbox: ['checkbox'],
label: ['label'],
groupTransition: ['groupTransition'],
};
@@ -128,6 +129,9 @@ const StyledTreeItemContent = styled(TreeItemContent, {
position: 'relative',
...theme.typography.body1,
},
+ [`& .${treeItemClasses.checkbox}`]: {
+ padding: 0,
+ },
}));
const TreeItemGroup = styled(Collapse, {
@@ -336,6 +340,7 @@ export const TreeItem = React.forwardRef(function TreeItem(
disabled: classes.disabled,
iconContainer: classes.iconContainer,
label: classes.label,
+ checkbox: classes.checkbox,
}}
label={label}
itemId={itemId}
diff --git a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx
index 467418e30e722..161d3d3afef6a 100644
--- a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx
+++ b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
+import Checkbox from '@mui/material/Checkbox';
import { useTreeItemState } from './useTreeItemState';
export interface TreeItemContentProps extends React.HTMLAttributes {
@@ -23,6 +24,8 @@ export interface TreeItemContentProps extends React.HTMLAttributes
iconContainer: string;
/** Styles applied to the label element. */
label: string;
+ /** Styles applied to the checkbox element. */
+ checkbox: string;
};
/**
* The tree item label.
@@ -73,12 +76,16 @@ const TreeItemContent = React.forwardRef(function TreeItemContent(
expanded,
selected,
focused,
+ disableSelection,
+ checkboxSelection,
handleExpansion,
handleSelection,
+ handleCheckboxSelection,
preventSelection,
} = useTreeItemState(itemId);
const icon = iconProp || expansionIcon || displayIcon;
+ const checkboxRef = React.useRef(null);
const handleMouseDown = (event: React.MouseEvent) => {
preventSelection(event);
@@ -89,8 +96,15 @@ const TreeItemContent = React.forwardRef(function TreeItemContent(
};
const handleClick = (event: React.MouseEvent) => {
+ if (checkboxRef.current?.contains(event.target as HTMLElement)) {
+ return;
+ }
+
handleExpansion(event);
- handleSelection(event);
+
+ if (!checkboxSelection) {
+ handleSelection(event);
+ }
if (onClick) {
onClick(event);
@@ -112,6 +126,17 @@ const TreeItemContent = React.forwardRef(function TreeItemContent(
ref={ref}
>
{icon}
+ {checkboxSelection && (
+
+ )}
+
{label}
);
diff --git a/packages/x-tree-view/src/TreeItem/treeItemClasses.ts b/packages/x-tree-view/src/TreeItem/treeItemClasses.ts
index b5cab9b9719d1..ceff7c2a2ca60 100644
--- a/packages/x-tree-view/src/TreeItem/treeItemClasses.ts
+++ b/packages/x-tree-view/src/TreeItem/treeItemClasses.ts
@@ -20,6 +20,8 @@ export interface TreeItemClasses {
iconContainer: string;
/** Styles applied to the label element. */
label: string;
+ /** Styles applied to the checkbox element. */
+ checkbox: string;
}
export type TreeItemClassKey = keyof TreeItemClasses;
@@ -38,4 +40,5 @@ export const treeItemClasses: TreeItemClasses = generateUtilityClasses('MuiTreeI
'disabled',
'iconContainer',
'label',
+ 'checkbox',
]);
diff --git a/packages/x-tree-view/src/TreeItem/useTreeItemState.ts b/packages/x-tree-view/src/TreeItem/useTreeItemState.ts
index d43fcd4f2c4aa..f429ac981d20b 100644
--- a/packages/x-tree-view/src/TreeItem/useTreeItemState.ts
+++ b/packages/x-tree-view/src/TreeItem/useTreeItemState.ts
@@ -5,7 +5,7 @@ import { DefaultTreeViewPlugins } from '../internals/plugins';
export function useTreeItemState(itemId: string) {
const {
instance,
- selection: { multiSelect },
+ selection: { multiSelect, checkboxSelection, disableSelection },
} = useTreeViewContext();
const expandable = instance.isItemExpandable(itemId);
@@ -29,7 +29,7 @@ export function useTreeItemState(itemId: string) {
}
};
- const handleSelection = (event: React.MouseEvent) => {
+ const handleSelection = (event: React.MouseEvent) => {
if (!disabled) {
if (!focused) {
instance.focusItem(event, itemId);
@@ -44,11 +44,24 @@ export function useTreeItemState(itemId: string) {
instance.selectItem(event, itemId, true);
}
} else {
- instance.selectItem(event, itemId);
+ instance.selectItem(event, itemId, false);
}
}
};
+ const handleCheckboxSelection = (event: React.ChangeEvent) => {
+ if (disableSelection || disabled) {
+ return;
+ }
+
+ const hasShift = (event.nativeEvent as PointerEvent).shiftKey;
+ if (multiSelect && hasShift) {
+ instance.expandSelectionRange(event, itemId);
+ } else {
+ instance.selectItem(event, itemId, multiSelect, event.target.checked);
+ }
+ };
+
const preventSelection = (event: React.MouseEvent) => {
if (event.shiftKey || event.ctrlKey || event.metaKey || disabled) {
// Prevent text selection
@@ -61,8 +74,11 @@ export function useTreeItemState(itemId: string) {
expanded,
selected,
focused,
+ disableSelection,
+ checkboxSelection,
handleExpansion,
handleSelection,
+ handleCheckboxSelection,
preventSelection,
};
}
diff --git a/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx b/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx
index 57450189b31c4..a2b6c825e060d 100644
--- a/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx
+++ b/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx
@@ -4,6 +4,7 @@ import clsx from 'clsx';
import unsupportedProp from '@mui/utils/unsupportedProp';
import { alpha, styled, useThemeProps } from '@mui/material/styles';
import Collapse from '@mui/material/Collapse';
+import MuiCheckbox, { CheckboxProps } from '@mui/material/Checkbox';
import { useSlotProps } from '@mui/base/utils';
import { shouldForwardProp } from '@mui/system';
import composeClasses from '@mui/utils/composeClasses';
@@ -139,6 +140,26 @@ export const TreeItem2GroupTransition = styled(Collapse, {
paddingLeft: 12,
});
+export const TreeItem2Checkbox = styled(
+ React.forwardRef(
+ (props: CheckboxProps & { visible: boolean }, ref: React.Ref) => {
+ const { visible, ...other } = props;
+ if (!visible) {
+ return null;
+ }
+
+ return ;
+ },
+ ),
+ {
+ name: 'MuiTreeItem2',
+ slot: 'Checkbox',
+ overridesResolver: (props, styles) => styles.checkbox,
+ },
+)({
+ padding: 0,
+});
+
const useUtilityClasses = (ownerState: TreeItem2OwnerState) => {
const { classes } = ownerState;
@@ -150,6 +171,7 @@ const useUtilityClasses = (ownerState: TreeItem2OwnerState) => {
focused: ['focused'],
disabled: ['disabled'],
iconContainer: ['iconContainer'],
+ checkbox: ['checkbox'],
label: ['label'],
groupTransition: ['groupTransition'],
};
@@ -183,6 +205,7 @@ export const TreeItem2 = React.forwardRef(function TreeItem2(
getRootProps,
getContentProps,
getIconContainerProps,
+ getCheckboxProps,
getLabelProps,
getGroupTransitionProps,
status,
@@ -246,6 +269,15 @@ export const TreeItem2 = React.forwardRef(function TreeItem2(
className: classes.label,
});
+ const Checkbox: React.ElementType = slots.checkbox ?? TreeItem2Checkbox;
+ const checkboxProps = useSlotProps({
+ elementType: Checkbox,
+ getSlotProps: getCheckboxProps,
+ externalSlotProps: slotProps.checkbox,
+ ownerState: {},
+ className: classes.checkbox,
+ });
+
const GroupTransition: React.ElementType | undefined = slots.groupTransition ?? undefined;
const groupTransitionProps = useSlotProps({
elementType: GroupTransition,
@@ -262,6 +294,7 @@ export const TreeItem2 = React.forwardRef(function TreeItem2(
+
{children && }
diff --git a/packages/x-tree-view/src/TreeItem2/TreeItem2.types.ts b/packages/x-tree-view/src/TreeItem2/TreeItem2.types.ts
index a2bb8a88b2437..0bce978f46b92 100644
--- a/packages/x-tree-view/src/TreeItem2/TreeItem2.types.ts
+++ b/packages/x-tree-view/src/TreeItem2/TreeItem2.types.ts
@@ -26,6 +26,11 @@ export interface TreeItem2Slots extends TreeItem2IconSlots {
* @default TreeItem2IconContainer
*/
iconContainer?: React.ElementType;
+ /**
+ * The component that renders the item checkbox for selection.
+ * @default TreeItem2Checkbox
+ */
+ checkbox?: React.ElementType;
/**
* The component that renders the item label.
* @default TreeItem2Label
@@ -38,6 +43,7 @@ export interface TreeItem2SlotProps extends TreeItem2IconSlotProps {
content?: SlotComponentProps<'div', {}, {}>;
groupTransition?: SlotComponentProps<'div', {}, {}>;
iconContainer?: SlotComponentProps<'div', {}, {}>;
+ checkbox?: SlotComponentProps<'button', {}, {}>;
label?: SlotComponentProps<'div', {}, {}>;
}
diff --git a/packages/x-tree-view/src/TreeItem2/index.ts b/packages/x-tree-view/src/TreeItem2/index.ts
index f8fddaaa0c83e..9bc803abb6c8e 100644
--- a/packages/x-tree-view/src/TreeItem2/index.ts
+++ b/packages/x-tree-view/src/TreeItem2/index.ts
@@ -4,6 +4,7 @@ export {
TreeItem2Content,
TreeItem2IconContainer,
TreeItem2GroupTransition,
+ TreeItem2Checkbox,
TreeItem2Label,
} from './TreeItem2';
export type { TreeItem2Props, TreeItem2Slots, TreeItem2SlotProps } from './TreeItem2.types';
diff --git a/packages/x-tree-view/src/TreeView/TreeView.tsx b/packages/x-tree-view/src/TreeView/TreeView.tsx
index 0bd1b90b6a4e3..1d21ae575b052 100644
--- a/packages/x-tree-view/src/TreeView/TreeView.tsx
+++ b/packages/x-tree-view/src/TreeView/TreeView.tsx
@@ -96,6 +96,11 @@ TreeView.propTypes = {
setItemExpansion: PropTypes.func.isRequired,
}),
}),
+ /**
+ * If `true`, the tree view renders a checkbox at the left of its label that allows selecting it.
+ * @default false
+ */
+ checkboxSelection: PropTypes.bool,
/**
* The content of the component.
*/
@@ -138,7 +143,7 @@ TreeView.propTypes = {
*/
id: PropTypes.string,
/**
- * If true `ctrl` and `shift` will trigger multiselect.
+ * If `true`, `ctrl` and `shift` will trigger multiselect.
* @default false
*/
multiSelect: PropTypes.bool,
diff --git a/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx b/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx
index c8c67baf0f9e1..b76e3fcf96aa4 100644
--- a/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx
+++ b/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx
@@ -6,6 +6,7 @@ import type { UseTreeItem2Status } from '../../useTreeItem2';
interface UseTreeItem2Interactions {
handleExpansion: (event: React.MouseEvent) => void;
handleSelection: (event: React.MouseEvent) => void;
+ handleCheckboxSelection: (event: React.ChangeEvent) => void;
}
interface UseTreeItem2UtilsReturnValue {
@@ -68,11 +69,24 @@ export const useTreeItem2Utils = ({
instance.selectItem(event, itemId, true);
}
} else {
- instance.selectItem(event, itemId);
+ instance.selectItem(event, itemId, false);
}
};
- const interactions: UseTreeItem2Interactions = { handleExpansion, handleSelection };
+ const handleCheckboxSelection = (event: React.ChangeEvent) => {
+ const hasShift = (event.nativeEvent as PointerEvent).shiftKey;
+ if (multiSelect && hasShift) {
+ instance.expandSelectionRange(event, itemId);
+ } else {
+ instance.selectItem(event, itemId, multiSelect, event.target.checked);
+ }
+ };
+
+ const interactions: UseTreeItem2Interactions = {
+ handleExpansion,
+ handleSelection,
+ handleCheckboxSelection,
+ };
return { interactions, status };
};
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts
index 75a7538bc1d1a..b16b3d4577873 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts
@@ -91,7 +91,10 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin<
return;
}
- if (event.altKey || event.currentTarget !== event.target) {
+ if (
+ event.altKey ||
+ event.currentTarget !== (event.target as HTMLElement).closest('*[role="treeitem"]')
+ ) {
return;
}
@@ -108,7 +111,7 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin<
} else if (params.multiSelect) {
instance.selectItem(event, itemId, true);
} else {
- instance.selectItem(event, itemId);
+ instance.selectItem(event, itemId, false);
}
break;
}
@@ -124,7 +127,7 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin<
event.preventDefault();
instance.selectItem(event, itemId, true);
} else if (!instance.isItemSelected(itemId)) {
- instance.selectItem(event, itemId);
+ instance.selectItem(event, itemId, false);
event.preventDefault();
}
}
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 c6d5e885ec2c8..63324c7ed7c39 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
@@ -212,7 +212,7 @@ describeTreeView<[UseTreeViewSelectionSignature]>('useTreeViewSelection plugin',
});
describe('multi selection', () => {
- it('should select un-selected item when clicking on an item content', () => {
+ it('should select un-selected item and remove other selected items when clicking on an item content', () => {
const response = render({
multiSelect: true,
items: [{ id: '1' }, { id: '2' }],
@@ -460,6 +460,324 @@ describeTreeView<[UseTreeViewSelectionSignature]>('useTreeViewSelection plugin',
});
});
+ describe('checkbox interaction', () => {
+ describe('render checkbox when needed', () => {
+ it('should not render a checkbox when checkboxSelection is not defined', () => {
+ const response = render({
+ items: [{ id: '1' }],
+ });
+
+ expect(response.getItemCheckbox('1')).to.equal(null);
+ });
+
+ it('should not render a checkbox when checkboxSelection is false', () => {
+ const response = render({
+ checkboxSelection: false,
+ items: [{ id: '1' }],
+ });
+
+ expect(response.getItemCheckbox('1')).to.equal(null);
+ });
+
+ it('should render a checkbox when checkboxSelection is true', () => {
+ const response = render({
+ checkboxSelection: true,
+ items: [{ id: '1' }],
+ });
+
+ expect(response.getItemCheckbox('1')).not.to.equal(null);
+ });
+ });
+
+ describe('single selection', () => {
+ it('should not change selection when clicking on an item content', () => {
+ const response = render({
+ checkboxSelection: true,
+ items: [{ id: '1' }],
+ });
+
+ expect(response.isItemSelected('1')).to.equal(false);
+
+ fireEvent.click(response.getItemContent('1'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ });
+
+ it('should select un-selected item when clicking on an item checkbox', () => {
+ const response = render({
+ items: [{ id: '1' }, { id: '2' }],
+ checkboxSelection: true,
+ });
+
+ expect(response.isItemSelected('1')).to.equal(false);
+
+ fireEvent.click(response.getItemCheckboxInput('1'));
+ expect(response.isItemSelected('1')).to.equal(true);
+ });
+
+ it('should un-select selected item when clicking on an item checkbox', () => {
+ const response = render({
+ items: [{ id: '1' }, { id: '2' }],
+ defaultSelectedItems: '1',
+ checkboxSelection: true,
+ });
+
+ expect(response.isItemSelected('1')).to.equal(true);
+
+ fireEvent.click(response.getItemCheckboxInput('1'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ });
+
+ it('should not select an item when click and disableSelection', () => {
+ const response = render({
+ items: [{ id: '1' }, { id: '2' }],
+ disableSelection: true,
+ checkboxSelection: true,
+ });
+
+ expect(response.isItemSelected('1')).to.equal(false);
+
+ fireEvent.click(response.getItemCheckboxInput('1'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ });
+
+ it('should not select an item when clicking on a disabled item checkbox', () => {
+ const response = render({
+ items: [{ id: '1', disabled: true }, { id: '2' }],
+ checkboxSelection: true,
+ });
+
+ expect(response.isItemSelected('1')).to.equal(false);
+ fireEvent.click(response.getItemCheckboxInput('1'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ });
+ });
+
+ describe('multi selection', () => {
+ it('should not change selection when clicking on an item content', () => {
+ const response = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1' }],
+ });
+
+ expect(response.isItemSelected('1')).to.equal(false);
+
+ fireEvent.click(response.getItemContent('1'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ });
+
+ it('should select un-selected item and keep other items selected when clicking on an item checkbox', () => {
+ const response = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1' }, { id: '2' }],
+ defaultSelectedItems: ['2'],
+ });
+
+ expect(response.isItemSelected('1')).to.equal(false);
+ expect(response.isItemSelected('2')).to.equal(true);
+
+ fireEvent.click(response.getItemCheckboxInput('1'));
+ expect(response.isItemSelected('1')).to.equal(true);
+ expect(response.isItemSelected('2')).to.equal(true);
+ });
+
+ it('should un-select selected item when clicking on an item checkbox', () => {
+ const response = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1' }, { id: '2' }],
+ defaultSelectedItems: ['1'],
+ });
+
+ expect(response.isItemSelected('1')).to.equal(true);
+
+ fireEvent.click(response.getItemCheckboxInput('1'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ });
+
+ it('should not select an item when click and disableSelection', () => {
+ const response = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1' }, { id: '2' }],
+ disableSelection: true,
+ });
+
+ expect(response.isItemSelected('1')).to.equal(false);
+
+ fireEvent.click(response.getItemCheckboxInput('1'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ });
+
+ it('should not select an item when clicking on a disabled item content', () => {
+ const response = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1', disabled: true }, { id: '2' }],
+ });
+
+ expect(response.isItemSelected('1')).to.equal(false);
+ fireEvent.click(response.getItemCheckboxInput('1'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ });
+
+ it('should expand the selection range when clicking on an item checkbox below the last selected item while holding Shift', () => {
+ const response = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }],
+ });
+
+ fireEvent.click(response.getItemCheckboxInput('2'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ expect(response.isItemSelected('2')).to.equal(true);
+ expect(response.isItemSelected('2.1')).to.equal(false);
+ expect(response.isItemSelected('3')).to.equal(false);
+ expect(response.isItemSelected('4')).to.equal(false);
+
+ fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true });
+ expect(response.isItemSelected('1')).to.equal(false);
+ expect(response.isItemSelected('2')).to.equal(true);
+ expect(response.isItemSelected('2.1')).to.equal(true);
+ expect(response.isItemSelected('3')).to.equal(true);
+ expect(response.isItemSelected('4')).to.equal(false);
+ });
+
+ it('should expand the selection range when clicking on an item checkbox above the last selected item while holding Shift', () => {
+ const response = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }],
+ });
+
+ fireEvent.click(response.getItemCheckboxInput('3'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ expect(response.isItemSelected('2')).to.equal(false);
+ expect(response.isItemSelected('2.1')).to.equal(false);
+ expect(response.isItemSelected('3')).to.equal(true);
+ expect(response.isItemSelected('4')).to.equal(false);
+
+ fireEvent.click(response.getItemCheckboxInput('2'), { shiftKey: true });
+ expect(response.isItemSelected('1')).to.equal(false);
+ expect(response.isItemSelected('2')).to.equal(true);
+ expect(response.isItemSelected('2.1')).to.equal(true);
+ expect(response.isItemSelected('3')).to.equal(true);
+ expect(response.isItemSelected('4')).to.equal(false);
+ });
+
+ it('should expand the selection range when clicking on an item checkbox while holding Shift after un-selecting another item', () => {
+ const response = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1' }, { id: '2' }, { id: '2.1' }, { id: '3' }, { id: '4' }],
+ });
+
+ fireEvent.click(response.getItemCheckboxInput('1'));
+ expect(response.isItemSelected('1')).to.equal(true);
+ expect(response.isItemSelected('2')).to.equal(false);
+ expect(response.isItemSelected('2.1')).to.equal(false);
+ expect(response.isItemSelected('3')).to.equal(false);
+ expect(response.isItemSelected('4')).to.equal(false);
+
+ fireEvent.click(response.getItemCheckboxInput('2'));
+ expect(response.isItemSelected('1')).to.equal(true);
+ expect(response.isItemSelected('2')).to.equal(true);
+ expect(response.isItemSelected('2.1')).to.equal(false);
+ expect(response.isItemSelected('3')).to.equal(false);
+ expect(response.isItemSelected('4')).to.equal(false);
+
+ fireEvent.click(response.getItemCheckboxInput('2'));
+ expect(response.isItemSelected('1')).to.equal(true);
+ expect(response.isItemSelected('2')).to.equal(false);
+ expect(response.isItemSelected('2.1')).to.equal(false);
+ expect(response.isItemSelected('3')).to.equal(false);
+ expect(response.isItemSelected('4')).to.equal(false);
+
+ fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true });
+ expect(response.isItemSelected('1')).to.equal(true);
+ expect(response.isItemSelected('2')).to.equal(true);
+ expect(response.isItemSelected('2.1')).to.equal(true);
+ expect(response.isItemSelected('3')).to.equal(true);
+ expect(response.isItemSelected('4')).to.equal(false);
+ });
+
+ it('should not expand the selection range when clicking on a disabled item checkbox then clicking on an item checkbox while holding Shift', () => {
+ const response = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [
+ { id: '1' },
+ { id: '2', disabled: true },
+ { id: '2.1' },
+ { id: '3' },
+ { id: '4' },
+ ],
+ });
+
+ fireEvent.click(response.getItemCheckboxInput('2'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ expect(response.isItemSelected('2')).to.equal(false);
+ expect(response.isItemSelected('2.1')).to.equal(false);
+ expect(response.isItemSelected('3')).to.equal(false);
+ expect(response.isItemSelected('4')).to.equal(false);
+
+ fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true });
+ expect(response.isItemSelected('1')).to.equal(false);
+ expect(response.isItemSelected('2')).to.equal(false);
+ expect(response.isItemSelected('2.1')).to.equal(false);
+ expect(response.isItemSelected('3')).to.equal(false);
+ expect(response.isItemSelected('4')).to.equal(false);
+ });
+
+ it('should not expand the selection range when clicking on an item checkbox then clicking a disabled item checkbox while holding Shift', () => {
+ const response = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [
+ { id: '1' },
+ { id: '2' },
+ { id: '2.1' },
+ { id: '3', disabled: true },
+ { id: '4' },
+ ],
+ });
+
+ fireEvent.click(response.getItemCheckboxInput('2'));
+ expect(response.isItemSelected('1')).to.equal(false);
+ expect(response.isItemSelected('2')).to.equal(true);
+ expect(response.isItemSelected('2.1')).to.equal(false);
+ expect(response.isItemSelected('3')).to.equal(false);
+ expect(response.isItemSelected('4')).to.equal(false);
+
+ fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true });
+ expect(response.isItemSelected('1')).to.equal(false);
+ expect(response.isItemSelected('2')).to.equal(true);
+ expect(response.isItemSelected('2.1')).to.equal(false);
+ expect(response.isItemSelected('3')).to.equal(false);
+ expect(response.isItemSelected('4')).to.equal(false);
+ });
+
+ it('should not select disabled items that are part of the selected range', () => {
+ const response = render({
+ multiSelect: true,
+ checkboxSelection: true,
+ items: [{ id: '1' }, { id: '2', disabled: true }, { id: '3' }],
+ });
+
+ fireEvent.click(response.getItemCheckboxInput('1'));
+ expect(response.isItemSelected('1')).to.equal(true);
+ expect(response.isItemSelected('2')).to.equal(false);
+ expect(response.isItemSelected('3')).to.equal(false);
+
+ fireEvent.click(response.getItemCheckboxInput('3'), { shiftKey: true });
+ expect(response.isItemSelected('1')).to.equal(true);
+ expect(response.isItemSelected('2')).to.equal(false);
+ expect(response.isItemSelected('3')).to.equal(true);
+ });
+ });
+ });
+
describe('aria-multiselectable tree attribute', () => {
it('should have the attribute `aria-multiselectable=false if using single select`', () => {
const response = render({
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 02fa885cb08a9..1de3cebb122d4 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts
@@ -8,7 +8,10 @@ import {
getLastNavigableItem,
getNonDisabledItemsInRange,
} from '../../utils/tree';
-import { UseTreeViewSelectionSignature } from './useTreeViewSelection.types';
+import {
+ UseTreeViewSelectionInstance,
+ UseTreeViewSelectionSignature,
+} from './useTreeViewSelection.types';
import { convertSelectedItemsToArray, getLookupFromArray } from './useTreeViewSelection.utils';
export const useTreeViewSelection: TreeViewPlugin = ({
@@ -71,21 +74,34 @@ export const useTreeViewSelection: TreeViewPlugin
const isItemSelected = (itemId: string) => selectedItemsMap.has(itemId);
- const selectItem = (event: React.SyntheticEvent, itemId: string, multiple = false) => {
+ const selectItem: UseTreeViewSelectionInstance['selectItem'] = (
+ event,
+ itemId,
+ keepExistingSelection,
+ newValue,
+ ) => {
if (params.disableSelection) {
return;
}
let newSelected: typeof models.selectedItems.value;
- if (multiple) {
+ if (keepExistingSelection) {
const cleanSelectedItems = convertSelectedItemsToArray(models.selectedItems.value);
- if (instance.isItemSelected(itemId)) {
+ const isSelectedBefore = instance.isItemSelected(itemId);
+ if (isSelectedBefore && (newValue === false || newValue == null)) {
newSelected = cleanSelectedItems.filter((id) => id !== itemId);
- } else {
+ } else if (!isSelectedBefore && (newValue === true || newValue == null)) {
newSelected = [itemId].concat(cleanSelectedItems);
+ } else {
+ newSelected = cleanSelectedItems;
}
} else {
- newSelected = params.multiSelect ? [itemId] : itemId;
+ // eslint-disable-next-line no-lonely-if
+ if (newValue === false) {
+ newSelected = params.multiSelect ? [] : null;
+ } else {
+ newSelected = params.multiSelect ? [itemId] : itemId;
+ }
}
setSelectedItems(event, newSelected);
@@ -189,6 +205,8 @@ export const useTreeViewSelection: TreeViewPlugin
contextValue: {
selection: {
multiSelect: params.multiSelect,
+ checkboxSelection: params.checkboxSelection,
+ disableSelection: params.disableSelection,
},
},
};
@@ -206,6 +224,7 @@ useTreeViewSelection.getDefaultizedParams = (params) => ({
...params,
disableSelection: params.disableSelection ?? false,
multiSelect: params.multiSelect ?? false,
+ checkboxSelection: params.checkboxSelection ?? false,
defaultSelectedItems:
params.defaultSelectedItems ?? (params.multiSelect ? DEFAULT_SELECTED_ITEMS : null),
});
@@ -213,6 +232,7 @@ useTreeViewSelection.getDefaultizedParams = (params) => ({
useTreeViewSelection.params = {
disableSelection: true,
multiSelect: true,
+ checkboxSelection: true,
defaultSelectedItems: true,
selectedItems: true,
onSelectedItemsChange: 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 99c8e8d12d99c..005ab581ce1ec 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
@@ -5,10 +5,18 @@ import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion';
export interface UseTreeViewSelectionInstance {
isItemSelected: (itemId: string) => boolean;
+ /**
+ * Select or deselect an item.
+ * @param {React.SyntheticEvent} event The event source of the callback.
+ * @param {string} itemId The id of the item to select or deselect.
+ * @param {boolean} keepExistingSelection If `true`, don't remove the other selected items.
+ * @param {boolean | undefined} newValue The new selection status of the item. If not defined, the new state will be the opposite of the current state.
+ */
selectItem: (
event: React.SyntheticEvent,
itemId: string,
- keepExistingSelection?: boolean,
+ keepExistingSelection: boolean,
+ newValue?: boolean,
) => void;
/**
* Select all the navigable items in the tree.
@@ -68,10 +76,15 @@ export interface UseTreeViewSelectionParameters;
/**
- * If true `ctrl` and `shift` will trigger multiselect.
+ * If `true`, `ctrl` and `shift` will trigger multiselect.
* @default false
*/
multiSelect?: Multiple;
+ /**
+ * If `true`, the tree view renders a checkbox at the left of its label that allows selecting it.
+ * @default false
+ */
+ checkboxSelection?: boolean;
/**
* Callback fired when tree items are selected/deselected.
* @param {React.SyntheticEvent} event The event source of the callback
@@ -97,11 +110,14 @@ export interface UseTreeViewSelectionParameters = DefaultizedProps<
UseTreeViewSelectionParameters,
- 'disableSelection' | 'defaultSelectedItems' | 'multiSelect'
+ 'disableSelection' | 'defaultSelectedItems' | 'multiSelect' | 'checkboxSelection'
>;
interface UseTreeViewSelectionContextValue {
- selection: Pick, 'multiSelect'>;
+ selection: Pick<
+ UseTreeViewSelectionDefaultizedParameters,
+ 'multiSelect' | 'checkboxSelection' | 'disableSelection'
+ >;
}
export type UseTreeViewSelectionSignature = TreeViewPluginSignature<{
diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts
index 529824dab3333..047bf9660b52f 100644
--- a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts
+++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts
@@ -9,6 +9,7 @@ import {
UseTreeItem2GroupTransitionSlotProps,
UseTreeItem2LabelSlotProps,
UseTreeItemIconContainerSlotProps,
+ UseTreeItem2CheckboxSlotProps,
} from './useTreeItem2.types';
import { useTreeViewContext } from '../internals/TreeViewProvider/useTreeViewContext';
import { DefaultTreeViewPlugins } from '../internals/plugins/defaultPlugins';
@@ -20,7 +21,7 @@ export const useTreeItem2 = => {
const {
runItemPlugins,
- selection: { multiSelect },
+ selection: { multiSelect, disableSelection, checkboxSelection },
disabledItemsFocusable,
instance,
publicAPI,
@@ -32,6 +33,7 @@ export const useTreeItem2 = (null);
const createRootHandleFocus =
(otherHandlers: EventHandlers) =>
@@ -72,12 +74,15 @@ export const useTreeItem2 = (event: React.MouseEvent & MuiCancellableEvent) => {
otherHandlers.onClick?.(event);
- if (event.defaultMuiPrevented) {
+ if (event.defaultMuiPrevented || checkboxRef.current?.contains(event.target as HTMLElement)) {
return;
}
interactions.handleExpansion(event);
- interactions.handleSelection(event);
+
+ if (!checkboxSelection) {
+ interactions.handleSelection(event);
+ }
};
const createContentHandleMouseDown =
@@ -93,6 +98,21 @@ export const useTreeItem2 =
+ (event: React.ChangeEvent & MuiCancellableEvent) => {
+ otherHandlers.onChange?.(event);
+ if (event.defaultMuiPrevented) {
+ return;
+ }
+
+ if (disableSelection || status.disabled) {
+ return;
+ }
+
+ interactions.handleCheckboxSelection(event);
+ };
+
const getRootProps = = {}>(
externalProps: ExternalProps = {} as ExternalProps,
): UseTreeItem2RootSlotProps => {
@@ -145,6 +165,23 @@ export const useTreeItem2 = = {}>(
+ externalProps: ExternalProps = {} as ExternalProps,
+ ): UseTreeItem2CheckboxSlotProps => {
+ const externalEventHandlers = extractEventHandlers(externalProps);
+
+ return {
+ ...externalEventHandlers,
+ visible: checkboxSelection,
+ ref: checkboxRef,
+ checked: status.selected,
+ disabled: disableSelection || status.disabled,
+ tabIndex: -1,
+ ...externalProps,
+ onChange: createCheckboxHandleChange(externalEventHandlers),
+ };
+ };
+
const getLabelProps = = {}>(
externalProps: ExternalProps = {} as ExternalProps,
): UseTreeItem2LabelSlotProps => {
@@ -192,6 +229,7 @@ export const useTreeItem2 = = ExternalProps &
UseTreeItem2LabelSlotOwnProps;
+export interface UseTreeItem2CheckboxSlotOwnProps {
+ visible: boolean;
+ checked: boolean;
+ onChange: React.ChangeEventHandler;
+ disabled: boolean;
+ ref: React.RefObject;
+ tabIndex: -1;
+}
+
+export type UseTreeItem2CheckboxSlotProps = ExternalProps &
+ UseTreeItem2CheckboxSlotOwnProps;
+
export interface UseTreeItem2GroupTransitionSlotOwnProps {
unmountOnExit: boolean;
in: boolean;
@@ -111,6 +123,14 @@ export interface UseTreeItem2ReturnValue = {}>(
externalProps?: ExternalProps,
) => UseTreeItem2LabelSlotProps;
+ /**
+ * Resolver for the checkbox slot's props.
+ * @param {ExternalProps} externalProps Additional props for the checkbox slot
+ * @returns {UseTreeItem2CheckboxSlotProps} Props that should be spread on the checkbox slot
+ */
+ getCheckboxProps: = {}>(
+ externalProps?: ExternalProps,
+ ) => UseTreeItem2CheckboxSlotProps;
/**
* Resolver for the iconContainer slot's props.
* @param {ExternalProps} externalProps Additional props for the iconContainer slot
diff --git a/scripts/x-tree-view.exports.json b/scripts/x-tree-view.exports.json
index d976b52c03da6..aba988ecc2335 100644
--- a/scripts/x-tree-view.exports.json
+++ b/scripts/x-tree-view.exports.json
@@ -24,6 +24,7 @@
{ "name": "SingleSelectTreeViewProps", "kind": "TypeAlias" },
{ "name": "TreeItem", "kind": "Variable" },
{ "name": "TreeItem2", "kind": "Variable" },
+ { "name": "TreeItem2Checkbox", "kind": "Variable" },
{ "name": "TreeItem2Content", "kind": "Variable" },
{ "name": "TreeItem2GroupTransition", "kind": "Variable" },
{ "name": "TreeItem2Icon", "kind": "Function" },
diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.tsx b/test/utils/tree-view/describeTreeView/describeTreeView.tsx
index 3eb291137dbeb..309fd9f5634db 100644
--- a/test/utils/tree-view/describeTreeView/describeTreeView.tsx
+++ b/test/utils/tree-view/describeTreeView/describeTreeView.tsx
@@ -42,6 +42,12 @@ const innerDescribeTreeView = (
const getItemContent = (id: string) =>
getItemRoot(id).querySelector(`.${treeItemClasses.content}`)!;
+ const getItemCheckbox = (id: string) =>
+ getItemRoot(id).querySelector(`.${treeItemClasses.checkbox}`)!;
+
+ const getItemCheckboxInput = (id: string) =>
+ getItemCheckbox(id).querySelector(`input`)!;
+
const getItemLabel = (id: string) =>
getItemRoot(id).querySelector(`.${treeItemClasses.label}`)!;
@@ -58,6 +64,8 @@ const innerDescribeTreeView = (
getFocusedItemId,
getItemRoot,
getItemContent,
+ getItemCheckbox,
+ getItemCheckboxInput,
getItemLabel,
getItemIconContainer,
isItemExpanded,
diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts
index 082f2fb3bd4d9..f9bc6b88f63a9 100644
--- a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts
+++ b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts
@@ -56,6 +56,18 @@ export interface DescribeTreeViewRendererReturnValue<
* @returns {HTMLElement} `content` slot of the item with the given id.
*/
getItemContent: (id: string) => HTMLElement;
+ /**
+ * Returns the `checkbox` slot of the item with the given id.
+ * @param {string} id The id of the item to retrieve.
+ * @returns {HTMLElement} `checkbox` slot of the item with the given id.
+ */
+ getItemCheckbox: (id: string) => HTMLElement;
+ /**
+ * Returns the input element inside the `checkbox` slot of the item with the given id.
+ * @param {string} id The id of the item to retrieve.
+ * @returns {HTMLInputElement} input element inside the `checkbox` slot of the item with the given id.
+ */
+ getItemCheckboxInput: (id: string) => HTMLInputElement;
/**
* Returns the `label` slot of the item with the given id.
* @param {string} id The id of the item to retrieve.