Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[docs] Improve Tree View selection doc #13105

Merged
merged 16 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 |
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
| ---------------------------: | :----------------------------------------------------------- |
| <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';
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike the data grid, I added those as success because IMHO this is more a side not than the main content here.
The main content is how to enable selection, and then we describe how to use selection but this mostly target end users which won't read to doc, so it's more to say "Hey we have good selection interactions".

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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it's worth adding a demo here on how to do it in user-land?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth it, yes 👌

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍


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.
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

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