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] New API method: setItemExpansion #12595

Merged
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';

import { useTreeViewApiRef } from '@mui/x-tree-view';
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

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

export default function ChangeItemExpansion() {
const apiRef = useTreeViewApiRef();

const handleExpandClick = (event) => {
apiRef.current.setItemExpansion(event, 'grid', true);
};

const handleCollapseClick = (event) => {
apiRef.current.setItemExpansion(event, 'grid', false);
};

return (
<Box sx={{ flexGrow: 1, maxWidth: 400 }}>
<Stack sx={{ mb: 1 }} spacing={2} direction="row">
<Button onClick={handleExpandClick}>Expand Data Grid</Button>
<Button onClick={handleCollapseClick}>Collapse Data Grid</Button>
</Stack>
<Box sx={{ minHeight: 220, flexGrow: 1 }}>
<RichTreeView items={MUI_X_PRODUCTS} apiRef={apiRef} />
</Box>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
import { TreeViewBaseItem } from '@mui/x-tree-view/models';
import { useTreeViewApiRef } from '@mui/x-tree-view';
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' },
],
},
];

export default function ChangeItemExpansion() {
const apiRef = useTreeViewApiRef();

const handleExpandClick = (event: React.MouseEvent) => {
apiRef.current!.setItemExpansion(event, 'grid', true);
};

const handleCollapseClick = (event: React.MouseEvent) => {
apiRef.current!.setItemExpansion(event, 'grid', false);
};

return (
<Box sx={{ flexGrow: 1, maxWidth: 400 }}>
<Stack sx={{ mb: 1 }} spacing={2} direction="row">
<Button onClick={handleExpandClick}>Expand Data Grid</Button>
<Button onClick={handleCollapseClick}>Collapse Data Grid</Button>
</Stack>
<Box sx={{ minHeight: 220, flexGrow: 1 }}>
<RichTreeView items={MUI_X_PRODUCTS} apiRef={apiRef} />
</Box>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Stack sx={{ mb: 1 }} spacing={2} direction="row">
<Button onClick={handleExpandClick}>Expand Data Grid</Button>
<Button onClick={handleCollapseClick}>Collapse Data Grid</Button>
</Stack>
<Box sx={{ minHeight: 220, flexGrow: 1 }}>
<RichTreeView items={MUI_X_PRODUCTS} apiRef={apiRef} />
</Box>
6 changes: 6 additions & 0 deletions docs/data/tree-view/rich-tree-view/expansion/expansion.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen
Use the `onItemExpansionToggle` prop if you want to react to an item expansion change:

{{"demo": "TrackItemExpansionToggle.js"}}

## Change item expansion

You can use the `setItemExpansion` api method to imperatively change the expansion of an item:
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

{{"demo": "ChangeItemExpansion.js"}}
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 Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
import { TreeItem } from '@mui/x-tree-view/TreeItem';
import { useTreeViewApiRef } from '@mui/x-tree-view';
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

export default function ChangeItemExpansion() {
const apiRef = useTreeViewApiRef();

const handleExpandClick = (event) => {
apiRef.current.setItemExpansion(event, 'grid', true);
};

const handleCollapseClick = (event) => {
apiRef.current.setItemExpansion(event, 'grid', false);
};

return (
<Box sx={{ flexGrow: 1, maxWidth: 400 }}>
<Stack sx={{ mb: 1 }} spacing={2} direction="row">
<Button onClick={handleExpandClick}>Expand Data Grid</Button>
<Button onClick={handleCollapseClick}>Collapse Data Grid</Button>
</Stack>
<Box sx={{ minHeight: 220, flexGrow: 1 }}>
<SimpleTreeView apiRef={apiRef}>
<TreeItem itemId="grid" label="Data Grid">
<TreeItem itemId="grid-community" label="@mui/x-data-grid" />
<TreeItem itemId="grid-pro" label="@mui/x-data-grid-pro" />
<TreeItem itemId="grid-premium" label="@mui/x-data-grid-premium" />
</TreeItem>
<TreeItem itemId="pickers" label="Date and Time Pickers">
<TreeItem itemId="pickers-community" label="@mui/x-date-pickers" />
<TreeItem itemId="pickers-pro" label="@mui/x-date-pickers-pro" />
</TreeItem>
</SimpleTreeView>
</Box>
</Box>
);
}
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 Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
import { TreeItem } from '@mui/x-tree-view/TreeItem';
import { useTreeViewApiRef } from '@mui/x-tree-view';
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

export default function ChangeItemExpansion() {
const apiRef = useTreeViewApiRef();

const handleExpandClick = (event: React.MouseEvent) => {
apiRef.current!.setItemExpansion(event, 'grid', true);
};

const handleCollapseClick = (event: React.MouseEvent) => {
apiRef.current!.setItemExpansion(event, 'grid', false);
};

return (
<Box sx={{ flexGrow: 1, maxWidth: 400 }}>
<Stack sx={{ mb: 1 }} spacing={2} direction="row">
<Button onClick={handleExpandClick}>Expand Data Grid</Button>
<Button onClick={handleCollapseClick}>Collapse Data Grid</Button>
</Stack>
<Box sx={{ minHeight: 220, flexGrow: 1 }}>
<SimpleTreeView apiRef={apiRef}>
<TreeItem itemId="grid" label="Data Grid">
<TreeItem itemId="grid-community" label="@mui/x-data-grid" />
<TreeItem itemId="grid-pro" label="@mui/x-data-grid-pro" />
<TreeItem itemId="grid-premium" label="@mui/x-data-grid-premium" />
</TreeItem>
<TreeItem itemId="pickers" label="Date and Time Pickers">
<TreeItem itemId="pickers-community" label="@mui/x-date-pickers" />
<TreeItem itemId="pickers-pro" label="@mui/x-date-pickers-pro" />
</TreeItem>
</SimpleTreeView>
</Box>
</Box>
);
}
6 changes: 6 additions & 0 deletions docs/data/tree-view/simple-tree-view/expansion/expansion.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen
Use the `onItemExpansionToggle` prop to trigger an action upon an item being expanded.

{{"demo": "TrackItemExpansionToggle.js"}}

## Change item expansion

You can use the `setItemExpansion` api method to imperatively change the expansion of an item:
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

{{"demo": "ChangeItemExpansion.js"}}
Original file line number Diff line number Diff line change
@@ -1,50 +1,56 @@
import * as React from 'react';
import useEventCallback from '@mui/utils/useEventCallback';
import { TreeViewPlugin } from '../../models';
import { populateInstance } from '../../useTreeView/useTreeView.utils';
import { populateInstance, populatePublicAPI } from '../../useTreeView/useTreeView.utils';
import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types';
import { TreeViewItemId } from '../../../models';

export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature> = ({
instance,
publicAPI,
params,
models,
}) => {
const expandedItemsMap = React.useMemo(() => {
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 took the opportunity to do this performance improvement now because in order to avoid code duplication between toggleItemExpansion and setItemExpansion, I need to check the expansion status twice and doing it with a linear complexity with awefull.

@romgrk do you have a preference on the data structure to use for this model? Knowing that for now it's purely internal and we keep the array for the expandedItems prop (we can reconsider in the future).

const temp = new Map<TreeViewItemId, boolean>();
models.expandedItems.value.forEach((id) => {
temp.set(id, true);
});

return temp;
}, [models.expandedItems.value]);

const setExpandedItems = (event: React.SyntheticEvent, value: string[]) => {
params.onExpandedItemsChange?.(event, value);
models.expandedItems.setControlledValue(value);
};

const isItemExpanded = React.useCallback(
(itemId: string) => {
return Array.isArray(models.expandedItems.value)
? models.expandedItems.value.indexOf(itemId) !== -1
: false;
},
[models.expandedItems.value],
(itemId: string) => expandedItemsMap.has(itemId),
[expandedItemsMap],
);

const isItemExpandable = React.useCallback(
(itemId: string) => !!instance.getNode(itemId)?.expandable,
[instance],
);

const toggleItemExpansion = useEventCallback(
(event: React.SyntheticEvent, itemId: string | null) => {
if (itemId == null) {
return;
}

const isExpandedBefore = models.expandedItems.value.indexOf(itemId!) !== -1;
const toggleItemExpansion = useEventCallback((event: React.SyntheticEvent, itemId: string) => {
const isExpandedBefore = instance.isItemExpanded(itemId);
instance.setItemExpansion(event, itemId, !isExpandedBefore);
});

const setItemExpansion = useEventCallback(
(event: React.SyntheticEvent, itemId: string, isExpanded: boolean) => {
let newExpanded: string[];
if (isExpandedBefore) {
newExpanded = models.expandedItems.value.filter((id) => id !== itemId);
} else {
if (isExpanded) {
newExpanded = [itemId].concat(models.expandedItems.value);
} else {
newExpanded = models.expandedItems.value.filter((id) => id !== itemId);
}

if (params.onItemExpansionToggle) {
params.onItemExpansionToggle(event, itemId, !isExpandedBefore);
params.onItemExpansionToggle(event, itemId, isExpanded);
}

setExpandedItems(event, newExpanded);
Expand Down Expand Up @@ -75,9 +81,12 @@ export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature>
populateInstance<UseTreeViewExpansionSignature>(instance, {
isItemExpanded,
isItemExpandable,
setItemExpansion,
toggleItemExpansion,
expandAllSiblings,
});

populatePublicAPI<UseTreeViewExpansionSignature>(publicAPI, { setItemExpansion });
};

useTreeViewExpansion.models = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@ import * as React from 'react';
import { DefaultizedProps, TreeViewPluginSignature } from '../../models';
import { UseTreeViewItemsSignature } from '../useTreeViewItems';

export interface UseTreeViewExpansionInstance {
export interface UseTreeViewExpansionPublicAPI {
Copy link
Member Author

Choose a reason for hiding this comment

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

@noraleonte I propose you to switch the logic for the definition of the instance / publicAPI and to instead define the method on XXXPublicAPI and use extend on XXXInstance.
That way it's easy to be sure that every public method has a good JSDoc.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's probably a much better way to do it 👍

/**
* Change the expansion status of a given item.
* @param {React.SyntheticEvent} event The UI event that triggered the change.
* @param {string} itemId The id of the item to modify.
* @param {boolean} isExpanded The new expansion status of the given item.
*/
setItemExpansion: (event: React.SyntheticEvent, itemId: string, isExpanded: boolean) => void;
Copy link
Member Author

Choose a reason for hiding this comment

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

The main DX question here is about the event

Do we require the user to pass an event? This is needed right now to call the onItemExpansionToggle prop.
If we don't, do we only call onItemExpansionToggle when the event is defined or do we make the event nullable on onItemExpansionToggle?

Copy link
Contributor

@noraleonte noraleonte Apr 3, 2024

Choose a reason for hiding this comment

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

Do we require the user to pass an event

I personally don't see a problem with keeping the event required here 🤔 I'm trying to think of a scenario where users would want to specifically avoid passing the event here 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

We could imagine scenarios where there is no event (expand an item when receiving some data from the server for instance).
But I'd be in favor of keeping the event mandatory and making it nullable if we have feedback of valid use-cases that ask for it.

}

export interface UseTreeViewExpansionInstance extends UseTreeViewExpansionPublicAPI {
isItemExpanded: (itemId: string) => boolean;
isItemExpandable: (itemId: string) => boolean;
toggleItemExpansion: (event: React.SyntheticEvent, value: string) => void;
toggleItemExpansion: (event: React.SyntheticEvent, itemId: string) => void;
LukasTy marked this conversation as resolved.
Show resolved Hide resolved
expandAllSiblings: (event: React.KeyboardEvent, itemId: string) => void;
}

Expand Down Expand Up @@ -49,6 +59,7 @@ export type UseTreeViewExpansionSignature = TreeViewPluginSignature<{
params: UseTreeViewExpansionParameters;
defaultizedParams: UseTreeViewExpansionDefaultizedParameters;
instance: UseTreeViewExpansionInstance;
publicAPI: UseTreeViewExpansionPublicAPI;
modelNames: 'expandedItems';
dependantPlugins: [UseTreeViewItemsSignature];
}>;