Skip to content

Commit

Permalink
[docs] Improve Tree View selection doc (mui#13105)
Browse files Browse the repository at this point in the history
Signed-off-by: Michel Engelen <[email protected]>
Signed-off-by: Gene Arch <[email protected]>
Signed-off-by: Flavien DELANGLE <[email protected]>
Co-authored-by: Michel Engelen <[email protected]>
Co-authored-by: Olivier Tassinari <[email protected]>
Co-authored-by: Gene Arch <[email protected]>
Co-authored-by: Rom Grk <[email protected]>
Co-authored-by: Nora <[email protected]>
  • Loading branch information
6 people authored and thomasmoon committed Sep 6, 2024
1 parent dcd6fec commit a41a835
Show file tree
Hide file tree
Showing 12 changed files with 449 additions and 2 deletions.
5 changes: 5 additions & 0 deletions docs/data/tree-view/accessibility/accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| ---------------------------: | :----------------------------------------------------------- |
| <kbd class="key">Space</kbd> | Selects the focused item. |
| <kbd class="key">Enter</kbd> | Selects the focused item if the item does not have children. |

### On multi-select trees

When a multi-select tree receives focus:
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Box sx={{ height: 264, flexGrow: 1, maxWidth: 400 }}>
<RichTreeView
multiSelect
checkboxSelection
apiRef={apiRef}
items={MUI_X_PRODUCTS}
selectedItems={selectedItems}
onSelectedItemsChange={handleSelectedItemsChange}
onItemSelectionToggle={handleItemSelectionToggle}
/>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);
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 (
<Box sx={{ height: 264, flexGrow: 1, maxWidth: 400 }}>
<RichTreeView
multiSelect
checkboxSelection
apiRef={apiRef}
items={MUI_X_PRODUCTS}
selectedItems={selectedItems}
onSelectedItemsChange={handleSelectedItemsChange}
onItemSelectionToggle={handleItemSelectionToggle}
/>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<RichTreeView
multiSelect
checkboxSelection
apiRef={apiRef}
items={MUI_X_PRODUCTS}
selectedItems={selectedItems}
onSelectedItemsChange={handleSelectedItemsChange}
onItemSelectionToggle={handleItemSelectionToggle}
/>
Original file line number Diff line number Diff line change
@@ -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 (
<Box sx={{ minHeight: 200, flexGrow: 1, maxWidth: 400 }}>
<RichTreeView items={MUI_X_PRODUCTS} />
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<Box sx={{ minHeight: 200, flexGrow: 1, maxWidth: 400 }}>
<RichTreeView items={MUI_X_PRODUCTS} />
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<RichTreeView items={MUI_X_PRODUCTS} />
42 changes: 41 additions & 1 deletion docs/data/tree-view/rich-tree-view/selection/selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,32 @@ waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/

<p class="description">Handle how users can select items.</p>

## 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 <kbd class="key">Ctrl</kbd> (or <kbd class="key">⌘ Command</kbd> on macOS) and click the items.
- To select a range of items, click on the first item of the range, then hold the <kbd class="key">Shift</kbd> 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:
Expand Down Expand Up @@ -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"}}
Loading

0 comments on commit a41a835

Please sign in to comment.