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

[TreeView] Introduce a new TreeItem2 component and a new useTreeItem2 hook #11721

Merged
merged 60 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
d9444f7
[TreeView] New hook useTreeItem
flaviendelangle Jan 17, 2024
659158c
Merge branch 'next' into headless-tree-item
flaviendelangle Jan 18, 2024
fc58e4d
Work
flaviendelangle Jan 18, 2024
9864641
Work
flaviendelangle Jan 18, 2024
bc393e5
Work
flaviendelangle Jan 18, 2024
a3fce49
Fix
flaviendelangle Jan 19, 2024
939700b
Merge
flaviendelangle Jan 22, 2024
9ce8cfc
Merge branch 'next' into headless-tree-item
flaviendelangle Jan 22, 2024
83be9de
Merge branch 'next' into headless-tree-item
flaviendelangle Jan 25, 2024
4e0107f
Merge branch 'next' into headless-tree-item
flaviendelangle Feb 15, 2024
871a368
Work
flaviendelangle Feb 15, 2024
d49adca
Add example
flaviendelangle Feb 15, 2024
aedcc17
Merge branch 'next' into headless-tree-item
flaviendelangle Feb 15, 2024
04f3856
Migrate CustomContentTreeView demo
flaviendelangle Feb 15, 2024
7e1a698
Work
flaviendelangle Feb 15, 2024
33f3c9f
Fix
flaviendelangle Feb 15, 2024
838e424
Fix
flaviendelangle Feb 15, 2024
767c6cd
Fix
flaviendelangle Feb 15, 2024
c97b7c2
Try CSS
flaviendelangle Feb 15, 2024
94c6fae
Fix
flaviendelangle Feb 16, 2024
0b7395d
Work
flaviendelangle Feb 16, 2024
98e6d42
Fix
flaviendelangle Feb 16, 2024
0b427bf
Merge branch 'next' into headless-tree-item
flaviendelangle Feb 16, 2024
8a404a4
Fix
flaviendelangle Feb 16, 2024
91d4a15
Work
flaviendelangle Feb 16, 2024
fda96cd
Fix
flaviendelangle Feb 16, 2024
afcd14e
Fix
flaviendelangle Feb 16, 2024
26431c6
Fix
flaviendelangle Feb 16, 2024
4e07140
Merge branch 'next' into headless-tree-item
flaviendelangle Feb 19, 2024
909d97a
Work
flaviendelangle Feb 19, 2024
1e1ef17
Migrate GmailTreeView
flaviendelangle Feb 20, 2024
f3709df
Merge branch 'next' into headless-tree-item
flaviendelangle Feb 21, 2024
f43704d
Fix
flaviendelangle Feb 21, 2024
0c0faee
Work on demos
flaviendelangle Feb 22, 2024
5ef7108
Fix
flaviendelangle Feb 22, 2024
bc82c34
Fix
flaviendelangle Feb 22, 2024
cbe16ed
Merge branch 'next' into headless-tree-item
flaviendelangle Feb 22, 2024
caeeb8a
Do not use ownerState in TreeItemNextContent
flaviendelangle Feb 23, 2024
bdb09d9
Merge branch 'next' into headless-tree-item
flaviendelangle Feb 26, 2024
4f106e1
Expose publicAPI in useTreeItem
flaviendelangle Feb 28, 2024
31ca8bd
Fix
flaviendelangle Feb 28, 2024
4ecc5b1
Merge
flaviendelangle Mar 4, 2024
47c410d
Fix
flaviendelangle Mar 4, 2024
cba8a52
Add Next suffix to TreeItemIcon and TreeItemProvider
flaviendelangle Mar 4, 2024
a32b9ac
Merge branch 'next' into headless-tree-item
flaviendelangle Mar 5, 2024
fe514cb
Rename component and move to public API
flaviendelangle Mar 5, 2024
430cd92
Add doc and proptypes
flaviendelangle Mar 5, 2024
f96e050
Fix
flaviendelangle Mar 5, 2024
8c32e8f
Fix
flaviendelangle Mar 5, 2024
1357ad9
Fix
flaviendelangle Mar 5, 2024
f8c80be
Merge
flaviendelangle Mar 6, 2024
0d25b26
Merge branch 'next' into headless-tree-item
flaviendelangle Mar 6, 2024
b4b0f86
Improve doc
flaviendelangle Mar 6, 2024
1d77d53
Merge branch 'next' into headless-tree-item
flaviendelangle Mar 7, 2024
06ae133
Work
flaviendelangle Mar 7, 2024
ba11589
Fix
flaviendelangle Mar 7, 2024
f256fec
Review: Lukas
flaviendelangle Mar 7, 2024
75b1cc9
Fix CI
flaviendelangle Mar 7, 2024
4d50562
Fix
flaviendelangle Mar 7, 2024
83ecbea
Clean doc
flaviendelangle Mar 11, 2024
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
1 change: 1 addition & 0 deletions docs/data/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ const pages: MuiPage[] = [
{ pathname: '/x/react-tree-view/rich-tree-view/items' },
{ pathname: '/x/react-tree-view/rich-tree-view/selection' },
{ pathname: '/x/react-tree-view/rich-tree-view/expansion' },
{ pathname: '/x/react-tree-view/rich-tree-view/customization' },
{ pathname: '/x/react-tree-view/rich-tree-view/focus' },
],
},
Expand Down
4 changes: 4 additions & 0 deletions docs/data/tree-view-component-api-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const apiPages: MuiPage[] = [
pathname: '/x/api/tree-view/tree-item',
title: 'TreeItem',
},
{
pathname: '/x/api/tree-view/tree-item-2',
title: 'TreeItem2',
},
{
pathname: '/x/api/tree-view/tree-view',
title: 'TreeView',
Expand Down
136 changes: 136 additions & 0 deletions docs/data/tree-view/rich-tree-view/customization/LabelSlots.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import * as React from 'react';
import { TreeItem2, TreeItem2Label } from '@mui/x-tree-view/TreeItem2';
import { RichTreeView } from '@mui/x-tree-view';

function CustomLabel(props) {
const { children, onChange, ...other } = props;

const [isEditing, setIsEditing] = React.useState(false);
const [value, setValue] = React.useState('');
const editingLabelRef = React.useRef(null);

const handleLabelDoubleClick = (event) => {
event.stopPropagation();
setIsEditing(true);
setValue(children);
};

const handleEditingLabelChange = (event) => {
setValue(event.target.value);
};

const handleEditingLabelKeyDown = (event) => {
if (event.key === 'Enter') {
event.stopPropagation();
setIsEditing(false);
onChange(value);
}
};

React.useEffect(() => {
if (isEditing) {
editingLabelRef.current?.focus();
}
}, [isEditing]);

if (isEditing) {
return (
<input
value={value}
onChange={handleEditingLabelChange}
onKeyDown={handleEditingLabelKeyDown}
ref={editingLabelRef}
/>
);
}

return (
<TreeItem2Label {...other} onDoubleClick={handleLabelDoubleClick}>
{children}
</TreeItem2Label>
);
}

const TreeItemContext = React.createContext({ onLabelValueChange: () => {} });

const CustomTreeItem = React.forwardRef((props, ref) => {
const { onLabelValueChange } = React.useContext(TreeItemContext);

const handleLabelValueChange = (newLabel) => {
onLabelValueChange(props.nodeId, newLabel);
};

return (
<TreeItem2
ref={ref}
{...props}
slots={{
label: CustomLabel,
}}
slotProps={{
label: {
onChange: handleLabelValueChange,
},
}}
/>
);
});

const DEFAULT_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' },
],
},
];

const DEFAULT_EXPANDED_NODES = ['pickers'];

export default function LabelSlots() {
const [products, setProducts] = React.useState(DEFAULT_MUI_X_PRODUCTS);

const context = React.useMemo(
() => ({
onLabelValueChange: (nodeId, label) =>
setProducts((prev) => {
const walkTree = (item) => {
if (item.id === nodeId) {
return { ...item, label };
}
if (item.children) {
return { ...item, children: item.children.map(walkTree) };
}

return item;
};

return prev.map(walkTree);
}),
}),
[],
);

return (
<TreeItemContext.Provider value={context}>
<RichTreeView
items={products}
aria-label="customized"
defaultExpandedNodes={DEFAULT_EXPANDED_NODES}
sx={{ overflowX: 'hidden', minHeight: 224, flexGrow: 1, maxWidth: 300 }}
slots={{ item: CustomTreeItem }}
/>
</TreeItemContext.Provider>
);
}
150 changes: 150 additions & 0 deletions docs/data/tree-view/rich-tree-view/customization/LabelSlots.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import * as React from 'react';
import {
TreeItem2,
TreeItem2Label,
TreeItem2Props,
} from '@mui/x-tree-view/TreeItem2';
import { RichTreeView, TreeViewBaseItem } from '@mui/x-tree-view';
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

interface CustomLabelProps {
children: string;
className: string;
onChange: (value: string) => void;
}

function CustomLabel(props: CustomLabelProps) {
const { children, onChange, ...other } = props;

const [isEditing, setIsEditing] = React.useState<boolean>(false);
const [value, setValue] = React.useState('');
const editingLabelRef = React.useRef<HTMLInputElement>(null);

const handleLabelDoubleClick = (event: React.MouseEvent) => {
event.stopPropagation();
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
setIsEditing(true);
setValue(children);
};

const handleEditingLabelChange = (event: React.ChangeEvent<HTMLInputElement>) => {
LukasTy marked this conversation as resolved.
Show resolved Hide resolved
setValue(event.target.value);
};

const handleEditingLabelKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
event.stopPropagation();
setIsEditing(false);
onChange(value);
}
};

React.useEffect(() => {
if (isEditing) {
editingLabelRef.current?.focus();
LukasTy marked this conversation as resolved.
Show resolved Hide resolved
}
}, [isEditing]);

if (isEditing) {
return (
<input
value={value}
onChange={handleEditingLabelChange}
onKeyDown={handleEditingLabelKeyDown}
ref={editingLabelRef}
/>
);
}

return (
<TreeItem2Label {...other} onDoubleClick={handleLabelDoubleClick}>
{children}
</TreeItem2Label>
);
}

const TreeItemContext = React.createContext<{
onLabelValueChange: (nodeId: string, label: string) => void;
}>({ onLabelValueChange: () => {} });

const CustomTreeItem = React.forwardRef(
(props: TreeItem2Props, ref: React.Ref<HTMLLIElement>) => {
const { onLabelValueChange } = React.useContext(TreeItemContext);

const handleLabelValueChange = (newLabel: string) => {
onLabelValueChange(props.nodeId, newLabel);
};

return (
<TreeItem2
ref={ref}
{...props}
slots={{
label: CustomLabel,
}}
slotProps={{
label: {
onChange: handleLabelValueChange,
} as any,
}}
/>
);
},
);

const DEFAULT_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' },
],
},
];

const DEFAULT_EXPANDED_NODES = ['pickers'];

export default function LabelSlots() {
const [products, setProducts] = React.useState(DEFAULT_MUI_X_PRODUCTS);

const context = React.useMemo(
() => ({
onLabelValueChange: (nodeId: string, label: string) =>
setProducts((prev) => {
const walkTree = (item: TreeViewBaseItem): TreeViewBaseItem => {
if (item.id === nodeId) {
return { ...item, label };
}
if (item.children) {
return { ...item, children: item.children.map(walkTree) };
}

return item;
};

return prev.map(walkTree);
}),
}),
[],
);

return (
<TreeItemContext.Provider value={context}>
<RichTreeView
items={products}
aria-label="customized"
defaultExpandedNodes={DEFAULT_EXPANDED_NODES}
sx={{ overflowX: 'hidden', minHeight: 224, flexGrow: 1, maxWidth: 300 }}
slots={{ item: CustomTreeItem }}
/>
</TreeItemContext.Provider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<TreeItemContext.Provider value={context}>
<RichTreeView
items={products}
aria-label="customized"
defaultExpandedNodes={DEFAULT_EXPANDED_NODES}
sx={{ overflowX: 'hidden', minHeight: 224, flexGrow: 1, maxWidth: 300 }}
slots={{ item: CustomTreeItem }}
/>
</TreeItemContext.Provider>
29 changes: 29 additions & 0 deletions docs/data/tree-view/rich-tree-view/customization/customization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
productId: x-tree-view
title: Rich Tree View - Customization
components: RichTreeView, TreeItem, TreeItem2
packageName: '@mui/x-tree-view'
githubLabel: 'component: tree view'
waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
---

# Rich Tree View - Customization

<p class="description">Learn how to customize the rich version of the Tree View component.</p>
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

## Basics

### Custom label

Use the `label` slot to customize the Tree Item label or to replace it with an entirely custom component.
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

The `slotProps` prop allows you to pass props to the default label component:
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

:::warning
TODO
:::

The `slots` prop allows you to replace the default label with your own component.
Copy link
Member Author

Choose a reason for hiding this comment

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

I added this demo here because it make no sense on the SimpleTreeView

I think it can be a good example for slots.label until we have a built-in edition

Copy link
Member

Choose a reason for hiding this comment

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

Great example. 👍

The demo below shows how to implement a very basic label-editing feature:

{{"demo": "LabelSlots.js", "defaultCodeOpen": false}}
14 changes: 7 additions & 7 deletions docs/data/tree-view/rich-tree-view/headless/headless.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@
You can use the `contextValue` property in the returned object to pass elements to the Tree Item:

:::warning
The context is private for now and cannot be accessed outside of our own plugins.
You need to modify the `useTreeItem` hook to return the new value returned by your plugin.
The context is private for now and cannot be accessed outside our own plugins.

Check warning on line 209 in docs/data/tree-view/rich-tree-view/headless/headless.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.We] Try to avoid using first-person plural like 'our'. Raw Output: {"message": "[Google.We] Try to avoid using first-person plural like 'our'.", "location": {"path": "docs/data/tree-view/rich-tree-view/headless/headless.md", "range": {"start": {"line": 209, "column": 63}}}, "severity": "WARNING"}
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
You need to modify the `useTreeItemState` hook to return the new value returned by your plugin.
:::

```tsx
Expand All @@ -219,25 +219,25 @@
};
};

function useTreeItem(nodeId: string) {
function useTreeItemState(nodeId: string) {
const {
customPlugin,
// ...other elements returned by the context
} = useTreeViewContext<DefaultTreeViewPlugins>();

// ...rest of the `useTreeItem` hook content
// ...rest of the `useTreeItemState` hook content

return {
customPlugin,
// ...other elements returned by `useTreeItem`
// ...other elements returned by `useTreeItemState`
};
}

function TreeItemContent() {
const {
customPlugin,
// ...other elements returned by `useTreeItem`
} = useTreeItem(props.nodeId);
// ...other elements returned by `useTreeItemState`
} = useTreeItemState(props.nodeId);

// Do something with customPlugin.enabled
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const CustomTreeItem = styled(TreeItem)(({ theme }) => ({
opacity: 0.3,
},
},
[`& .${treeItemClasses.group}`]: {
[`& .${treeItemClasses.groupTransition}`]: {
marginLeft: 15,
paddingLeft: 18,
borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`,
Expand Down
Loading
Loading