From 3f89cf75f67e81d5d95f619c6473cc2160805184 Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Mon, 13 May 2024 10:44:59 +0200 Subject: [PATCH] [TreeView] Add support for checkbox selection (#11452) --- .../customization/CustomContentTreeView.js | 3 + .../customization/CustomContentTreeView.tsx | 3 + .../customization/FileExplorer.js | 4 +- .../customization/FileExplorer.tsx | 4 +- .../selection/CheckboxMultiSelection.js | 41 +++ .../selection/CheckboxMultiSelection.tsx | 41 +++ .../CheckboxMultiSelection.tsx.preview | 1 + .../selection/CheckboxSelection.js | 41 +++ .../selection/CheckboxSelection.tsx | 41 +++ .../selection/CheckboxSelection.tsx.preview | 1 + .../rich-tree-view/selection/selection.md | 10 + .../customization/CustomContentTreeView.js | 3 + .../customization/CustomContentTreeView.tsx | 3 + .../selection/CheckboxMultiSelection.js | 28 ++ .../selection/CheckboxMultiSelection.tsx | 28 ++ .../selection/CheckboxSelection.js | 28 ++ .../selection/CheckboxSelection.tsx | 28 ++ .../simple-tree-view/selection/selection.md | 10 + .../pages/x/api/tree-view/rich-tree-view.json | 1 + .../x/api/tree-view/simple-tree-view.json | 1 + docs/pages/x/api/tree-view/tree-item-2.json | 6 + docs/pages/x/api/tree-view/tree-item.json | 6 + docs/pages/x/api/tree-view/tree-view.json | 1 + .../rich-tree-view/rich-tree-view.json | 5 +- .../simple-tree-view/simple-tree-view.json | 5 +- .../tree-view/tree-item-2/tree-item-2.json | 1 + .../tree-view/tree-item/tree-item.json | 4 + .../tree-view/tree-view/tree-view.json | 5 +- .../src/RichTreeView/RichTreeView.tsx | 7 +- .../src/SimpleTreeView/SimpleTreeView.tsx | 7 +- .../src/TreeItem/TreeItem.test.tsx | 2 + .../x-tree-view/src/TreeItem/TreeItem.tsx | 5 + .../src/TreeItem/TreeItemContent.tsx | 27 +- .../src/TreeItem/treeItemClasses.ts | 3 + .../src/TreeItem/useTreeItemState.ts | 22 +- .../x-tree-view/src/TreeItem2/TreeItem2.tsx | 33 ++ .../src/TreeItem2/TreeItem2.types.ts | 6 + packages/x-tree-view/src/TreeItem2/index.ts | 1 + .../x-tree-view/src/TreeView/TreeView.tsx | 7 +- .../useTreeItem2Utils/useTreeItem2Utils.tsx | 18 +- .../useTreeViewKeyboardNavigation.ts | 9 +- .../useTreeViewSelection.test.tsx | 320 +++++++++++++++++- .../useTreeViewSelection.ts | 32 +- .../useTreeViewSelection.types.ts | 24 +- .../src/useTreeItem2/useTreeItem2.ts | 44 ++- .../src/useTreeItem2/useTreeItem2.types.ts | 20 ++ scripts/x-tree-view.exports.json | 1 + .../describeTreeView/describeTreeView.tsx | 8 + .../describeTreeView.types.ts | 12 + 49 files changed, 930 insertions(+), 31 deletions(-) create mode 100644 docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.js create mode 100644 docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.tsx create mode 100644 docs/data/tree-view/rich-tree-view/selection/CheckboxMultiSelection.tsx.preview create mode 100644 docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.js create mode 100644 docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.tsx create mode 100644 docs/data/tree-view/rich-tree-view/selection/CheckboxSelection.tsx.preview create mode 100644 docs/data/tree-view/simple-tree-view/selection/CheckboxMultiSelection.js create mode 100644 docs/data/tree-view/simple-tree-view/selection/CheckboxMultiSelection.tsx create mode 100644 docs/data/tree-view/simple-tree-view/selection/CheckboxSelection.js create mode 100644 docs/data/tree-view/simple-tree-view/selection/CheckboxSelection.tsx 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( +