diff --git a/docs/data/tree-view/accessibility/accessibility.md b/docs/data/tree-view/accessibility/accessibility.md index 7f53c06af65c6..341b6fec4ccb8 100644 --- a/docs/data/tree-view/accessibility/accessibility.md +++ b/docs/data/tree-view/accessibility/accessibility.md @@ -61,6 +61,11 @@ When a single-select tree receives focus: - If none of the items are selected when the tree receives focus, focus is set on the first item. - If an item is selected before the tree receives focus, focus is set on the selected item. +| Keys | Description | +| ---------------------------: | :----------------------------------------------------------- | +| Space | Selects the focused item. | +| Enter | Selects the focused item if the item does not have children. | + ### On multi-select trees When a multi-select tree receives focus: diff --git a/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.js b/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.js new file mode 100644 index 0000000000000..056b2d2cb4bd7 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.js @@ -0,0 +1,99 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; + +import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; + +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' }], + }, +]; + +function getItemDescendantsIds(item) { + const ids = []; + item.children?.forEach((child) => { + ids.push(child.id); + ids.push(...getItemDescendantsIds(child)); + }); + + return ids; +} + +export default function ParentChildrenSelectionRelationship() { + const [selectedItems, setSelectedItems] = React.useState([]); + const toggledItemRef = React.useRef({}); + const apiRef = useTreeViewApiRef(); + + const handleItemSelectionToggle = (event, itemId, isSelected) => { + toggledItemRef.current[itemId] = isSelected; + }; + + const handleSelectedItemsChange = (event, newSelectedItems) => { + setSelectedItems(newSelectedItems); + + // Select / unselect the children of the toggled item + const itemsToSelect = []; + const itemsToUnSelect = {}; + Object.entries(toggledItemRef.current).forEach(([itemId, isSelected]) => { + const item = apiRef.current.getItem(itemId); + if (isSelected) { + itemsToSelect.push(...getItemDescendantsIds(item)); + } else { + getItemDescendantsIds(item).forEach((descendantId) => { + itemsToUnSelect[descendantId] = true; + }); + } + }); + + const newSelectedItemsWithChildren = Array.from( + new Set( + [...newSelectedItems, ...itemsToSelect].filter( + (itemId) => !itemsToUnSelect[itemId], + ), + ), + ); + + setSelectedItems(newSelectedItemsWithChildren); + + toggledItemRef.current = {}; + }; + + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.tsx b/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.tsx new file mode 100644 index 0000000000000..c7ed6099696e6 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; +import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; + +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' }], + }, +]; + +function getItemDescendantsIds(item: TreeViewBaseItem) { + const ids: string[] = []; + item.children?.forEach((child) => { + ids.push(child.id); + ids.push(...getItemDescendantsIds(child)); + }); + + return ids; +} + +export default function ParentChildrenSelectionRelationship() { + const [selectedItems, setSelectedItems] = React.useState([]); + const toggledItemRef = React.useRef<{ [itemId: string]: boolean }>({}); + const apiRef = useTreeViewApiRef(); + + const handleItemSelectionToggle = ( + event: React.SyntheticEvent, + itemId: string, + isSelected: boolean, + ) => { + toggledItemRef.current[itemId] = isSelected; + }; + + const handleSelectedItemsChange = ( + event: React.SyntheticEvent, + newSelectedItems: string[], + ) => { + setSelectedItems(newSelectedItems); + + // Select / unselect the children of the toggled item + const itemsToSelect: string[] = []; + const itemsToUnSelect: { [itemId: string]: boolean } = {}; + Object.entries(toggledItemRef.current).forEach(([itemId, isSelected]) => { + const item = apiRef.current!.getItem(itemId); + if (isSelected) { + itemsToSelect.push(...getItemDescendantsIds(item)); + } else { + getItemDescendantsIds(item).forEach((descendantId) => { + itemsToUnSelect[descendantId] = true; + }); + } + }); + + const newSelectedItemsWithChildren = Array.from( + new Set( + [...newSelectedItems, ...itemsToSelect].filter( + (itemId) => !itemsToUnSelect[itemId], + ), + ), + ); + + setSelectedItems(newSelectedItemsWithChildren); + + toggledItemRef.current = {}; + }; + + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.tsx.preview b/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.tsx.preview new file mode 100644 index 0000000000000..6fa38db9c231b --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/selection/ParentChildrenSelectionRelationship.tsx.preview @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/selection/SingleSelectTreeView.js b/docs/data/tree-view/rich-tree-view/selection/SingleSelectTreeView.js new file mode 100644 index 0000000000000..60caf9c535862 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/selection/SingleSelectTreeView.js @@ -0,0 +1,41 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; + +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 SingleSelectTreeView() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/selection/SingleSelectTreeView.tsx b/docs/data/tree-view/rich-tree-view/selection/SingleSelectTreeView.tsx new file mode 100644 index 0000000000000..fbd000d8af63d --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/selection/SingleSelectTreeView.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; + +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 SingleSelectTreeView() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/selection/SingleSelectTreeView.tsx.preview b/docs/data/tree-view/rich-tree-view/selection/SingleSelectTreeView.tsx.preview new file mode 100644 index 0000000000000..19ab6390267f1 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/selection/SingleSelectTreeView.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 929d048d53776..d58f37d61b5dd 100644 --- a/docs/data/tree-view/rich-tree-view/selection/selection.md +++ b/docs/data/tree-view/rich-tree-view/selection/selection.md @@ -11,12 +11,32 @@ waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/

Handle how users can select items.

+## Single selection + +By default, the Tree View allows selecting a single item. + +{{"demo": "SingleSelectTreeView.js"}} + +:::success +When the Tree View uses single selection, you can select an item by clicking it, +or using the [keyboard shortcuts](/x/react-tree-view/accessibility/#on-single-select-trees). +::: + ## Multi selection -The Tree View also supports multi-selection: +Use the `multiSelect` prop to enable multi-selection. {{"demo": "MultiSelectTreeView.js"}} +:::success +When the Tree View uses multi selection, you can select multiple items using the mouse in two ways: + +- To select multiple independent items, hold Ctrl (or ⌘ Command on macOS) and click the items. +- To select a range of items, click on the first item of the range, then hold the Shift key while clicking on the last item of the range. + +You can also use the [keyboard shortcuts](/x/react-tree-view/accessibility/#on-multi-select-trees) to select items. +::: + ## Disable selection Use the `disableSelection` prop if you don't want your items to be selectable: @@ -54,3 +74,23 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen Use the `onItemSelectionToggle` prop if you want to react to an item selection change: {{"demo": "TrackItemSelectionToggle.js"}} + +## Parent / children selection relationship + +Automatically select an item when all of its children are selected and automatically select all children when the parent is selected. + +:::warning +This feature isn't implemented yet. It's coming. + +👍 Upvote [issue #12883](https://github.com/mui/mui-x/issues/4821) if you want to see it land faster. + +Don't hesitate to leave a comment on the same issue to influence what gets built. +Especially if you already have a use case for this component, +or if you are facing a pain point with your current solution. +::: + +If you cannot wait for the official implementation, +you can create your own custom solution using the `selectedItems`, +`onSelectedItemsChange` and `onItemSelectionToggle` props: + +{{"demo": "ParentChildrenSelectionRelationship.js"}} diff --git a/docs/data/tree-view/simple-tree-view/selection/SingleSelectTreeView.js b/docs/data/tree-view/simple-tree-view/selection/SingleSelectTreeView.js new file mode 100644 index 0000000000000..60caf9c535862 --- /dev/null +++ b/docs/data/tree-view/simple-tree-view/selection/SingleSelectTreeView.js @@ -0,0 +1,41 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; + +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 SingleSelectTreeView() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/simple-tree-view/selection/SingleSelectTreeView.tsx b/docs/data/tree-view/simple-tree-view/selection/SingleSelectTreeView.tsx new file mode 100644 index 0000000000000..fbd000d8af63d --- /dev/null +++ b/docs/data/tree-view/simple-tree-view/selection/SingleSelectTreeView.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; + +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 SingleSelectTreeView() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/simple-tree-view/selection/SingleSelectTreeView.tsx.preview b/docs/data/tree-view/simple-tree-view/selection/SingleSelectTreeView.tsx.preview new file mode 100644 index 0000000000000..19ab6390267f1 --- /dev/null +++ b/docs/data/tree-view/simple-tree-view/selection/SingleSelectTreeView.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file 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 18337196bec76..434a4284a0a2e 100644 --- a/docs/data/tree-view/simple-tree-view/selection/selection.md +++ b/docs/data/tree-view/simple-tree-view/selection/selection.md @@ -11,12 +11,32 @@ waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/

Learn how to enable item selection for the Tree View component.

+## Single selection + +By default, the Tree View allows selecting a single item. + +{{"demo": "SingleSelectTreeView.js"}} + +:::success +When the Tree View uses single selection, you can select an item by clicking it, +or using the [keyboard shortcuts](/x/react-tree-view/accessibility/#on-single-select-trees). +::: + ## Multi selection -Apply the `multiSelect` prop on the Tree View to let users select multiple items. +Use the `multiSelect` prop to enable multi-selection. {{"demo": "MultiSelectTreeView.js"}} +:::success +When the Tree View uses multi selection, you can select multiple items using the mouse in two ways: + +- To select multiple independent items, hold Ctrl (or ⌘ Command on macOS) and click the items. +- To select a range of items, click on the first item of the range, then hold the Shift key while clicking on the last item of the range. + +You can also use the [keyboard shortcuts](/x/react-tree-view/accessibility/#on-multi-select-trees) to select items. +::: + ## Disable selection Use the `disableSelection` prop if you don't want your items to be selectable: