From 5fbedd101596311859f321b9da903be414f47573 Mon Sep 17 00:00:00 2001 From: delangle Date: Thu, 30 May 2024 09:03:37 +0200 Subject: [PATCH 01/16] [TreeView] Support item reordering using drag and drop --- docs/data/pages.ts | 1 + .../customization/CustomContentTreeView.js | 3 + .../customization/CustomContentTreeView.tsx | 3 + .../customization/FileExplorer.js | 3 + .../customization/FileExplorer.tsx | 3 + .../ApiMethodGetItemOrderedChildrenIds.js | 68 ++++ .../ApiMethodGetItemOrderedChildrenIds.tsx | 73 ++++ ...ethodGetItemOrderedChildrenIds.tsx.preview | 12 + .../items/ApiMethodGetItemTree.js | 66 ++++ .../items/ApiMethodGetItemTree.tsx | 66 ++++ .../items/ApiMethodGetItemTree.tsx.preview | 8 + .../tree-view/rich-tree-view/items/items.md | 28 ++ .../rich-tree-view/ordering/DragAndDrop.js | 47 +++ .../rich-tree-view/ordering/DragAndDrop.tsx | 47 +++ .../ordering/DragAndDrop.tsx.preview | 6 + .../ordering/OnItemPositionChange.js | 66 ++++ .../ordering/OnItemPositionChange.tsx | 74 ++++ .../ordering/OnlyReorderFromDragHandle.js | 112 ++++++ .../ordering/OnlyReorderFromDragHandle.tsx | 126 +++++++ .../OnlyReorderFromDragHandle.tsx.preview | 7 + .../ordering/OnlyReorderInSameParent.js | 50 +++ .../ordering/OnlyReorderInSameParent.tsx | 50 +++ .../OnlyReorderInSameParent.tsx.preview | 9 + .../ordering/OnlyReorderLeaves.js | 54 +++ .../ordering/OnlyReorderLeaves.tsx | 54 +++ .../ordering/OnlyReorderLeaves.tsx.preview | 10 + .../ordering/SendAllItemsToServer.js | 85 +++++ .../ordering/SendAllItemsToServer.tsx | 85 +++++ .../rich-tree-view/ordering/ordering.md | 68 ++++ .../pages/x/api/tree-view/rich-tree-view.json | 4 +- .../x/api/tree-view/simple-tree-view.json | 2 +- docs/pages/x/api/tree-view/tree-item-2.json | 8 +- docs/pages/x/api/tree-view/tree-item.json | 6 + docs/pages/x/api/tree-view/tree-view.json | 2 +- .../rich-tree-view/ordering.js | 7 + .../tree-view/tree-item-2/tree-item-2.json | 1 + .../tree-view/tree-item/tree-item.json | 4 + .../DateRangeCalendar.test.tsx | 2 +- .../hooks/useField/useField.utils.ts | 2 +- .../x-tree-view-pro/src/internals/index.ts | 1 + .../src/internals/plugins/defaultPlugins.ts | 8 +- .../useTreeViewItemsReordering/index.ts | 7 + .../useTreeViewItemsReordering.itemPlugin.ts | 152 ++++++++ .../useTreeViewItemsReordering.test.tsx | 349 ++++++++++++++++++ .../useTreeViewItemsReordering.ts | 273 ++++++++++++++ .../useTreeViewItemsReordering.types.ts | 158 ++++++++ .../useTreeViewItemsReordering.utils.ts | 182 +++++++++ .../src/RichTreeView/RichTreeView.tsx | 2 + .../SimpleTreeView/SimpleTreeView.plugins.ts | 45 ++- .../src/SimpleTreeView/SimpleTreeView.tsx | 2 + .../src/TreeItem/TreeItem.test.tsx | 8 +- .../x-tree-view/src/TreeItem/TreeItem.tsx | 37 +- .../src/TreeItem/TreeItemContent.tsx | 11 + .../src/TreeItem/treeItemClasses.ts | 3 + .../src/TreeItem/useTreeItemState.ts | 2 +- .../x-tree-view/src/TreeItem2/TreeItem2.tsx | 15 + .../src/TreeItem2/TreeItem2.types.ts | 7 + .../TreeItem2DragAndDropOverlay.tsx | 71 ++++ .../TreeItem2DragAndDropOverlay.types.ts | 7 + .../src/TreeItem2DragAndDropOverlay/index.ts | 2 + .../src/TreeItem2Icon/TreeItem2Icon.tsx | 2 +- .../TreeItem2Provider/TreeItem2Provider.tsx | 2 +- .../x-tree-view/src/TreeView/TreeView.tsx | 2 + .../useTreeItem2Utils/useTreeItem2Utils.tsx | 2 +- .../src/internals/TreeViewProvider/index.ts | 1 + packages/x-tree-view/src/internals/index.ts | 16 +- .../x-tree-view/src/internals/models/index.ts | 2 + .../src/internals/models/itemPlugin.ts | 51 +++ .../src/internals/models/plugin.ts | 20 +- .../plugins/useTreeViewItems/index.ts | 2 + .../useTreeViewItems/useTreeViewItems.tsx | 23 +- .../useTreeViewItems.types.ts | 18 +- .../useTreeViewJSXItems.tsx | 2 +- .../useTreeViewKeyboardNavigation.ts | 9 +- .../useTreeViewKeyboardNavigation.types.ts | 3 +- .../src/internals/useTreeView/useTreeView.ts | 41 ++ .../x-tree-view/src/internals/utils/tree.ts | 12 + packages/x-tree-view/src/models/items.ts | 6 + .../x-tree-view/src/useTreeItem2/index.ts | 5 + .../src/useTreeItem2/useTreeItem2.ts | 62 +++- .../src/useTreeItem2/useTreeItem2.types.ts | 30 +- scripts/x-tree-view.exports.json | 6 + test/utils/dragAndDrop.ts | 65 ++++ test/utils/pickers/calendar.ts | 67 +--- .../describeTreeView/describeTreeView.tsx | 29 +- .../describeTreeView.types.ts | 9 + 86 files changed, 3006 insertions(+), 145 deletions(-) create mode 100644 docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.js create mode 100644 docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx create mode 100644 docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx.preview create mode 100644 docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.js create mode 100644 docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx create mode 100644 docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx.preview create mode 100644 docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.js create mode 100644 docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx create mode 100644 docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx.preview create mode 100644 docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.js create mode 100644 docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.tsx create mode 100644 docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js create mode 100644 docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx create mode 100644 docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx.preview create mode 100644 docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.js create mode 100644 docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx create mode 100644 docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx.preview create mode 100644 docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.js create mode 100644 docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx create mode 100644 docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx.preview create mode 100644 docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.js create mode 100644 docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.tsx create mode 100644 docs/data/tree-view/rich-tree-view/ordering/ordering.md create mode 100644 docs/pages/x/react-tree-view/rich-tree-view/ordering.js create mode 100644 packages/x-tree-view-pro/src/internals/index.ts create mode 100644 packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/index.ts create mode 100644 packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts create mode 100644 packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx create mode 100644 packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts create mode 100644 packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts create mode 100644 packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts create mode 100644 packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.tsx create mode 100644 packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.types.ts create mode 100644 packages/x-tree-view/src/TreeItem2DragAndDropOverlay/index.ts create mode 100644 packages/x-tree-view/src/internals/models/itemPlugin.ts create mode 100644 test/utils/dragAndDrop.ts diff --git a/docs/data/pages.ts b/docs/data/pages.ts index ee4684125d361..dced408e18b3b 100644 --- a/docs/data/pages.ts +++ b/docs/data/pages.ts @@ -502,6 +502,7 @@ const pages: MuiPage[] = [ { 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' }, + { pathname: '/x/react-tree-view/rich-tree-view/ordering', plan: 'pro' }, ], }, { diff --git a/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.js b/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.js index 24ddeb962f48d..858b1e5110df5 100644 --- a/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.js +++ b/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.js @@ -15,6 +15,7 @@ import { } from '@mui/x-tree-view/TreeItem2'; import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; +import { TreeItem2DragAndDropOverlay } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'; const ITEMS = [ { @@ -50,6 +51,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { getCheckboxProps, getLabelProps, getGroupTransitionProps, + getDragAndDropOverlayProps, status, } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); @@ -74,6 +76,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { + {children && } diff --git a/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.tsx b/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.tsx index 0caea393d6977..b5f75932311e8 100644 --- a/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.tsx +++ b/docs/data/tree-view/rich-tree-view/customization/CustomContentTreeView.tsx @@ -18,6 +18,7 @@ import { } from '@mui/x-tree-view/TreeItem2'; import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; +import { TreeItem2DragAndDropOverlay } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'; const ITEMS: TreeViewBaseItem[] = [ { @@ -60,6 +61,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( getCheckboxProps, getLabelProps, getGroupTransitionProps, + getDragAndDropOverlayProps, status, } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); @@ -84,6 +86,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( + {children && } diff --git a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js index 4f11f68fa6e5c..e0db95bc4540a 100644 --- a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js +++ b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js @@ -25,6 +25,7 @@ import { } from '@mui/x-tree-view/TreeItem2'; import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; +import { TreeItem2DragAndDropOverlay } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'; const ITEMS = [ { @@ -212,6 +213,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { getCheckboxProps, getLabelProps, getGroupTransitionProps, + getDragAndDropOverlayProps, status, publicAPI, } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); @@ -245,6 +247,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { + {children && } diff --git a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx index b7361409735a6..df000f9ee091e 100644 --- a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx +++ b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx @@ -28,6 +28,7 @@ import { } from '@mui/x-tree-view/TreeItem2'; import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; +import { TreeItem2DragAndDropOverlay } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'; import { TreeViewBaseItem } from '@mui/x-tree-view/models'; type FileType = 'image' | 'pdf' | 'doc' | 'video' | 'folder' | 'pinned' | 'trash'; @@ -248,6 +249,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( getCheckboxProps, getLabelProps, getGroupTransitionProps, + getDragAndDropOverlayProps, status, publicAPI, } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); @@ -281,6 +283,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( + {children && } diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.js b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.js new file mode 100644 index 0000000000000..f42a0bee4ab08 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.js @@ -0,0 +1,68 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +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' }], + }, +]; + +export default function ApiMethodGetItemOrderedChildrenIds() { + const apiRef = useTreeViewApiRef(); + const [isSelectedItemLeaf, setIsSelectedItemLeaf] = React.useState(null); + + const handleSelectedItemsChange = (event, itemId) => { + if (itemId == null) { + setIsSelectedItemLeaf(null); + } else { + const children = apiRef.current.getItemOrderedChildrenIds(itemId); + setIsSelectedItemLeaf(children.length === 0); + } + }; + + return ( + + + {isSelectedItemLeaf == null && 'No item selected'} + {isSelectedItemLeaf === true && 'The selected item is a leaf'} + {isSelectedItemLeaf === false && 'The selected item is a node with children'} + + + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx new file mode 100644 index 0000000000000..a6d017c44bdac --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +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' }], + }, +]; + +export default function ApiMethodGetItemOrderedChildrenIds() { + const apiRef = useTreeViewApiRef(); + const [isSelectedItemLeaf, setIsSelectedItemLeaf] = React.useState( + null, + ); + + const handleSelectedItemsChange = ( + event: React.SyntheticEvent, + itemId: string | null, + ) => { + if (itemId == null) { + setIsSelectedItemLeaf(null); + } else { + const children = apiRef.current!.getItemOrderedChildrenIds(itemId); + setIsSelectedItemLeaf(children.length === 0); + } + }; + + return ( + + + {isSelectedItemLeaf == null && 'No item selected'} + {isSelectedItemLeaf === true && 'The selected item is a leaf'} + {isSelectedItemLeaf === false && 'The selected item is a node with children'} + + + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx.preview b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx.preview new file mode 100644 index 0000000000000..79e185eeca732 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx.preview @@ -0,0 +1,12 @@ + + {isSelectedItemLeaf == null && 'No item selected'} + {isSelectedItemLeaf === true && 'The selected item is a leaf'} + {isSelectedItemLeaf === false && 'The selected item is a node with children'} + + + + \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.js b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.js new file mode 100644 index 0000000000000..b30d1fab59217 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.js @@ -0,0 +1,66 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +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' }], + }, +]; + +export default function ApiMethodGetItemTree() { + const apiRef = useTreeViewApiRef(); + + const [items, setItems] = React.useState(MUI_X_PRODUCTS); + const [itemOnTop, setItemOnTop] = React.useState(items[0].label); + + const handleInvertItems = () => { + setItems((prevItems) => [...prevItems].reverse()); + }; + + const handleUpdateItemOnTop = () => { + setItemOnTop(apiRef.current.getItemTree()[0].label); + }; + + return ( + + + + + + Item on top: {itemOnTop} + + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx new file mode 100644 index 0000000000000..33c261e024514 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +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' }], + }, +]; + +export default function ApiMethodGetItemTree() { + const apiRef = useTreeViewApiRef(); + + const [items, setItems] = React.useState(MUI_X_PRODUCTS); + const [itemOnTop, setItemOnTop] = React.useState(items[0].label); + + const handleInvertItems = () => { + setItems((prevItems) => [...prevItems].reverse()); + }; + + const handleUpdateItemOnTop = () => { + setItemOnTop(apiRef.current!.getItemTree()[0].label); + }; + + return ( + + + + + + Item on top: {itemOnTop} + + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx.preview b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx.preview new file mode 100644 index 0000000000000..34006649c7754 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx.preview @@ -0,0 +1,8 @@ + + + + +Item on top: {itemOnTop} + + + \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/items/items.md b/docs/data/tree-view/rich-tree-view/items/items.md index 053934eabefae..604d910f22eeb 100644 --- a/docs/data/tree-view/rich-tree-view/items/items.md +++ b/docs/data/tree-view/rich-tree-view/items/items.md @@ -155,3 +155,31 @@ const item = apiRef.current.getItem( ``` {{"demo": "ApiMethodGetItem.js", "defaultCodeOpen": false}} + +### Get the current item tree + +Use the `getItemTree` API method to get the current item tree. + +```ts +const itemTree = apiRef.current.getItemTree(); +``` + +{{"demo": "ApiMethodGetItemTree.js", "defaultCodeOpen": false}} + +:::info +This method is mostly useful when the Tree View has some internal updates on the items. +For now, the only features causing updates on the items is the [re-ordering](/x/react-tree-view/rich-tree-view/ordering/). +::: + +### Get an item's children by ID + +Use the `getItemOrderedChildrenIds` API method to get an item's children by its ID. + +```ts +const childrenIds = apiRef.current.getItemOrderedChildrenIds( + // The ID of the item to retrieve the children from + itemId, +); +``` + +{{"demo": "ApiMethodGetItemOrderedChildrenIds.js", "defaultCodeOpen": false}} diff --git a/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.js b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.js new file mode 100644 index 0000000000000..8ef54b5774a29 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.js @@ -0,0 +1,47 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; + +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; + +const ITEMS = [ + { + 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 DragAndDrop() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx new file mode 100644 index 0000000000000..8e4801fd27788 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; + +const ITEMS: 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 DragAndDrop() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx.preview b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx.preview new file mode 100644 index 0000000000000..b1fe1bcaede71 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx.preview @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.js b/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.js new file mode 100644 index 0000000000000..9b37121a4a2c5 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.js @@ -0,0 +1,66 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; + +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +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 OnItemPositionChange() { + const [lastReorder, setLastReorder] = React.useState(null); + + return ( + + + setLastReorder(params)} + /> + + {lastReorder == null ? ( + No reorder registered yet + ) : ( + + Last reordered item: {lastReorder.itemId} +
+ Position before: {lastReorder.oldPosition.parentId ?? 'root'} (index{' '} + {lastReorder.oldPosition.index})
+ Position after: {lastReorder.newPosition.parentId ?? 'root'} (index{' '} + {lastReorder.newPosition.index}) +
+ )} +
+ ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.tsx b/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.tsx new file mode 100644 index 0000000000000..27977b8564215 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; +import { + RichTreeViewPro, + RichTreeViewProProps, +} from '@mui/x-tree-view-pro/RichTreeViewPro'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +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 OnItemPositionChange() { + const [lastReorder, setLastReorder] = React.useState< + | Parameters< + NonNullable['onItemPositionChange']> + >[0] + | null + >(null); + + return ( + + + setLastReorder(params)} + /> + + {lastReorder == null ? ( + No reorder registered yet + ) : ( + + Last reordered item: {lastReorder.itemId} +
+ Position before: {lastReorder.oldPosition.parentId ?? 'root'} (index{' '} + {lastReorder.oldPosition.index})
+ Position after: {lastReorder.newPosition.parentId ?? 'root'} (index{' '} + {lastReorder.newPosition.index}) +
+ )} +
+ ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js new file mode 100644 index 0000000000000..5fa432ee96f49 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js @@ -0,0 +1,112 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; + +import { unstable_useTreeItem2 as useTreeItem2 } from '@mui/x-tree-view/useTreeItem2'; +import { + TreeItem2Content, + TreeItem2IconContainer, + TreeItem2GroupTransition, + TreeItem2Label, + TreeItem2Root, + TreeItem2Checkbox, +} from '@mui/x-tree-view/TreeItem2'; +import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; +import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; +import { TreeItem2DragAndDropOverlay } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'; + +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' }], + }, +]; + +const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { + const { id, itemId, label, disabled, children, ...other } = props; + + const { + getRootProps, + getContentProps, + getIconContainerProps, + getCheckboxProps, + getLabelProps, + getGroupTransitionProps, + getDragAndDropOverlayProps, + status, + } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); + + const { draggable, onDragStart, onDragOver, onDragEnd, ...otherRootProps } = + getRootProps(other); + + const handleDragStart = (event) => { + if (!onDragStart) { + return; + } + + onDragStart(event); + event.dataTransfer.setDragImage(event.target.parentElement, 0, 0); + }; + + return ( + + + + + + + + + + + + + + {children && } + + + ); +}); + +export default function OnlyReorderFromDragHandle() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx new file mode 100644 index 0000000000000..01fcff4f73ecf --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx @@ -0,0 +1,126 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; +import { + unstable_useTreeItem2 as useTreeItem2, + UseTreeItem2Parameters, +} from '@mui/x-tree-view/useTreeItem2'; +import { + TreeItem2Content, + TreeItem2IconContainer, + TreeItem2GroupTransition, + TreeItem2Label, + TreeItem2Root, + TreeItem2Checkbox, +} from '@mui/x-tree-view/TreeItem2'; +import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; +import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; +import { TreeItem2DragAndDropOverlay } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'; + +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' }], + }, +]; + +interface CustomTreeItemProps + extends Omit, + Omit, 'onFocus'> {} + +const CustomTreeItem = React.forwardRef(function CustomTreeItem( + props: CustomTreeItemProps, + ref: React.Ref, +) { + const { id, itemId, label, disabled, children, ...other } = props; + + const { + getRootProps, + getContentProps, + getIconContainerProps, + getCheckboxProps, + getLabelProps, + getGroupTransitionProps, + getDragAndDropOverlayProps, + status, + } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); + + const { draggable, onDragStart, onDragOver, onDragEnd, ...otherRootProps } = + getRootProps(other); + + const handleDragStart = (event: React.DragEvent) => { + if (!onDragStart) { + return; + } + + onDragStart(event); + event.dataTransfer.setDragImage( + (event.target as HTMLElement).parentElement!, + 0, + 0, + ); + }; + + return ( + + + + + + + + + + + + + + {children && } + + + ); +}); + +export default function OnlyReorderFromDragHandle() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx.preview b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx.preview new file mode 100644 index 0000000000000..12753c6f59fe6 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx.preview @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.js b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.js new file mode 100644 index 0000000000000..e971e8d858560 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.js @@ -0,0 +1,50 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; + +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; + +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 OnlyReorderInSameParent() { + return ( + + + params.oldPosition.parentId === params.newPosition.parentId + } + /> + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx new file mode 100644 index 0000000000000..ab82c5561b9bd --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; + +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 OnlyReorderInSameParent() { + return ( + + + params.oldPosition.parentId === params.newPosition.parentId + } + /> + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx.preview b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx.preview new file mode 100644 index 0000000000000..cfde0e44e822f --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx.preview @@ -0,0 +1,9 @@ + + params.oldPosition.parentId === params.newPosition.parentId + } +/> \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.js b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.js new file mode 100644 index 0000000000000..4d2dba8d20e69 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.js @@ -0,0 +1,54 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; + +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; +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' }], + }, +]; + +export default function OnlyReorderLeaves() { + const apiRef = useTreeViewApiRef(); + + return ( + + + apiRef.current.getItemOrderedChildrenIds(itemId).length === 0 + } + /> + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx new file mode 100644 index 0000000000000..7fd556d782253 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; +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' }], + }, +]; + +export default function OnlyReorderLeaves() { + const apiRef = useTreeViewApiRef(); + + return ( + + + apiRef.current!.getItemOrderedChildrenIds(itemId).length === 0 + } + /> + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx.preview b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx.preview new file mode 100644 index 0000000000000..a3271fa077200 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx.preview @@ -0,0 +1,10 @@ + + apiRef.current!.getItemOrderedChildrenIds(itemId).length === 0 + } +/> \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.js b/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.js new file mode 100644 index 0000000000000..5c596f1eeaedf --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.js @@ -0,0 +1,85 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; + +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; +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' }], + }, +]; + +const getAllItemsWithChildrenItemIds = (items) => { + const itemIds = []; + const registerItemId = (item) => { + if (item.children?.length) { + itemIds.push(item.id); + item.children.forEach(registerItemId); + } + }; + + items.forEach(registerItemId); + + return itemIds; +}; + +export default function SendAllItemsToServer() { + const apiRefTreeViewA = useTreeViewApiRef(); + const [itemsTreeViewB, setItemsTreeViewB] = React.useState(MUI_X_PRODUCTS); + + const handleItemPositionChangeTreeViewA = () => { + // We need to wait for the new items to be updated in the state + setTimeout(() => { + const newItemsTreeViewA = apiRefTreeViewA.current.getItemTree(); + setItemsTreeViewB(newItemsTreeViewA); + }); + }; + + return ( + + + + + + true} + /> + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.tsx b/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.tsx new file mode 100644 index 0000000000000..78152018396a7 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; +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' }], + }, +]; + +const getAllItemsWithChildrenItemIds = (items: TreeViewBaseItem[]) => { + const itemIds: string[] = []; + const registerItemId = (item: TreeViewBaseItem) => { + if (item.children?.length) { + itemIds.push(item.id); + item.children.forEach(registerItemId); + } + }; + + items.forEach(registerItemId); + + return itemIds; +}; + +export default function SendAllItemsToServer() { + const apiRefTreeViewA = useTreeViewApiRef(); + const [itemsTreeViewB, setItemsTreeViewB] = React.useState(MUI_X_PRODUCTS); + + const handleItemPositionChangeTreeViewA = () => { + // We need to wait for the new items to be updated in the state + setTimeout(() => { + const newItemsTreeViewA = apiRefTreeViewA.current!.getItemTree(); + setItemsTreeViewB(newItemsTreeViewA); + }); + }; + + return ( + + + + + + true} + /> + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/ordering.md b/docs/data/tree-view/rich-tree-view/ordering/ordering.md new file mode 100644 index 0000000000000..5c9a169649e24 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/ordering.md @@ -0,0 +1,68 @@ +--- +productId: x-tree-view +title: Rich Tree View - Ordering +components: RichTreeView, TreeItem2 +packageName: '@mui/x-tree-view' +githubLabel: 'component: tree view' +waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ +--- + +# Rich Tree View - Ordering [](/x/introduction/licensing/#pro-plan 'Pro plan') + +

Drag and drop your items to reorder them.

+ +:::success +To be able to reorder items, you first have to enable the `indentationAtItemLevel` experimental feature. +When this flag is enabled, the indentation of nested items is applied by the item itself instead of its ancestors. +This allows correctly placing the drag & drop overlay. + +You can enable it as follows: + +```tsx + +``` + +If you are building your custom Tree Item, make sure that you do not apply any custom `padding-left` on your Tree Item Content component. +If you want to change the indentation value, you can use the `itemChildrenIndentation` prop on the Tree View component. +::: + +## Enable drag & drop re-ordering + +You can enable the drag & drop re-ordering of items by setting the `itemsReordering` prop to `true`: + +{{"demo": "DragAndDrop.js"}} + +## Limit the re-ordering + +By default, all the items are reorderable. +You can prevent the re-ordering of some items using the `isItemReorderable` prop. +The following example shows how to only allow re-ordering of the leaves using the [`getItemOrderedChildrenIds`](/x/react-tree-view/rich-tree-view/items/#get-an-items-children-by-id) API method. + +{{"demo": "OnlyReorderLeaves.js"}} + +You can also limit the items in which an item can be dropped using the `canMoveItemToNewPosition` prop. +The following example shows how to only allow re-ordering inside the same parent: + +{{"demo": "OnlyReorderInSameParent.js"}} + +## React to an item re-ordering + +You can use the `onItemPositionChange` to send the new position of an item to your backend: + +{{"demo": "OnItemPositionChange.js"}} + +If you want to send the entire dataset to your backend, you can use the [`getItemTree`](/x/react-tree-view/rich-tree-view/items/#get-the-current-item-tree) API method. +The following demo demonstrates it by synchronizing the first tree view with the second one whenever you do a re-ordering: + +{{"demo": "SendAllItemsToServer.js"}} + +## Customization + +### Only trigger the reordering from a drag handle + +You can create a custom Tree Item component to render a drag handle icon and only trigger the reordering when dragging from it: + +{{"demo": "OnlyReorderFromDragHandle.js"}} diff --git a/docs/pages/x/api/tree-view/rich-tree-view.json b/docs/pages/x/api/tree-view/rich-tree-view.json index 7da4e28d56f04..64a239b7dc498 100644 --- a/docs/pages/x/api/tree-view/rich-tree-view.json +++ b/docs/pages/x/api/tree-view/rich-tree-view.json @@ -3,7 +3,7 @@ "apiRef": { "type": { "name": "shape", - "description": "{ current?: { focusItem: func, getItem: func, setItemExpansion: func } }" + "description": "{ current?: { focusItem: func, getItem: func, getItemOrderedChildrenIds: func, getItemTree: func, setItemExpansion: func } }" } }, "checkboxSelection": { "type": { "name": "bool" }, "default": "false" }, @@ -142,6 +142,6 @@ "forwardsRefTo": "HTMLUListElement", "filename": "/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/x/api/tree-view/simple-tree-view.json b/docs/pages/x/api/tree-view/simple-tree-view.json index 1c52f9e03646b..299de39edc821 100644 --- a/docs/pages/x/api/tree-view/simple-tree-view.json +++ b/docs/pages/x/api/tree-view/simple-tree-view.json @@ -3,7 +3,7 @@ "apiRef": { "type": { "name": "shape", - "description": "{ current?: { focusItem: func, getItem: func, setItemExpansion: func } }" + "description": "{ current?: { focusItem: func, getItem: func, getItemOrderedChildrenIds: func, getItemTree: func, setItemExpansion: func } }" } }, "checkboxSelection": { "type": { "name": "bool" }, "default": "false" }, diff --git a/docs/pages/x/api/tree-view/tree-item-2.json b/docs/pages/x/api/tree-view/tree-item-2.json index 5a3135b2b98b4..2a95cd31f11cd 100644 --- a/docs/pages/x/api/tree-view/tree-item-2.json +++ b/docs/pages/x/api/tree-view/tree-item-2.json @@ -56,6 +56,12 @@ "default": "TreeItem2Label", "class": "MuiTreeItem2-label" }, + { + "name": "dragAndDropOverlay", + "description": "The component that renders the overlay when an item reordering is ongoing.\nWarning: This slot is only useful when using the `RichTreeViewPro` component.", + "default": "TreeItem2DragAndDropOverlay", + "class": "MuiTreeItem2-dragAndDropOverlay" + }, { "name": "collapseIcon", "description": "The icon used to collapse the item.", "class": null }, { "name": "expandIcon", "description": "The icon used to expand the item.", "class": null }, { "name": "endIcon", "description": "The icon displayed next to an end item.", "class": null }, @@ -94,6 +100,6 @@ "muiName": "MuiTreeItem2", "filename": "/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/x/api/tree-view/tree-item.json b/docs/pages/x/api/tree-view/tree-item.json index 6173f6442d930..7d6d77348a0dc 100644 --- a/docs/pages/x/api/tree-view/tree-item.json +++ b/docs/pages/x/api/tree-view/tree-item.json @@ -65,6 +65,12 @@ "description": "State class applied to the element when disabled.", "isGlobal": true }, + { + "key": "dragAndDropOverlay", + "className": "MuiTreeItem-dragAndDropOverlay", + "description": "Styles applied to the drag and drop overlay.", + "isGlobal": false + }, { "key": "expanded", "className": "Mui-expanded", diff --git a/docs/pages/x/api/tree-view/tree-view.json b/docs/pages/x/api/tree-view/tree-view.json index a528b9927844b..cab56ecb56137 100644 --- a/docs/pages/x/api/tree-view/tree-view.json +++ b/docs/pages/x/api/tree-view/tree-view.json @@ -3,7 +3,7 @@ "apiRef": { "type": { "name": "shape", - "description": "{ current?: { focusItem: func, getItem: func, setItemExpansion: func } }" + "description": "{ current?: { focusItem: func, getItem: func, getItemOrderedChildrenIds: func, getItemTree: func, setItemExpansion: func } }" } }, "checkboxSelection": { "type": { "name": "bool" }, "default": "false" }, diff --git a/docs/pages/x/react-tree-view/rich-tree-view/ordering.js b/docs/pages/x/react-tree-view/rich-tree-view/ordering.js new file mode 100644 index 0000000000000..72994d240844b --- /dev/null +++ b/docs/pages/x/react-tree-view/rich-tree-view/ordering.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docsx/data/tree-view/rich-tree-view/ordering/ordering.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json b/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json index 47b99c2cf4d8c..8b84bdc46080b 100644 --- a/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json +++ b/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json @@ -39,6 +39,7 @@ "checkbox": "The component that renders the item checkbox for selection.", "collapseIcon": "The icon used to collapse the item.", "content": "The component that renders the content of the item. (e.g.: everything related to this item, not to its children).", + "dragAndDropOverlay": "The component that renders the overlay when an item reordering is ongoing. Warning: This slot is only useful when using the RichTreeViewPro component.", "endIcon": "The icon displayed next to an end item.", "expandIcon": "The icon used to expand the item.", "groupTransition": "The component that renders the children of the item.", diff --git a/docs/translations/api-docs/tree-view/tree-item/tree-item.json b/docs/translations/api-docs/tree-view/tree-item/tree-item.json index 88c3575d3fb96..ff5c8e94e8f02 100644 --- a/docs/translations/api-docs/tree-view/tree-item/tree-item.json +++ b/docs/translations/api-docs/tree-view/tree-item/tree-item.json @@ -34,6 +34,10 @@ "nodeName": "the element", "conditions": "disabled" }, + "dragAndDropOverlay": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the drag and drop overlay" + }, "expanded": { "description": "State class applied to {{nodeName}} when {{conditions}}.", "nodeName": "the content element", diff --git a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.test.tsx b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.test.tsx index 1947567216c9c..8bd250efb64e6 100644 --- a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.test.tsx +++ b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.test.tsx @@ -11,11 +11,11 @@ import { import { adapterToUse, buildPickerDragInteractions, - MockedDataTransfer, rangeCalendarDayTouches, createPickerRenderer, wrapPickerMount, } from 'test/utils/pickers'; +import { MockedDataTransfer } from 'test/utils/dragAndDrop'; import { DateRangeCalendar, dateRangeCalendarClasses as classes, diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts index c12ffc2c1ab83..1b1fd2a2d9760 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts @@ -745,7 +745,7 @@ export const mergeDateIntoReferenceDate = ( return mergedDate; }, referenceDate); -export const isAndroid = () => navigator.userAgent.toLowerCase().indexOf('android') > -1; +export const isAndroid = () => navigator.userAgent.toLowerCase().includes('android'); // TODO v8: Remove if we drop the v6 TextField approach. export const getSectionOrder = ( diff --git a/packages/x-tree-view-pro/src/internals/index.ts b/packages/x-tree-view-pro/src/internals/index.ts new file mode 100644 index 0000000000000..cb9559d5f241f --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/index.ts @@ -0,0 +1 @@ +export { UseTreeViewItemsReorderingSignature } from './plugins/useTreeViewItemsReordering'; diff --git a/packages/x-tree-view-pro/src/internals/plugins/defaultPlugins.ts b/packages/x-tree-view-pro/src/internals/plugins/defaultPlugins.ts index 8f12846c5043a..2391f429448ad 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/defaultPlugins.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/defaultPlugins.ts @@ -15,6 +15,10 @@ import { ConvertPluginsIntoSignatures, MergePluginsProperty, } from '@mui/x-tree-view/internals'; +import { + useTreeViewItemsReordering, + UseTreeViewItemsReorderingParameters, +} from './useTreeViewItemsReordering'; export const DEFAULT_TREE_VIEW_PRO_PLUGINS = [ useTreeViewId, @@ -24,6 +28,7 @@ export const DEFAULT_TREE_VIEW_PRO_PLUGINS = [ useTreeViewFocus, useTreeViewKeyboardNavigation, useTreeViewIcons, + useTreeViewItemsReordering, ] as const; export type DefaultTreeViewProPlugins = ConvertPluginsIntoSignatures< @@ -49,4 +54,5 @@ export interface DefaultTreeViewProPluginParameters< UseTreeViewExpansionParameters, UseTreeViewFocusParameters, UseTreeViewSelectionParameters, - UseTreeViewIconsParameters {} + UseTreeViewIconsParameters, + UseTreeViewItemsReorderingParameters {} diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/index.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/index.ts new file mode 100644 index 0000000000000..517ac7914e588 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/index.ts @@ -0,0 +1,7 @@ +export { useTreeViewItemsReordering } from './useTreeViewItemsReordering'; +export type { + UseTreeViewItemsReorderingSignature, + UseTreeViewItemsReorderingParameters, + UseTreeViewItemsReorderingDefaultizedParameters, + TreeViewItemReorderPosition, +} from './useTreeViewItemsReordering.types'; diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts new file mode 100644 index 0000000000000..e522f4c7bf0e8 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts @@ -0,0 +1,152 @@ +import * as React from 'react'; +import { + MuiCancellableEvent, + TreeViewItemPlugin, + useTreeViewContext, + UseTreeViewItemsSignature, + isEventTargetInDescendants, +} from '@mui/x-tree-view/internals'; +import { TreeItem2Props } from '@mui/x-tree-view/TreeItem2'; +import { + UseTreeItem2DragAndDropOverlaySlotPropsFromItemsReordering, + UseTreeItem2RootSlotPropsFromItemsReordering, + UseTreeViewItemsReorderingSignature, + TreeViewItemItemReorderingValidActions, + UseTreeItem2ContentSlotPropsFromItemsReordering, +} from './useTreeViewItemsReordering.types'; + +export const isAndroid = () => navigator.userAgent.toLowerCase().includes('android'); + +export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ + props, +}) => { + const { itemsReordering, instance } = + useTreeViewContext<[UseTreeViewItemsSignature, UseTreeViewItemsReorderingSignature]>(); + const { itemId } = props; + + const validActionsRef = React.useRef(null); + + return { + propsEnhancers: { + root: ({ + rootRefObject, + contentRefObject, + externalEventHandlers, + }): UseTreeItem2RootSlotPropsFromItemsReordering => { + const draggable = instance.canItemBeDragged(itemId); + if (!draggable) { + return {}; + } + + const handleDragStart = (event: React.DragEvent & MuiCancellableEvent) => { + externalEventHandlers.onDragStart?.(event); + if (event.defaultMuiPrevented || event.defaultPrevented) { + return; + } + + // We don't use `event.currentTarget` here. + // This is to allow people to pass `onDragStart` to another element than the root. + if (isEventTargetInDescendants(event, rootRefObject.current)) { + return; + } + + // Comment to show the children in the drag preview + // TODO: Improve the customization of the drag preview + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setDragImage(contentRefObject.current!, 0, 0); + + const { types } = event.dataTransfer; + if (isAndroid() && !types.includes('text/plain') && !types.includes('text/uri-list')) { + event.dataTransfer.setData('text/plain', 'android-fallback'); + } + + instance.startDraggingItem(itemId); + }; + + const handleRootDragOver = (event: React.DragEvent & MuiCancellableEvent) => { + externalEventHandlers.onDragOver?.(event); + if (event.defaultMuiPrevented) { + return; + } + + event.preventDefault(); + }; + + const handleRootDragEnd = (event: React.DragEvent & MuiCancellableEvent) => { + externalEventHandlers.onDragEnd?.(event); + if (event.defaultMuiPrevented) { + return; + } + + instance.stopDraggingItem(itemId); + }; + + return { + draggable: true, + onDragStart: handleDragStart, + onDragOver: handleRootDragOver, + onDragEnd: handleRootDragEnd, + }; + }, + content: ({ + externalEventHandlers, + contentRefObject, + }): UseTreeItem2ContentSlotPropsFromItemsReordering => { + const currentDrag = itemsReordering.currentDrag; + if (!currentDrag || currentDrag.draggedItemId === itemId) { + return {}; + } + + const handleDragOver = (event: React.DragEvent & MuiCancellableEvent) => { + externalEventHandlers.onDragOver?.(event); + if (event.defaultMuiPrevented || validActionsRef.current == null) { + return; + } + + const rect = (event.target as HTMLDivElement).getBoundingClientRect(); + const y = event.clientY - rect.top; + const x = event.clientX - rect.left; + instance.setDragTargetItem({ + itemId, + validActions: validActionsRef.current, + targetHeight: rect.height, + cursorY: y, + cursorX: x, + contentElement: contentRefObject.current!, + }); + }; + + const handleDragEnter = (event: React.DragEvent & MuiCancellableEvent) => { + externalEventHandlers.onDragEnter?.(event); + if (event.defaultMuiPrevented) { + return; + } + + validActionsRef.current = instance.getDroppingTargetValidActions(itemId); + }; + + return { + onDragEnter: handleDragEnter, + onDragOver: handleDragOver, + }; + }, + dragAndDropOverlay: (): UseTreeItem2DragAndDropOverlaySlotPropsFromItemsReordering => { + const currentDrag = itemsReordering.currentDrag; + if (!currentDrag || currentDrag.targetItemId !== itemId || currentDrag.action == null) { + return {}; + } + + const targetDepth = + currentDrag.newPosition?.parentId == null + ? 0 + : // The depth is always defined because drag&drop is only usable with Rich Tree View components. + instance.getItemMeta(currentDrag.newPosition.parentId).depth! + 1; + + return { + action: currentDrag.action, + style: { '--TreeView-targetDepth': targetDepth } as React.CSSProperties, + }; + }, + }, + }; +}; diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx new file mode 100644 index 0000000000000..95fa1bf2b47f5 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx @@ -0,0 +1,349 @@ +import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { fireEvent, createEvent } from '@mui/internal-test-utils'; +import { UseTreeViewItemsReorderingSignature } from '@mui/x-tree-view-pro/internals'; +import { DragEventTypes, MockedDataTransfer } from 'test/utils/dragAndDrop'; +import { UseTreeViewItemsSignature } from '@mui/x-tree-view/internals'; +import { chooseActionToApply } from './useTreeViewItemsReordering.utils'; +import { TreeViewItemItemReorderingValidActions } from './useTreeViewItemsReordering.types'; + +interface DragEventOptions { + /** + * Coordinates of the mouse pointer relative to the target element. + * @default: { x: targetWidth / 2, y: targetHeight / 2 } + */ + coordinates?: { x: number; y: number }; +} + +const buildTreeViewDragInteractions = (dataTransfer: DataTransfer) => { + const createFireEvent = + (type: DragEventTypes) => + (target: HTMLElement, options: DragEventOptions = {}) => { + const rect = target.getBoundingClientRect(); + const coordinates = options.coordinates ?? { x: rect.width / 2, y: rect.height / 2 }; + const createdEvent = createEvent[type](target, { + clientX: rect.left + coordinates.x, + clientY: rect.top + coordinates.y, + }); + Object.defineProperty(createdEvent, 'dataTransfer', { + value: dataTransfer, + }); + + return fireEvent(target, createdEvent); + }; + + const dragStart = createFireEvent('dragStart'); + const dragEnter = createFireEvent('dragEnter'); + const dragOver = createFireEvent('dragOver'); + const dragEnd = createFireEvent('dragEnd'); + + return { + fullDragSequence: ( + draggedItem: HTMLElement, + targetItem: HTMLElement, + options: DragEventOptions = {}, + ) => { + dragStart(draggedItem); + dragEnter(targetItem); + dragOver(targetItem, { coordinates: options.coordinates }); + dragEnd(draggedItem); + }, + }; +}; + +describeTreeView<[UseTreeViewItemsReorderingSignature, UseTreeViewItemsSignature]>( + 'useTreeViewKeyboardNavigation', + ({ render, treeViewComponentName }) => { + if (treeViewComponentName === 'SimpleTreeView' || treeViewComponentName === 'RichTreeView') { + return; + } + + let dragEvents: ReturnType; + // eslint-disable-next-line mocha/no-top-level-hooks + beforeEach(() => { + const dataTransfer = new MockedDataTransfer(); + dragEvents = buildTreeViewDragInteractions(dataTransfer); + }); + + // eslint-disable-next-line mocha/no-top-level-hooks + afterEach(() => { + dragEvents = {} as typeof dragEvents; + }); + + describe('itemReordering prop', () => { + it('should allow to drag and drop items when props.itemsReordering={true}', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([ + { id: '2', children: [{ id: '1' }] }, + { id: '3' }, + ]); + }); + + it('should not allow to drag and drop items when props.itemsReordering={false}', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: false, + }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + }); + + it('should not allow to drag and drop items when props.itemsReordering is not defined', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + }); + }); + + describe('onItemPositionChange prop', () => { + it('should call onItemPositionChange when an item is moved', () => { + const onItemPositionChange = spy(); + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + onItemPositionChange, + }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(onItemPositionChange.callCount).to.equal(1); + expect(onItemPositionChange.lastCall.firstArg).to.deep.equal({ + itemId: '1', + oldPosition: { parentId: null, index: 0 }, + newPosition: { parentId: '2', index: 0 }, + }); + }); + }); + + describe('isItemReorderable prop', () => { + it('should not allow to drag an item when isItemReorderable returns false', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + canMoveItemToNewPosition: () => false, + }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + }); + + it('should allow to drag an item when isItemReorderable returns true', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + canMoveItemToNewPosition: () => true, + }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([ + { id: '2', children: [{ id: '1' }] }, + { id: '3' }, + ]); + }); + }); + + describe('canMoveItemToNewPosition prop', () => { + it('should not allow to drop an item when canMoveItemToNewPosition returns false', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + canMoveItemToNewPosition: () => false, + }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + }); + + it('should allow to drop an item when canMoveItemToNewPosition returns true', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + canMoveItemToNewPosition: () => true, + }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([ + { id: '2', children: [{ id: '1' }] }, + { id: '3' }, + ]); + }); + }); + }, +); + +describe('getNewPosition util', () => { + // The actions use the following tree when dropping "1.1" on "1.2": + // - 1 + // - 1.1 + // - 1.2 + // - 1.3 + // - 2 + const ALL_ACTIONS: TreeViewItemItemReorderingValidActions = { + 'reorder-above': { parentId: '1', index: 0 }, + 'reorder-below': { parentId: '1', index: 1 }, + 'make-child': { parentId: '1.2', index: 0 }, + 'move-to-parent': { parentId: null, index: 2 }, + }; + + const FAKE_CONTENT_ELEMENT = {} as HTMLDivElement; + + const COMMON_PROPERTIES = { + itemChildrenIndentation: 12, + validActions: ALL_ACTIONS, + targetHeight: 100, + targetDepth: 1, + cursorY: 50, + cursorX: 100, + contentElement: FAKE_CONTENT_ELEMENT, + }; + + it('should choose the "reorder-above" action when the cursor is in the top quarter of the target item', () => { + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 1, + }), + ).to.equal('reorder-above'); + + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 24, + }), + ).to.equal('reorder-above'); + }); + + it('should choose the "reorder-above" action when the cursor is in the top half of the target item and the "make-child" action is not valid', () => { + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 25, + validActions: { ...ALL_ACTIONS, 'make-child': undefined }, + }), + ).to.equal('reorder-above'); + + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 49, + validActions: { ...ALL_ACTIONS, 'make-child': undefined }, + }), + ).to.equal('reorder-above'); + }); + + it('should choose the "reorder-below" action when the cursor is in the bottom quarter of the target item', () => { + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 99, + }), + ).to.equal('reorder-below'); + + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 76, + }), + ).to.equal('reorder-below'); + }); + + it('should choose the "reorder-below" action when the cursor is in the bottom half of the target item and the "make-child" action is not valid', () => { + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 75, + validActions: { ...ALL_ACTIONS, 'make-child': undefined }, + }), + ).to.equal('reorder-below'); + + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 51, + validActions: { ...ALL_ACTIONS, 'make-child': undefined }, + }), + ).to.equal('reorder-below'); + + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 50, + validActions: { ...ALL_ACTIONS, 'make-child': undefined }, + }), + ).to.equal('reorder-below'); + }); + + it('should choose the "make-child" action when the cursor is in the middle of the target item', () => { + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 25, + }), + ).to.equal('make-child'); + + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 50, + }), + ).to.equal('make-child'); + + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorY: 74, + }), + ).to.equal('make-child'); + }); + + it('should choose the "move-to-parent" action when the cursor is inside the depth-offset of the target item', () => { + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorX: 1, + cursorY: 1, + }), + ).to.equal('move-to-parent'); + + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorX: 11, + cursorY: 1, + }), + ).to.equal('move-to-parent'); + + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorX: 1, + cursorY: 50, + }), + ).to.equal('move-to-parent'); + + expect( + chooseActionToApply({ + ...COMMON_PROPERTIES, + cursorX: 1, + cursorY: 99, + }), + ).to.equal('move-to-parent'); + }); +}); diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts new file mode 100644 index 0000000000000..e378fca5c73d2 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts @@ -0,0 +1,273 @@ +import * as React from 'react'; +import { buildWarning, TreeViewPlugin } from '@mui/x-tree-view/internals'; +import { TreeViewItemsReorderingAction } from '@mui/x-tree-view/models'; +import { + TreeViewItemItemReorderingValidActions, + TreeViewItemReorderPosition, + UseTreeViewItemsReorderingInstance, + UseTreeViewItemsReorderingSignature, +} from './useTreeViewItemsReordering.types'; +import { + chooseActionToApply, + isAncestor, + moveItemInTree, +} from './useTreeViewItemsReordering.utils'; +import { useTreeViewItemsReorderingItemPlugin } from './useTreeViewItemsReordering.itemPlugin'; + +const wrongIndentationStrategyWarning = buildWarning([ + 'MUI X: The drag and drop feature requires the `indentationAtItemLevel` experimental feature to be enabled.', + 'You can do it by passing `experimentalFeatures={{ indentationAtItemLevel: true }}` to the `RichTreeViewPro` component.', + 'Check the documentation for more details: https://mui.com/x/react-tree-view/rich-tree-view/items/', +]); + +export const useTreeViewItemsReordering: TreeViewPlugin = ({ + params, + instance, + state, + setState, + experimentalFeatures, +}) => { + if (process.env.NODE_END !== 'production') { + if (params.itemsReordering && !experimentalFeatures?.indentationAtItemLevel) { + wrongIndentationStrategyWarning(); + } + } + + const canItemBeDragged = React.useCallback( + (itemId: string) => { + if (!params.itemsReordering) { + return false; + } + + const isItemReorderable = params.isItemReorderable; + if (isItemReorderable) { + return isItemReorderable(itemId); + } + + return true; + }, + [params.itemsReordering, params.isItemReorderable], + ); + + const getDroppingTargetValidActions = React.useCallback( + (itemId: string) => { + if (!state.itemsReordering) { + throw new Error('There is no ongoing reordering.'); + } + + if (itemId === state.itemsReordering.draggedItemId) { + return {}; + } + + const canMoveItemToNewPosition = params.canMoveItemToNewPosition; + const targetItemMeta = instance.getItemMeta(itemId); + const targetItemIndex = instance.getItemIndex(targetItemMeta.id); + const draggedItemMeta = instance.getItemMeta(state.itemsReordering.draggedItemId); + const draggedItemIndex = instance.getItemIndex(draggedItemMeta.id); + + const oldPosition: TreeViewItemReorderPosition = { + parentId: draggedItemMeta.parentId, + index: draggedItemIndex, + }; + + const checkIfPositionIsValid = (positionAfterAction: TreeViewItemReorderPosition) => { + let isValid: boolean; + // If the new position is equal to the old one, we don't want to show any dropping UI. + if ( + positionAfterAction.parentId === oldPosition.parentId && + positionAfterAction.index === oldPosition.index + ) { + isValid = false; + } else if (canMoveItemToNewPosition) { + isValid = canMoveItemToNewPosition({ + itemId, + oldPosition, + newPosition: positionAfterAction, + }); + } else { + isValid = true; + } + + return isValid; + }; + + const positionsAfterAction: Record< + TreeViewItemsReorderingAction, + TreeViewItemReorderPosition | null + > = { + 'make-child': { parentId: targetItemMeta.id, index: 0 }, + 'reorder-above': { + parentId: targetItemMeta.parentId, + index: + targetItemMeta.parentId === draggedItemMeta.parentId && + targetItemIndex > draggedItemIndex + ? targetItemIndex - 1 + : targetItemIndex, + }, + 'reorder-below': targetItemMeta.expandable + ? null + : { + parentId: targetItemMeta.parentId, + index: + targetItemMeta.parentId === draggedItemMeta.parentId && + targetItemIndex > draggedItemIndex + ? targetItemIndex + : targetItemIndex + 1, + }, + 'move-to-parent': + targetItemMeta.parentId == null + ? null + : { + parentId: targetItemMeta.parentId, + index: instance.getItemOrderedChildrenIds(targetItemMeta.parentId).length, + }, + }; + + const validActions: TreeViewItemItemReorderingValidActions = {}; + Object.keys(positionsAfterAction).forEach((action) => { + const positionAfterAction = positionsAfterAction[action as TreeViewItemsReorderingAction]; + if (positionAfterAction != null && checkIfPositionIsValid(positionAfterAction)) { + validActions[action as TreeViewItemsReorderingAction] = positionAfterAction; + } + }); + + return validActions; + }, + [instance, state.itemsReordering, params.canMoveItemToNewPosition], + ); + + const startDraggingItem = React.useCallback( + (itemId: string) => { + setState((prevState) => ({ + ...prevState, + itemsReordering: { + targetItemId: itemId, + draggedItemId: itemId, + action: null, + newPosition: null, + }, + })); + }, + [setState], + ); + + const stopDraggingItem = React.useCallback( + (itemId: string) => { + if (state.itemsReordering == null || state.itemsReordering.draggedItemId !== itemId) { + return; + } + + setState((prevState) => ({ ...prevState, itemsReordering: null })); + if ( + state.itemsReordering.draggedItemId === state.itemsReordering.targetItemId || + state.itemsReordering.action == null || + state.itemsReordering.newPosition == null + ) { + return; + } + + const draggedItemMeta = instance.getItemMeta(state.itemsReordering.draggedItemId); + + const oldPosition: TreeViewItemReorderPosition = { + parentId: draggedItemMeta.parentId, + index: instance.getItemIndex(draggedItemMeta.id), + }; + + const newPosition = state.itemsReordering.newPosition; + + setState((prevState) => ({ + ...prevState, + items: moveItemInTree({ + itemToMoveId: itemId, + newPosition, + oldPosition, + prevState: prevState.items, + }), + })); + + const onItemPositionChange = params.onItemPositionChange; + onItemPositionChange?.({ + itemId, + newPosition, + oldPosition, + }); + }, + [setState, state.itemsReordering, instance, params.onItemPositionChange], + ); + + const setDragTargetItem = React.useCallback< + UseTreeViewItemsReorderingInstance['setDragTargetItem'] + >( + ({ itemId, validActions, targetHeight, cursorY, cursorX, contentElement }) => { + setState((prevState) => { + const prevSubState = prevState.itemsReordering; + if (prevSubState == null || isAncestor(instance, itemId, prevSubState.draggedItemId)) { + return prevState; + } + const action = chooseActionToApply({ + itemChildrenIndentation: params.itemChildrenIndentation, + validActions, + targetHeight, + targetDepth: prevState.items.itemMetaMap[itemId].depth!, + cursorY, + cursorX, + contentElement, + }); + + const newPosition = action == null ? null : validActions[action]!; + + if ( + prevSubState.targetItemId === itemId && + prevSubState.action === action && + prevSubState.newPosition?.parentId === newPosition?.parentId && + prevSubState.newPosition?.index === newPosition?.index + ) { + return prevState; + } + + return { + ...prevState, + itemsReordering: { + ...prevSubState, + targetItemId: itemId, + newPosition, + action, + }, + }; + }); + }, + [instance, setState, params.itemChildrenIndentation], + ); + + return { + instance: { + canItemBeDragged, + getDroppingTargetValidActions, + startDraggingItem, + stopDraggingItem, + setDragTargetItem, + }, + contextValue: { + itemsReordering: { + enabled: params.itemsReordering ?? false, + currentDrag: state.itemsReordering, + }, + }, + }; +}; + +useTreeViewItemsReordering.itemPlugin = useTreeViewItemsReorderingItemPlugin; + +useTreeViewItemsReordering.getDefaultizedParams = (params) => ({ + ...params, + itemsReordering: params.itemsReordering ?? false, +}); + +useTreeViewItemsReordering.getInitialState = () => ({ itemsReordering: null }); + +useTreeViewItemsReordering.params = { + itemsReordering: true, + isItemReorderable: true, + canMoveItemToNewPosition: true, + onItemPositionChange: true, +}; diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts new file mode 100644 index 0000000000000..8997d6f4c26c2 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts @@ -0,0 +1,158 @@ +import * as React from 'react'; +import { + DefaultizedProps, + TreeViewPluginSignature, + UseTreeViewItemsSignature, + MuiCancellableEventHandler, + UseTreeViewIdSignature, +} from '@mui/x-tree-view/internals'; +import { TreeViewItemId, TreeViewItemsReorderingAction } from '@mui/x-tree-view/models'; +import { TreeItem2DragAndDropOverlayProps } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'; + +export interface UseTreeViewItemsReorderingInstance { + /** + * Check if a given item can be dragged. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item can be dragged, `false` otherwise. + */ + canItemBeDragged: (itemId: TreeViewItemId) => boolean; + /** + * Get the valid reordering action if a given item is the target of the ongoing reordering. + * @param {TreeViewItemId} itemId The id of the item to get the action of. + * @returns {TreeViewItemItemReorderingValidActions} The valid actions for theF item. + */ + getDroppingTargetValidActions: (itemId: TreeViewItemId) => TreeViewItemItemReorderingValidActions; + /** + * Start a reordering for the given item. + * @param {TreeViewItemId} itemId The id of the item to start the reordering for. + */ + startDraggingItem: (itemId: TreeViewItemId) => void; + /** + * Stop the reordering of a given item. + * @param {TreeViewItemId} itemId The id of the item to stop the reordering for. + */ + stopDraggingItem: (itemId: TreeViewItemId) => void; + /** + * Set the new target item for the ongoing reordering. + * The action will be determined based on the position of the cursor inside the target and the valid actions for this target. + * @param {object} params The params describing the new target item. + * @param {TreeViewItemId} params.itemId The id of the new target item. + * @param {TreeViewItemItemReorderingValidActions} params.validActions The valid actions for the new target item. + * @param {number} params.targetHeight The height of the target item. + * @param {number} params.cursorY The Y coordinate of the mouse cursor. + * @param {number} params.cursorX The X coordinate of the mouse cursor. + * @param {HTMLDivElement} params.contentElement The DOM element rendered for the content slot. + */ + setDragTargetItem: (params: { + itemId: TreeViewItemId; + validActions: TreeViewItemItemReorderingValidActions; + targetHeight: number; + cursorY: number; + cursorX: number; + contentElement: HTMLDivElement; + }) => void; +} + +export interface TreeViewItemReorderPosition { + parentId: string | null; + index: number; +} + +export type TreeViewItemItemReorderingValidActions = { + [key in TreeViewItemsReorderingAction]?: TreeViewItemReorderPosition; +}; + +export interface UseTreeViewItemsReorderingParameters { + /** + * If `true`, the reordering of items is enabled. + * @default false + */ + itemsReordering?: boolean; + /** + * Used to determine if a given item can be reordered. + * @param {string} itemId The id of the item to check. + * @returns {boolean} `true` if the item can be reordered. + * @default () => true + */ + isItemReorderable?: (itemId: string) => boolean; + /** + * Used to determine if a given item can move to some new position. + * @param {object} params The params describing the item re-ordering. + * @param {string} params.itemId The id of the item to check. + * @param {TreeViewItemReorderPosition} params.oldPosition The old position of the item. + * @param {TreeViewItemReorderPosition} params.newPosition The new position of the item. + * @returns {boolean} `true` if the item can move to the new position. + */ + canMoveItemToNewPosition?: (params: { + itemId: string; + oldPosition: TreeViewItemReorderPosition; + newPosition: TreeViewItemReorderPosition; + }) => boolean; + /** + * Callback fired when a tree item is moved in the tree. + * @param {object} params The params describing the item re-ordering. + * @param {string} params.itemId The id of the item moved. + * @param {TreeViewItemReorderPosition} params.oldPosition The old position of the item. + * @param {TreeViewItemReorderPosition} params.newPosition The new position of the item. + */ + onItemPositionChange?: (params: { + itemId: string; + oldPosition: TreeViewItemReorderPosition; + newPosition: TreeViewItemReorderPosition; + }) => void; +} + +export type UseTreeViewItemsReorderingDefaultizedParameters = DefaultizedProps< + UseTreeViewItemsReorderingParameters, + 'itemsReordering' +>; + +export interface UseTreeViewItemsReorderingState { + itemsReordering: { + draggedItemId: string; + targetItemId: string; + newPosition: TreeViewItemReorderPosition | null; + action: TreeViewItemsReorderingAction | null; + } | null; +} + +interface UseTreeViewItemsReorderingContextValue { + itemsReordering: { + enabled: boolean; + currentDrag: UseTreeViewItemsReorderingState['itemsReordering']; + }; +} + +export type UseTreeViewItemsReorderingSignature = TreeViewPluginSignature<{ + params: UseTreeViewItemsReorderingParameters; + defaultizedParams: UseTreeViewItemsReorderingDefaultizedParameters; + instance: UseTreeViewItemsReorderingInstance; + state: UseTreeViewItemsReorderingState; + contextValue: UseTreeViewItemsReorderingContextValue; + dependantPlugins: [UseTreeViewItemsSignature, UseTreeViewIdSignature]; +}>; + +export interface UseTreeItem2RootSlotPropsFromItemsReordering { + draggable?: true; + onDragStart?: MuiCancellableEventHandler; + onDragOver?: MuiCancellableEventHandler; + onDragEnd?: MuiCancellableEventHandler; +} + +export interface UseTreeItem2ContentSlotPropsFromItemsReordering { + onDragEnter?: MuiCancellableEventHandler; + onDragOver?: MuiCancellableEventHandler; +} + +export interface UseTreeItem2DragAndDropOverlaySlotPropsFromItemsReordering + extends TreeItem2DragAndDropOverlayProps {} + +declare module '@mui/x-tree-view/useTreeItem2' { + interface UseTreeItem2RootSlotOwnProps extends UseTreeItem2RootSlotPropsFromItemsReordering {} + + interface UseTreeItem2ContentSlotOwnProps + extends UseTreeItem2ContentSlotPropsFromItemsReordering {} + + interface UseTreeItem2DragAndDropOverlaySlotOwnProps + extends UseTreeItem2DragAndDropOverlaySlotPropsFromItemsReordering {} +} diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts new file mode 100644 index 0000000000000..7330da2869a3d --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts @@ -0,0 +1,182 @@ +import { + TreeViewInstance, + UseTreeViewItemsSignature, + UseTreeViewItemsState, + buildSiblingIndexes, + TREE_VIEW_ROOT_PARENT_ID, +} from '@mui/x-tree-view/internals'; +import { TreeViewItemId, TreeViewItemsReorderingAction } from '@mui/x-tree-view/models'; +import { + TreeViewItemItemReorderingValidActions, + TreeViewItemReorderPosition, +} from './useTreeViewItemsReordering.types'; + +/** + * Checks if the item with the id itemIdB is an ancestor of the item with the id itemIdA. + */ +export const isAncestor = ( + instance: TreeViewInstance<[UseTreeViewItemsSignature]>, + itemIdA: string, + itemIdB: string, +): boolean => { + const itemMetaA = instance.getItemMeta(itemIdA); + if (itemMetaA.parentId === itemIdB) { + return true; + } + + if (itemMetaA.parentId == null) { + return false; + } + + return isAncestor(instance, itemMetaA.parentId, itemIdB); +}; + +const parseItemChildrenIndentation = ( + itemChildrenIndentation: string | number, + contentElement: HTMLElement, +) => { + if (typeof itemChildrenIndentation === 'number') { + return itemChildrenIndentation; + } + + const pixelExec = /^(\d.+)(px)$/.exec(itemChildrenIndentation); + if (pixelExec) { + return parseFloat(pixelExec[1]); + } + + const tempElement = document.createElement('div'); + tempElement.style.width = itemChildrenIndentation; + tempElement.style.position = 'absolute'; + contentElement.appendChild(tempElement); + const value = tempElement.offsetWidth; + contentElement.removeChild(tempElement); + + return value; +}; + +interface GetNewPositionParams { + itemChildrenIndentation: string | number; + validActions: TreeViewItemItemReorderingValidActions; + targetHeight: number; + targetDepth: number; + cursorY: number; + cursorX: number; + contentElement: HTMLDivElement; +} + +export const chooseActionToApply = ({ + itemChildrenIndentation, + validActions, + targetHeight, + targetDepth, + cursorX, + cursorY, + contentElement, +}: GetNewPositionParams) => { + let action: TreeViewItemsReorderingAction | null; + + const itemChildrenIndentationPx = parseItemChildrenIndentation( + itemChildrenIndentation, + contentElement, + ); + // If we can move the item to the parent of the target, then we allocate the left offset to this action + // Support moving to other ancestors + if (validActions['move-to-parent'] && cursorX < itemChildrenIndentationPx * targetDepth) { + action = 'move-to-parent'; + } + + // If we can move the item inside the target, then we have the following split: + // - the upper quarter of the target moves it above + // - the lower quarter of the target moves it below + // - the inner half makes it a child + else if (validActions['make-child']) { + if (validActions['reorder-above'] && cursorY < (1 / 4) * targetHeight) { + action = 'reorder-above'; + } else if (validActions['reorder-below'] && cursorY > (3 / 4) * targetHeight) { + action = 'reorder-below'; + } else { + action = 'make-child'; + } + } + // If we can't move the item inside the target, then we have the following split: + // - the upper half of the target moves it above + // - the lower half of the target moves it below + else { + // eslint-disable-next-line no-lonely-if + if (validActions['reorder-above'] && cursorY < (1 / 2) * targetHeight) { + action = 'reorder-above'; + } else if (validActions['reorder-below'] && cursorY >= (1 / 2) * targetHeight) { + action = 'reorder-below'; + } else { + action = null; + } + } + + return action; +}; + +export const moveItemInTree = ({ + itemToMoveId, + oldPosition, + newPosition, + prevState, +}: { + itemToMoveId: TreeViewItemId; + oldPosition: TreeViewItemReorderPosition; + newPosition: TreeViewItemReorderPosition; + prevState: UseTreeViewItemsState['items']; +}): UseTreeViewItemsState['items'] => { + const itemToMoveMeta = prevState.itemMetaMap[itemToMoveId]; + + const oldParentId = oldPosition.parentId ?? TREE_VIEW_ROOT_PARENT_ID; + const newParentId = newPosition.parentId ?? TREE_VIEW_ROOT_PARENT_ID; + + // 1. Update the `itemOrderedChildrenIds`. + const itemOrderedChildrenIds = { ...prevState.itemOrderedChildrenIds }; + if (oldParentId === newParentId) { + const updatedChildren = [...itemOrderedChildrenIds[oldParentId]]; + updatedChildren.splice(oldPosition.index, 1); + updatedChildren.splice(newPosition.index, 0, itemToMoveId); + itemOrderedChildrenIds[itemToMoveMeta.parentId ?? TREE_VIEW_ROOT_PARENT_ID] = updatedChildren; + } else { + const updatedOldParentChildren = [...itemOrderedChildrenIds[oldParentId]]; + updatedOldParentChildren.splice(oldPosition.index, 1); + itemOrderedChildrenIds[oldParentId] = updatedOldParentChildren; + + const updatedNewParentChildren = [...(itemOrderedChildrenIds[newParentId] ?? [])]; + updatedNewParentChildren.splice(newPosition.index, 0, itemToMoveId); + itemOrderedChildrenIds[newParentId] = updatedNewParentChildren; + } + + // 2. Update the `itemChildrenIndexes` + const itemChildrenIndexes = { ...prevState.itemChildrenIndexes }; + itemChildrenIndexes[oldParentId] = buildSiblingIndexes(itemOrderedChildrenIds[oldParentId]); + if (newParentId !== oldParentId) { + itemChildrenIndexes[newParentId] = buildSiblingIndexes(itemOrderedChildrenIds[newParentId]); + } + + // 3. Update the `itemMetaMap` + const itemMetaMap = { ...prevState.itemMetaMap }; + // The depth is always defined because drag&drop is only usable with Rich Tree View components. + const itemToMoveDepth = newPosition.parentId == null ? 0 : itemMetaMap[newParentId].depth! + 1; + itemMetaMap[itemToMoveId] = { + ...itemToMoveMeta, + parentId: newPosition.parentId, + depth: itemToMoveDepth, + }; + + const updateItemDepth = (itemId: string, depth: number) => { + itemMetaMap[itemId] = { ...itemMetaMap[itemId], depth }; + itemOrderedChildrenIds[itemId]?.forEach((childId) => updateItemDepth(childId, depth + 1)); + }; + itemOrderedChildrenIds[itemToMoveId]?.forEach((childId) => + updateItemDepth(childId, itemToMoveDepth + 1), + ); + + return { + ...prevState, + itemOrderedChildrenIds, + itemChildrenIndexes, + itemMetaMap, + }; +}; diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx index 15cea0fe567f1..ac66ec2b9b7bd 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx @@ -154,6 +154,8 @@ RichTreeView.propTypes = { current: PropTypes.shape({ focusItem: PropTypes.func.isRequired, getItem: PropTypes.func.isRequired, + getItemOrderedChildrenIds: PropTypes.func.isRequired, + getItemTree: PropTypes.func.isRequired, setItemExpansion: PropTypes.func.isRequired, }), }), diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.plugins.ts b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.plugins.ts index 7d2cabef636fe..4147f5146cac8 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.plugins.ts +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.plugins.ts @@ -1,14 +1,40 @@ import { - DEFAULT_TREE_VIEW_PLUGINS, - DefaultTreeViewPluginParameters, DefaultTreeViewPluginSlotProps, DefaultTreeViewPluginSlots, } from '../internals/plugins/defaultPlugins'; import { useTreeViewJSXItems } from '../internals/plugins/useTreeViewJSXItems'; import { ConvertPluginsIntoSignatures } from '../internals/models'; +import { useTreeViewId, UseTreeViewIdParameters } from '../internals/plugins/useTreeViewId'; +import { + useTreeViewItems, + UseTreeViewItemsParameters, +} from '../internals/plugins/useTreeViewItems'; +import { + useTreeViewExpansion, + UseTreeViewExpansionParameters, +} from '../internals/plugins/useTreeViewExpansion'; +import { + useTreeViewSelection, + UseTreeViewSelectionParameters, +} from '../internals/plugins/useTreeViewSelection'; +import { + useTreeViewFocus, + UseTreeViewFocusParameters, +} from '../internals/plugins/useTreeViewFocus'; +import { useTreeViewKeyboardNavigation } from '../internals/plugins/useTreeViewKeyboardNavigation'; +import { + useTreeViewIcons, + UseTreeViewIconsParameters, +} from '../internals/plugins/useTreeViewIcons'; export const SIMPLE_TREE_VIEW_PLUGINS = [ - ...DEFAULT_TREE_VIEW_PLUGINS, + useTreeViewId, + useTreeViewItems, + useTreeViewExpansion, + useTreeViewSelection, + useTreeViewFocus, + useTreeViewKeyboardNavigation, + useTreeViewIcons, useTreeViewJSXItems, ] as const; @@ -20,7 +46,12 @@ export type SimpleTreeViewPluginSlotProps = DefaultTreeViewPluginSlotProps; // We can't infer this type from the plugin, otherwise we would lose the generics. export interface SimpleTreeViewPluginParameters - extends Omit< - DefaultTreeViewPluginParameters, - 'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemId' - > {} + extends UseTreeViewIdParameters, + Omit< + UseTreeViewItemsParameters, + 'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemId' + >, + UseTreeViewExpansionParameters, + UseTreeViewFocusParameters, + UseTreeViewSelectionParameters, + UseTreeViewIconsParameters {} diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx index 297792cd511cf..5f369ad0bbfbb 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx @@ -117,6 +117,8 @@ SimpleTreeView.propTypes = { current: PropTypes.shape({ focusItem: PropTypes.func.isRequired, getItem: PropTypes.func.isRequired, + getItemOrderedChildrenIds: PropTypes.func.isRequired, + getItemTree: PropTypes.func.isRequired, setItemExpansion: PropTypes.func.isRequired, }), }), diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx index 6256ac0bb6a75..1cc636570fb3b 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx @@ -23,9 +23,15 @@ const TEST_TREE_VIEW_CONTEXT_VALUE: TreeViewContextValue publicAPI: { focusItem: () => {}, getItem: () => ({}), + getItemOrderedChildrenIds: () => [], setItemExpansion: () => {}, + getItemTree: () => [], }, - runItemPlugins: () => ({ rootRef: null, contentRef: null }), + runItemPlugins: () => ({ + rootRef: null, + contentRef: null, + propsEnhancers: {}, + }), wrapItem: ({ children }) => children, wrapRoot: ({ children }) => children, disabledItemsFocusable: false, diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx index a330de1af8d98..d1ef4e95b46ba 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import Collapse from '@mui/material/Collapse'; -import { resolveComponentProps, useSlotProps } from '@mui/base/utils'; +import { extractEventHandlers, resolveComponentProps, useSlotProps } from '@mui/base/utils'; import useForkRef from '@mui/utils/useForkRef'; import { shouldForwardProp } from '@mui/system/createStyled'; import { alpha, styled, useThemeProps } from '@mui/material/styles'; @@ -13,7 +13,7 @@ import { unstable_composeClasses as composeClasses } from '@mui/base'; import { TreeItemContent } from './TreeItemContent'; import { treeItemClasses, getTreeItemUtilityClass } from './treeItemClasses'; import { TreeItemOwnerState, TreeItemProps } from './TreeItem.types'; -import { useTreeViewContext } from '../internals/TreeViewProvider/useTreeViewContext'; +import { useTreeViewContext } from '../internals/TreeViewProvider'; import { DefaultTreeViewPlugins } from '../internals/plugins'; import { TreeViewCollapseIcon, TreeViewExpandIcon } from '../icons'; import { TreeItem2Provider } from '../TreeItem2Provider'; @@ -69,6 +69,7 @@ const StyledTreeItemContent = styled(TreeItemContent, { borderRadius: theme.shape.borderRadius, width: '100%', boxSizing: 'border-box', // prevent width + padding to overflow + position: 'relative', display: 'flex', alignItems: 'center', gap: theme.spacing(1), @@ -206,9 +207,11 @@ export const TreeItem = React.forwardRef(function TreeItem( ...other } = props; - const { contentRef, rootRef } = runItemPlugins(props); - const handleRootRef = useForkRef(inRef, rootRef); - const handleContentRef = useForkRef(ContentProps?.ref, contentRef); + const { contentRef, rootRef, propsEnhancers } = runItemPlugins(props); + const rootRefObject = React.useRef(null); + const contentRefObject = React.useRef(null); + const handleRootRef = useForkRef(inRef, rootRef, rootRefObject); + const handleContentRef = useForkRef(ContentProps?.ref, contentRef, contentRefObject); const slots = { expandIcon: inSlots?.expandIcon ?? contextIcons.slots.expandIcon ?? TreeViewExpandIcon, @@ -335,6 +338,25 @@ export const TreeItem = React.forwardRef(function TreeItem( const idAttribute = instance.getTreeItemIdAttribute(itemId, id); const tabIndex = instance.canItemBeTabbed(itemId) ? 0 : -1; + const enhancedRootProps = + propsEnhancers.root?.({ + rootRefObject, + contentRefObject, + externalEventHandlers: extractEventHandlers(other), + }) ?? {}; + const enhancedContentProps = + propsEnhancers.content?.({ + rootRefObject, + contentRefObject, + externalEventHandlers: extractEventHandlers(ContentProps), + }) ?? {}; + const enhancedDragAndDropOverlayProps = + propsEnhancers.dragAndDropOverlay?.({ + rootRefObject, + contentRefObject, + externalEventHandlers: {}, + }) ?? {}; + return ( {children && ( diff --git a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx index 11c31e401ceeb..4dff60695d312 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx @@ -3,6 +3,10 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import Checkbox from '@mui/material/Checkbox'; import { useTreeItemState } from './useTreeItemState'; +import { + TreeItem2DragAndDropOverlay, + TreeItem2DragAndDropOverlayProps, +} from '../TreeItem2DragAndDropOverlay'; export interface TreeItemContentProps extends React.HTMLAttributes { className?: string; @@ -47,6 +51,7 @@ export interface TreeItemContentProps extends React.HTMLAttributes * The icon to display next to the tree item's label. Either a parent or end icon. */ displayIcon?: React.ReactNode; + dragAndDropOverlayProps?: TreeItem2DragAndDropOverlayProps; } export type TreeItemContentClassKey = keyof NonNullable; @@ -68,6 +73,7 @@ const TreeItemContent = React.forwardRef(function TreeItemContent( itemId, onClick, onMouseDown, + dragAndDropOverlayProps, ...other } = props; @@ -138,6 +144,7 @@ const TreeItemContent = React.forwardRef(function TreeItemContent( )}
{label}
+ {dragAndDropOverlayProps && } ); }); @@ -156,6 +163,10 @@ TreeItemContent.propTypes = { * The icon to display next to the tree item's label. Either a parent or end icon. */ displayIcon: PropTypes.node, + dragAndDropOverlayProps: PropTypes.shape({ + action: PropTypes.oneOf(['make-child', 'move-to-parent', 'reorder-above', 'reorder-below']), + style: PropTypes.object, + }), /** * The icon to display next to the tree item's label. Either an expansion or collapse icon. */ diff --git a/packages/x-tree-view/src/TreeItem/treeItemClasses.ts b/packages/x-tree-view/src/TreeItem/treeItemClasses.ts index ceff7c2a2ca60..a8ac533418c60 100644 --- a/packages/x-tree-view/src/TreeItem/treeItemClasses.ts +++ b/packages/x-tree-view/src/TreeItem/treeItemClasses.ts @@ -22,6 +22,8 @@ export interface TreeItemClasses { label: string; /** Styles applied to the checkbox element. */ checkbox: string; + /** Styles applied to the drag and drop overlay. */ + dragAndDropOverlay: string; } export type TreeItemClassKey = keyof TreeItemClasses; @@ -41,4 +43,5 @@ export const treeItemClasses: TreeItemClasses = generateUtilityClasses('MuiTreeI 'iconContainer', 'label', 'checkbox', + 'dragAndDropOverlay', ]); diff --git a/packages/x-tree-view/src/TreeItem/useTreeItemState.ts b/packages/x-tree-view/src/TreeItem/useTreeItemState.ts index f429ac981d20b..fe0585e27c70e 100644 --- a/packages/x-tree-view/src/TreeItem/useTreeItemState.ts +++ b/packages/x-tree-view/src/TreeItem/useTreeItemState.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useTreeViewContext } from '../internals/TreeViewProvider/useTreeViewContext'; +import { useTreeViewContext } from '../internals/TreeViewProvider'; import { DefaultTreeViewPlugins } from '../internals/plugins'; export function useTreeItemState(itemId: string) { diff --git a/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx b/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx index fd905cda2caa3..212a99353bce2 100644 --- a/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx +++ b/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx @@ -16,6 +16,7 @@ import { } from '../useTreeItem2'; import { getTreeItemUtilityClass } from '../TreeItem'; import { TreeItem2Icon } from '../TreeItem2Icon'; +import { TreeItem2DragAndDropOverlay } from '../TreeItem2DragAndDropOverlay'; import { TreeItem2Provider } from '../TreeItem2Provider'; export const TreeItem2Root = styled('li', { @@ -40,6 +41,7 @@ export const TreeItem2Content = styled('div', { borderRadius: theme.shape.borderRadius, width: '100%', boxSizing: 'border-box', // prevent width + padding to overflow + position: 'relative', display: 'flex', alignItems: 'center', gap: theme.spacing(1), @@ -184,6 +186,7 @@ const useUtilityClasses = (ownerState: TreeItem2OwnerState) => { checkbox: ['checkbox'], label: ['label'], groupTransition: ['groupTransition'], + dragAndDropOverlay: ['dragAndDropOverlay'], }; return composeClasses(slots, getTreeItemUtilityClass, classes); @@ -218,6 +221,7 @@ export const TreeItem2 = React.forwardRef(function TreeItem2( getCheckboxProps, getLabelProps, getGroupTransitionProps, + getDragAndDropOverlayProps, status, } = useTreeItem2({ id, @@ -296,6 +300,16 @@ export const TreeItem2 = React.forwardRef(function TreeItem2( className: classes.groupTransition, }); + const DragAndDropOverlay: React.ElementType | undefined = + slots.dragAndDropOverlay ?? TreeItem2DragAndDropOverlay; + const dragAndDropOverlayProps = useSlotProps({ + elementType: DragAndDropOverlay, + getSlotProps: getDragAndDropOverlayProps, + externalSlotProps: slotProps.dragAndDropOverlay, + ownerState: {}, + className: classes.dragAndDropOverlay, + }); + return ( @@ -305,6 +319,7 @@ export const TreeItem2 = React.forwardRef(function TreeItem2( diff --git a/packages/x-tree-view/src/TreeItem2/TreeItem2.types.ts b/packages/x-tree-view/src/TreeItem2/TreeItem2.types.ts index 0bce978f46b92..fb3003c830b7c 100644 --- a/packages/x-tree-view/src/TreeItem2/TreeItem2.types.ts +++ b/packages/x-tree-view/src/TreeItem2/TreeItem2.types.ts @@ -36,6 +36,12 @@ export interface TreeItem2Slots extends TreeItem2IconSlots { * @default TreeItem2Label */ label?: React.ElementType; + /** + * The component that renders the overlay when an item reordering is ongoing. + * Warning: This slot is only useful when using the `RichTreeViewPro` component. + * @default TreeItem2DragAndDropOverlay + */ + dragAndDropOverlay?: React.ElementType; } export interface TreeItem2SlotProps extends TreeItem2IconSlotProps { @@ -45,6 +51,7 @@ export interface TreeItem2SlotProps extends TreeItem2IconSlotProps { iconContainer?: SlotComponentProps<'div', {}, {}>; checkbox?: SlotComponentProps<'button', {}, {}>; label?: SlotComponentProps<'div', {}, {}>; + dragAndDropOverlay?: SlotComponentProps<'div', {}, {}>; } export interface TreeItem2Props diff --git a/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.tsx b/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.tsx new file mode 100644 index 0000000000000..772230496a280 --- /dev/null +++ b/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { alpha, styled } from '@mui/material/styles'; +import { shouldForwardProp } from '@mui/system'; +import { TreeItem2DragAndDropOverlayProps } from './TreeItem2DragAndDropOverlay.types'; +import { TreeViewItemsReorderingAction } from '../models'; + +const TreeItem2DragAndDropOverlayRoot = styled('div', { + name: 'MuiTreeItem2DragAndDropOverlay', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, + shouldForwardProp: (prop) => shouldForwardProp(prop) && prop !== 'action', +})<{ action?: TreeViewItemsReorderingAction | null }>(({ theme }) => ({ + position: 'absolute', + left: 0, + display: 'flex', + top: 0, + bottom: 0, + right: 0, + pointerEvents: 'none', + variants: [ + { + props: { action: 'make-child' }, + style: { + marginLeft: 'calc(var(--TreeView-indentMultiplier) * var(--TreeView-itemDepth))', + borderRadius: theme.shape.borderRadius, + backgroundColor: alpha((theme.vars || theme).palette.primary.dark, 0.15), + }, + }, + { + props: { action: 'reorder-above' }, + style: { + marginLeft: 'calc(var(--TreeView-indentMultiplier) * var(--TreeView-itemDepth))', + borderTop: `1px solid ${alpha((theme.vars || theme).palette.grey[900], 0.6)}`, + ...(theme.palette.mode === 'dark' && { + borderTop: `1px solid ${alpha((theme.vars || theme).palette.grey[100], 0.6)}`, + }), + }, + }, + { + props: { action: 'reorder-below' }, + style: { + marginLeft: 'calc(var(--TreeView-indentMultiplier) * var(--TreeView-itemDepth))', + borderBottom: `1px solid ${alpha((theme.vars || theme).palette.grey[900], 0.6)}`, + ...(theme.palette.mode === 'dark' && { + borderBottom: `1px solid ${alpha((theme.vars || theme).palette.grey[100], 0.6)}`, + }), + }, + }, + { + props: { action: 'move-to-parent' }, + style: { + marginLeft: + 'calc(var(--TreeView-indentMultiplier) * calc(var(--TreeView-itemDepth) - 1))' as any, + borderBottom: `1px solid ${alpha((theme.vars || theme).palette.grey[900], 0.6)}`, + ...(theme.palette.mode === 'dark' && { + borderBottom: `1px solid ${alpha((theme.vars || theme).palette.grey[900], 0.6)}`, + }), + }, + }, + ], +})); + +function TreeItem2DragAndDropOverlay(props: TreeItem2DragAndDropOverlayProps) { + if (props.action == null) { + return null; + } + + return ; +} + +export { TreeItem2DragAndDropOverlay }; diff --git a/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.types.ts b/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.types.ts new file mode 100644 index 0000000000000..a2b3b4d22468c --- /dev/null +++ b/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.types.ts @@ -0,0 +1,7 @@ +import * as React from 'react'; +import { TreeViewItemsReorderingAction } from '../models'; + +export interface TreeItem2DragAndDropOverlayProps { + action?: TreeViewItemsReorderingAction; + style?: React.CSSProperties; +} diff --git a/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/index.ts b/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/index.ts new file mode 100644 index 0000000000000..548f1827a8bc0 --- /dev/null +++ b/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/index.ts @@ -0,0 +1,2 @@ +export { TreeItem2DragAndDropOverlay } from './TreeItem2DragAndDropOverlay'; +export type { TreeItem2DragAndDropOverlayProps } from './TreeItem2DragAndDropOverlay.types'; diff --git a/packages/x-tree-view/src/TreeItem2Icon/TreeItem2Icon.tsx b/packages/x-tree-view/src/TreeItem2Icon/TreeItem2Icon.tsx index 6247cbb4dcabc..9229b11daeec0 100644 --- a/packages/x-tree-view/src/TreeItem2Icon/TreeItem2Icon.tsx +++ b/packages/x-tree-view/src/TreeItem2Icon/TreeItem2Icon.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { resolveComponentProps, useSlotProps } from '@mui/base/utils'; import { TreeItem2IconProps } from './TreeItem2Icon.types'; -import { useTreeViewContext } from '../internals/TreeViewProvider/useTreeViewContext'; +import { useTreeViewContext } from '../internals/TreeViewProvider'; import { UseTreeViewIconsSignature } from '../internals/plugins/useTreeViewIcons'; import { TreeViewCollapseIcon, TreeViewExpandIcon } from '../icons'; diff --git a/packages/x-tree-view/src/TreeItem2Provider/TreeItem2Provider.tsx b/packages/x-tree-view/src/TreeItem2Provider/TreeItem2Provider.tsx index 2b5387522cef1..ad586535f16f4 100644 --- a/packages/x-tree-view/src/TreeItem2Provider/TreeItem2Provider.tsx +++ b/packages/x-tree-view/src/TreeItem2Provider/TreeItem2Provider.tsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import { TreeItem2ProviderProps } from './TreeItem2Provider.types'; -import { useTreeViewContext } from '../internals/TreeViewProvider/useTreeViewContext'; +import { useTreeViewContext } from '../internals/TreeViewProvider'; function TreeItem2Provider(props: TreeItem2ProviderProps) { const { children, itemId } = props; diff --git a/packages/x-tree-view/src/TreeView/TreeView.tsx b/packages/x-tree-view/src/TreeView/TreeView.tsx index b6944f2df05aa..50cfecd1bce01 100644 --- a/packages/x-tree-view/src/TreeView/TreeView.tsx +++ b/packages/x-tree-view/src/TreeView/TreeView.tsx @@ -93,6 +93,8 @@ TreeView.propTypes = { current: PropTypes.shape({ focusItem: PropTypes.func.isRequired, getItem: PropTypes.func.isRequired, + getItemOrderedChildrenIds: PropTypes.func.isRequired, + getItemTree: PropTypes.func.isRequired, setItemExpansion: PropTypes.func.isRequired, }), }), diff --git a/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx b/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx index 911322d477666..3b8ef827659cb 100644 --- a/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx +++ b/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useTreeViewContext } from '../../internals/TreeViewProvider/useTreeViewContext'; +import { useTreeViewContext } from '../../internals/TreeViewProvider'; import { DefaultTreeViewPlugins } from '../../internals/plugins'; import type { UseTreeItem2Status } from '../../useTreeItem2'; diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/index.ts b/packages/x-tree-view/src/internals/TreeViewProvider/index.ts index 28aff7c61d46f..f8e1255619b3d 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/index.ts +++ b/packages/x-tree-view/src/internals/TreeViewProvider/index.ts @@ -1,2 +1,3 @@ export { TreeViewProvider } from './TreeViewProvider'; +export { useTreeViewContext } from './useTreeViewContext'; export type { TreeViewProviderProps, TreeViewContextValue } from './TreeViewProvider.types'; diff --git a/packages/x-tree-view/src/internals/index.ts b/packages/x-tree-view/src/internals/index.ts index 9500d00e37f34..06ea79caa9f27 100644 --- a/packages/x-tree-view/src/internals/index.ts +++ b/packages/x-tree-view/src/internals/index.ts @@ -1,5 +1,5 @@ export { useTreeView } from './useTreeView'; -export { TreeViewProvider } from './TreeViewProvider'; +export { TreeViewProvider, useTreeViewContext } from './TreeViewProvider'; export { unstable_resetCleanupTracking } from './hooks/useInstanceEventHandler'; @@ -10,6 +10,12 @@ export type { MergePluginsProperty, TreeViewPublicAPI, TreeViewExperimentalFeatures, + TreeViewItemMeta, + TreeViewInstance, + DefaultizedProps, + TreeViewItemPlugin, + MuiCancellableEvent, + MuiCancellableEventHandler, } from './models'; // Plugins @@ -44,10 +50,15 @@ export type { UseTreeViewIconsSignature, UseTreeViewIconsParameters, } from './plugins/useTreeViewIcons'; -export { useTreeViewItems } from './plugins/useTreeViewItems'; +export { + useTreeViewItems, + buildSiblingIndexes, + TREE_VIEW_ROOT_PARENT_ID, +} from './plugins/useTreeViewItems'; export type { UseTreeViewItemsSignature, UseTreeViewItemsParameters, + UseTreeViewItemsState, } from './plugins/useTreeViewItems'; export { useTreeViewJSXItems } from './plugins/useTreeViewJSXItems'; export type { @@ -55,5 +66,6 @@ export type { UseTreeViewJSXItemsParameters, } from './plugins/useTreeViewJSXItems'; +export { isEventTargetInDescendants } from './utils/tree'; export { buildWarning } from './utils/warning'; export { extractPluginParamsFromProps } from './utils/extractPluginParamsFromProps'; diff --git a/packages/x-tree-view/src/internals/models/index.ts b/packages/x-tree-view/src/internals/models/index.ts index 37098f979a240..7478186e4ea91 100644 --- a/packages/x-tree-view/src/internals/models/index.ts +++ b/packages/x-tree-view/src/internals/models/index.ts @@ -1,3 +1,5 @@ export * from './helpers'; export * from './plugin'; +export * from './itemPlugin'; export * from './treeView'; +export * from './MuiCancellableEvent'; diff --git a/packages/x-tree-view/src/internals/models/itemPlugin.ts b/packages/x-tree-view/src/internals/models/itemPlugin.ts new file mode 100644 index 0000000000000..ae25a111da1b9 --- /dev/null +++ b/packages/x-tree-view/src/internals/models/itemPlugin.ts @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { EventHandlers } from '@mui/base/utils'; +import type { + UseTreeItem2ContentSlotOwnProps, + UseTreeItem2DragAndDropOverlaySlotOwnProps, + UseTreeItem2RootSlotOwnProps, +} from '../../useTreeItem2'; + +export interface TreeViewItemPluginSlotPropsEnhancerParams { + rootRefObject: React.MutableRefObject; + contentRefObject: React.MutableRefObject; + externalEventHandlers: EventHandlers; +} + +type TreeViewItemPluginSlotPropsEnhancer = ( + params: TreeViewItemPluginSlotPropsEnhancerParams, +) => Partial; + +export interface TreeViewItemPluginSlotPropsEnhancers { + root?: TreeViewItemPluginSlotPropsEnhancer; + content?: TreeViewItemPluginSlotPropsEnhancer; + dragAndDropOverlay?: TreeViewItemPluginSlotPropsEnhancer; +} + +export interface TreeViewItemPluginResponse { + /** + * Root of the `content` slot enriched by the plugin. + */ + contentRef?: React.RefCallback | null; + /** + * Ref of the `root` slot enriched by the plugin + */ + rootRef?: React.RefCallback | null; + /** + * Callback to enhance the slot props of the Tree Item. + * + * Not all slots are enabled by default, + * if a new plugin needs to pass to an unconfigured slot, + * it just needs to be added to `TreeViewItemPluginSlotPropsEnhancers` + */ + propsEnhancers?: TreeViewItemPluginSlotPropsEnhancers; +} + +export interface TreeViewItemPluginOptions + extends Omit { + props: TProps; +} + +export type TreeViewItemPlugin = ( + options: TreeViewItemPluginOptions, +) => void | TreeViewItemPluginResponse; diff --git a/packages/x-tree-view/src/internals/models/plugin.ts b/packages/x-tree-view/src/internals/models/plugin.ts index 0d85c8b61754d..139c278965282 100644 --- a/packages/x-tree-view/src/internals/models/plugin.ts +++ b/packages/x-tree-view/src/internals/models/plugin.ts @@ -4,6 +4,7 @@ import { TreeViewExperimentalFeatures, TreeViewInstance, TreeViewModel } from '. import type { MergePluginsProperty, OptionalIfEmpty } from './helpers'; import { TreeViewEventLookupElement } from './events'; import type { TreeViewCorePluginsSignature } from '../corePlugins'; +import { TreeViewItemPlugin } from './itemPlugin'; import { TreeViewItemId } from '../../models'; export interface TreeViewPluginOptions { @@ -123,25 +124,6 @@ export type TreeViewUsedModels = export type TreeViewUsedEvents = TSignature['events'] & MergePluginsProperty, 'events'>; -export interface TreeViewItemPluginOptions extends TreeViewItemPluginResponse { - props: TProps; -} - -export interface TreeViewItemPluginResponse { - /** - * Root of the `content` slot enriched by the plugin. - */ - contentRef?: React.RefCallback | null; - /** - * Ref of the `root` slot enriched by the plugin - */ - rootRef?: React.RefCallback | null; -} - -export type TreeViewItemPlugin = ( - options: TreeViewItemPluginOptions, -) => void | TreeViewItemPluginResponse; - export type TreeItemWrapper = (params: { itemId: TreeViewItemId; children: React.ReactNode; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/index.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/index.ts index 63c1a5d694cea..b98e025875365 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/index.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/index.ts @@ -3,4 +3,6 @@ export type { UseTreeViewItemsSignature, UseTreeViewItemsParameters, UseTreeViewItemsDefaultizedParameters, + UseTreeViewItemsState, } from './useTreeViewItems.types'; +export { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx index c2a77e35bd972..c078969f21148 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx @@ -22,7 +22,7 @@ const updateItemsState = ({ isItemDisabled, getItemLabel, getItemId, -}: UpdateNodesStateParameters): UseTreeViewItemsState['items'] => { +}: UpdateNodesStateParameters): State => { const itemMetaMap: State['itemMetaMap'] = {}; const itemMap: State['itemMap'] = {}; const itemOrderedChildrenIds: State['itemOrderedChildrenIds'] = { @@ -76,7 +76,7 @@ const updateItemsState = ({ }; itemMap[id] = item; - itemOrderedChildrenIds[id] = []; + const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID; if (!itemOrderedChildrenIds[parentIdWithDefault]) { itemOrderedChildrenIds[parentIdWithDefault] = []; @@ -118,6 +118,20 @@ export const useTreeViewItems: TreeViewPlugin = ({ [state.items.itemMap], ); + const getItemTree = React.useCallback(() => { + const getItemFromItemId = (id: TreeViewItemId): TreeViewBaseItem => { + const { children: oldChildren, ...item } = state.items.itemMap[id]; + const newChildren = state.items.itemOrderedChildrenIds[id]; + if (newChildren) { + item.children = newChildren.map(getItemFromItemId); + } + + return item; + }; + + return state.items.itemOrderedChildrenIds[TREE_VIEW_ROOT_PARENT_ID].map(getItemFromItemId); + }, [state.items.itemMap, state.items.itemOrderedChildrenIds]); + const isItemDisabled = React.useCallback( (itemId: string | null): itemId is string => { if (itemId == null) { @@ -214,7 +228,7 @@ export const useTreeViewItems: TreeViewPlugin = ({ label: item.label!, itemId: item.id, id: item.idAttribute, - children: state.items.itemOrderedChildrenIds[id].map(getPropsFromItemId), + children: state.items.itemOrderedChildrenIds[id]?.map(getPropsFromItemId), }; }; @@ -232,10 +246,13 @@ export const useTreeViewItems: TreeViewPlugin = ({ }), publicAPI: { getItem, + getItemTree, + getItemOrderedChildrenIds, }, instance: { getItemMeta, getItem, + getItemTree, getItemsToRender, getItemIndex, getItemOrderedChildrenIds, diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts index dc234010c8550..993389e7c4de9 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts @@ -1,5 +1,5 @@ import { TreeViewItemMeta, DefaultizedProps, TreeViewPluginSignature } from '../../models'; -import { TreeViewItemId } from '../../../models'; +import { TreeViewBaseItem, TreeViewItemId } from '../../../models'; interface TreeViewItemProps { label: string; @@ -16,6 +16,18 @@ export interface UseTreeViewItemsPublicAPI { * @returns {R} The item with the given id. */ getItem: (itemId: TreeViewItemId) => R; + /** + * Get the children of the item with the given id. + * To get the root items, pass `null` as the `itemId`. + * @param {string | null} itemId The id of the item to return the children of. + * @returns {string[]} The children of the item with the given id. + */ + getItemOrderedChildrenIds: (itemId: string | null) => string[]; + /** + * Get all the items in the same provided as provided by `props.items`. + * @returns {TreeViewItemProps[]} The items in the tree. + */ + getItemTree: () => TreeViewBaseItem[]; } export interface UseTreeViewItemsInstance extends UseTreeViewItemsPublicAPI { @@ -73,7 +85,7 @@ export interface UseTreeViewItemsInstance extends UseTreeViewItems areItemUpdatesPrevented: () => boolean; } -export interface UseTreeViewItemsParameters { +export interface UseTreeViewItemsParameters { /** * If `true`, will allow focus on disabled items. * @default false @@ -113,7 +125,7 @@ export interface UseTreeViewItemsParameters { itemChildrenIndentation?: string | number; } -export type UseTreeViewItemsDefaultizedParameters = DefaultizedProps< +export type UseTreeViewItemsDefaultizedParameters = DefaultizedProps< UseTreeViewItemsParameters, 'disabledItemsFocusable' | 'itemChildrenIndentation' >; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx index c65cbe41913f4..f38822f47ea68 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx @@ -5,7 +5,7 @@ import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import { TreeViewItemPlugin, TreeViewItemMeta, TreeViewPlugin } from '../../models'; import { UseTreeViewJSXItemsSignature } from './useTreeViewJSXItems.types'; import { publishTreeViewEvent } from '../../utils/publishTreeViewEvent'; -import { useTreeViewContext } from '../../TreeViewProvider/useTreeViewContext'; +import { useTreeViewContext } from '../../TreeViewProvider'; import { TreeViewChildrenItemContext, TreeViewChildrenItemProvider, diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts index b16b3d4577873..a4f39966a5f62 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts @@ -1,18 +1,18 @@ import * as React from 'react'; import { useTheme } from '@mui/material/styles'; import useEventCallback from '@mui/utils/useEventCallback'; -import { TreeViewItemMeta, TreeViewPlugin } from '../../models'; +import { TreeViewItemMeta, TreeViewPlugin, MuiCancellableEvent } from '../../models'; import { getFirstNavigableItem, getLastNavigableItem, getNextNavigableItem, getPreviousNavigableItem, + isEventTargetInDescendants, } from '../../utils/tree'; import { TreeViewFirstCharMap, UseTreeViewKeyboardNavigationSignature, } from './useTreeViewKeyboardNavigation.types'; -import { MuiCancellableEvent } from '../../models/MuiCancellableEvent'; function isPrintableCharacter(string: string) { return !!string && string.length === 1 && !!string.match(/\S/); @@ -91,10 +91,7 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< return; } - if ( - event.altKey || - event.currentTarget !== (event.target as HTMLElement).closest('*[role="treeitem"]') - ) { + if (event.altKey || isEventTargetInDescendants(event, event.currentTarget as HTMLElement)) { return; } diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts index 9aceca62b73fd..be628eb38b956 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts @@ -1,10 +1,9 @@ import * as React from 'react'; -import { TreeViewPluginSignature } from '../../models'; +import { TreeViewPluginSignature, MuiCancellableEvent } from '../../models'; import { UseTreeViewItemsSignature } from '../useTreeViewItems'; import { UseTreeViewSelectionSignature } from '../useTreeViewSelection'; import { UseTreeViewFocusSignature } from '../useTreeViewFocus'; import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion'; -import { MuiCancellableEvent } from '../../models/MuiCancellableEvent'; import { TreeViewItemId } from '../../../models'; export interface UseTreeViewKeyboardNavigationInstance { diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts index 464da635e65e2..5be49ee56b8ae 100644 --- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts +++ b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts @@ -10,6 +10,8 @@ import { TreeItemWrapper, TreeRootWrapper, TreeViewPublicAPI, + TreeViewItemPluginSlotPropsEnhancers, + TreeViewItemPluginSlotPropsEnhancerParams, } from '../models'; import { UseTreeViewDefaultizedParameters, @@ -122,6 +124,9 @@ export const useTreeView = { let finalRootRef: React.RefCallback | null = null; let finalContentRef: React.RefCallback | null = null; + const pluginPropEnhancers: TreeViewItemPluginSlotPropsEnhancers[] = []; + const pluginPropEnhancersNames: { [key in keyof TreeViewItemPluginSlotPropsEnhancers]?: true } = + {}; plugins.forEach((plugin) => { if (!plugin.itemPlugin) { @@ -139,11 +144,47 @@ export const useTreeView = { + pluginPropEnhancersNames[ + propsEnhancerName as keyof TreeViewItemPluginSlotPropsEnhancers + ] = true; + }); + } }); + const resolvePropsEnhancer = + (currentSlotName: keyof TreeViewItemPluginSlotPropsEnhancers) => + (currentSlotParams: TreeViewItemPluginSlotPropsEnhancerParams) => { + const enhancedProps = {}; + pluginPropEnhancers.forEach((propsEnhancersForCurrentPlugin) => { + const propsEnhancerForCurrentPluginAndSlot = + propsEnhancersForCurrentPlugin[currentSlotName]; + if (propsEnhancerForCurrentPluginAndSlot != null) { + Object.assign(enhancedProps, propsEnhancerForCurrentPluginAndSlot(currentSlotParams)); + } + }); + + return enhancedProps; + }; + + const propsEnhancers = Object.fromEntries( + Object.keys(pluginPropEnhancersNames).map( + (propEnhancerName) => + [ + propEnhancerName, + resolvePropsEnhancer(propEnhancerName as keyof TreeViewItemPluginSlotPropsEnhancers), + ] as const, + ), + ); + return { contentRef: finalContentRef, rootRef: finalRootRef, + propsEnhancers, }; }; diff --git a/packages/x-tree-view/src/internals/utils/tree.ts b/packages/x-tree-view/src/internals/utils/tree.ts index 54c935a19f556..3bdb97a749448 100644 --- a/packages/x-tree-view/src/internals/utils/tree.ts +++ b/packages/x-tree-view/src/internals/utils/tree.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; import { TreeViewInstance } from '../models'; import type { UseTreeViewExpansionSignature } from '../plugins/useTreeViewExpansion'; import type { UseTreeViewItemsSignature } from '../plugins/useTreeViewItems'; @@ -259,3 +260,14 @@ export const getAllNavigableItems = ( return navigableItems; }; + +/** + * Checks if the event target is in a descendant of this item. + * This can prevent from firing some logic on the ancestors on the interacted item when the event handler is on the root. + * @param {React.UIEvent} event The event to check + * @param {HTMLElement | null} itemRoot The root of the item to check if the event target is in its descendants + * @returns {boolean} Whether the event target is in a descendant of this item + */ +export const isEventTargetInDescendants = (event: React.UIEvent, itemRoot: HTMLElement | null) => { + return itemRoot !== (event.target as HTMLElement).closest('*[role="treeitem"]'); +}; diff --git a/packages/x-tree-view/src/models/items.ts b/packages/x-tree-view/src/models/items.ts index 006f82e397247..f1ef54da13b9c 100644 --- a/packages/x-tree-view/src/models/items.ts +++ b/packages/x-tree-view/src/models/items.ts @@ -4,3 +4,9 @@ export type TreeViewItemId = string; export type TreeViewBaseItem = R & { children?: TreeViewBaseItem[]; }; + +export type TreeViewItemsReorderingAction = + | 'reorder-above' + | 'reorder-below' + | 'make-child' + | 'move-to-parent'; diff --git a/packages/x-tree-view/src/useTreeItem2/index.ts b/packages/x-tree-view/src/useTreeItem2/index.ts index dc1a81e7f114c..f99a0f545223f 100644 --- a/packages/x-tree-view/src/useTreeItem2/index.ts +++ b/packages/x-tree-view/src/useTreeItem2/index.ts @@ -3,5 +3,10 @@ export type { UseTreeItem2Parameters, UseTreeItem2ReturnValue, UseTreeItem2Status, + UseTreeItem2RootSlotOwnProps, UseTreeItem2ContentSlotOwnProps, + UseTreeItem2LabelSlotOwnProps, + UseTreeItem2IconContainerSlotOwnProps, + UseTreeItem2GroupTransitionSlotOwnProps, + UseTreeItem2DragAndDropOverlaySlotOwnProps, } from './useTreeItem2.types'; diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts index 7efc38056a9d4..e24a502687db2 100644 --- a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts +++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts @@ -10,10 +10,13 @@ import { UseTreeItem2LabelSlotProps, UseTreeItemIconContainerSlotProps, UseTreeItem2CheckboxSlotProps, + UseTreeItem2DragAndDropOverlaySlotProps, + UseTreeItem2RootSlotPropsFromUseTreeItem, + UseTreeItem2ContentSlotPropsFromUseTreeItem, } from './useTreeItem2.types'; -import { useTreeViewContext } from '../internals/TreeViewProvider/useTreeViewContext'; +import { useTreeViewContext } from '../internals/TreeViewProvider'; import { DefaultTreeViewPlugins } from '../internals/plugins/defaultPlugins'; -import { MuiCancellableEvent } from '../internals/models/MuiCancellableEvent'; +import { MuiCancellableEvent } from '../internals/models'; import { useTreeItem2Utils } from '../hooks/useTreeItem2Utils'; import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext'; @@ -32,10 +35,13 @@ export const useTreeItem2 = (null); + const contentRefObject = React.useRef(null); const idAttribute = instance.getTreeItemIdAttribute(itemId, id); - const handleRootRef = useForkRef(rootRef, pluginRootRef)!; + const handleRootRef = useForkRef(rootRef, pluginRootRef, rootRefObject)!; + const handleContentRef = useForkRef(contentRef, contentRefObject)!; const checkboxRef = React.useRef(null); const createRootHandleFocus = @@ -137,7 +143,7 @@ export const useTreeItem2 = = { + const props: UseTreeItem2RootSlotPropsFromUseTreeItem = { ...externalEventHandlers, ref: handleRootRef, role: 'treeitem', @@ -153,13 +159,19 @@ export const useTreeItem2 = ; }; const getContentProps = = {}>( @@ -167,20 +179,26 @@ export const useTreeItem2 = => { const externalEventHandlers = extractEventHandlers(externalProps); - const response: UseTreeItem2ContentSlotProps = { + const props: UseTreeItem2ContentSlotPropsFromUseTreeItem = { ...externalEventHandlers, ...externalProps, - ref: contentRef, + ref: handleContentRef, onClick: createContentHandleClick(externalEventHandlers), onMouseDown: createContentHandleMouseDown(externalEventHandlers), status, }; if (indentationAtItemLevel) { - response.indentationAtItemLevel = true; + props.indentationAtItemLevel = true; } - return response; + const enhancedContentProps = + propsEnhancers.content?.({ rootRefObject, contentRefObject, externalEventHandlers }) ?? {}; + + return { + ...props, + ...enhancedContentProps, + } as UseTreeItem2ContentSlotProps; }; const getCheckboxProps = = {}>( @@ -204,7 +222,6 @@ export const useTreeItem2 = => { const externalEventHandlers = { - ...extractEventHandlers(parameters), ...extractEventHandlers(externalProps), }; @@ -248,6 +265,26 @@ export const useTreeItem2 = = {}>( + externalProps: ExternalProps = {} as ExternalProps, + ): UseTreeItem2DragAndDropOverlaySlotProps => { + const externalEventHandlers = { + ...extractEventHandlers(externalProps), + }; + + const enhancedDragAndDropOverlayProps = + propsEnhancers.dragAndDropOverlay?.({ + rootRefObject, + contentRefObject, + externalEventHandlers, + }) ?? {}; + + return { + ...externalProps, + ...enhancedDragAndDropOverlayProps, + } as UseTreeItem2DragAndDropOverlaySlotProps; + }; + return { getRootProps, getContentProps, @@ -255,6 +292,7 @@ export const useTreeItem2 = = ExternalProps & UseTreeItem2RootSlotOwnProps; -export interface UseTreeItem2ContentSlotOwnProps { +export interface UseTreeItem2ContentSlotPropsFromUseTreeItem { onClick: MuiCancellableEventHandler; onMouseDown: MuiCancellableEventHandler; ref: React.RefCallback | null; @@ -60,6 +65,9 @@ export interface UseTreeItem2ContentSlotOwnProps { indentationAtItemLevel?: true; } +export interface UseTreeItem2ContentSlotOwnProps + extends UseTreeItem2ContentSlotPropsFromUseTreeItem {} + export type UseTreeItem2ContentSlotProps = ExternalProps & UseTreeItem2ContentSlotOwnProps; @@ -102,6 +110,11 @@ export interface UseTreeItem2GroupTransitionSlotOwnProps { export type UseTreeItem2GroupTransitionSlotProps = ExternalProps & UseTreeItem2GroupTransitionSlotOwnProps; +export interface UseTreeItem2DragAndDropOverlaySlotOwnProps {} + +export type UseTreeItem2DragAndDropOverlaySlotProps = ExternalProps & + UseTreeItem2DragAndDropOverlaySlotOwnProps; + export interface UseTreeItem2Status { expandable: boolean; expanded: boolean; @@ -159,6 +172,15 @@ export interface UseTreeItem2ReturnValue = {}>( externalProps?: ExternalProps, ) => UseTreeItem2GroupTransitionSlotProps; + /** + * Resolver for the DragAndDropOverlay slot's props. + * Warning: This slot is only useful when using the `RichTreeViewPro` component. + * @param {ExternalProps} externalProps Additional props for the DragAndDropOverlay slot + * @returns {UseTreeItem2DragAndDropOverlaySlotProps} Props that should be spread on the DragAndDropOverlay slot + */ + getDragAndDropOverlayProps: = {}>( + externalProps?: ExternalProps, + ) => UseTreeItem2DragAndDropOverlaySlotProps; /** * A ref to the component's root DOM element. */ diff --git a/scripts/x-tree-view.exports.json b/scripts/x-tree-view.exports.json index aba988ecc2335..d5d3638a2daba 100644 --- a/scripts/x-tree-view.exports.json +++ b/scripts/x-tree-view.exports.json @@ -56,14 +56,20 @@ { "name": "TreeViewCollapseIcon", "kind": "Variable" }, { "name": "TreeViewExpandIcon", "kind": "Variable" }, { "name": "TreeViewItemId", "kind": "TypeAlias" }, + { "name": "TreeViewItemsReorderingAction", "kind": "TypeAlias" }, { "name": "TreeViewProps", "kind": "Interface" }, { "name": "TreeViewSlotProps", "kind": "Interface" }, { "name": "TreeViewSlots", "kind": "Interface" }, { "name": "unstable_resetCleanupTracking", "kind": "Variable" }, { "name": "unstable_useTreeItem2", "kind": "Variable" }, { "name": "UseTreeItem2ContentSlotOwnProps", "kind": "Interface" }, + { "name": "UseTreeItem2DragAndDropOverlaySlotOwnProps", "kind": "Interface" }, + { "name": "UseTreeItem2GroupTransitionSlotOwnProps", "kind": "Interface" }, + { "name": "UseTreeItem2IconContainerSlotOwnProps", "kind": "Interface" }, + { "name": "UseTreeItem2LabelSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItem2Parameters", "kind": "Interface" }, { "name": "UseTreeItem2ReturnValue", "kind": "Interface" }, + { "name": "UseTreeItem2RootSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItem2Status", "kind": "Interface" }, { "name": "useTreeItem2Utils", "kind": "Variable" }, { "name": "useTreeItemState", "kind": "Function" }, diff --git a/test/utils/dragAndDrop.ts b/test/utils/dragAndDrop.ts new file mode 100644 index 0000000000000..04a4c05d17d9d --- /dev/null +++ b/test/utils/dragAndDrop.ts @@ -0,0 +1,65 @@ +export type DragEventTypes = + | 'dragStart' + | 'dragOver' + | 'dragEnter' + | 'dragLeave' + | 'dragEnd' + | 'drop'; + +export class MockedDataTransfer implements DataTransfer { + data: Record; + + dropEffect: 'none' | 'copy' | 'move' | 'link'; + + effectAllowed: + | 'none' + | 'copy' + | 'copyLink' + | 'copyMove' + | 'link' + | 'linkMove' + | 'move' + | 'all' + | 'uninitialized'; + + files: FileList; + + img?: Element; + + items: DataTransferItemList; + + types: string[]; + + xOffset: number; + + yOffset: number; + + constructor() { + this.data = {}; + this.dropEffect = 'none'; + this.effectAllowed = 'all'; + this.files = [] as unknown as FileList; + this.items = [] as unknown as DataTransferItemList; + this.types = []; + this.xOffset = 0; + this.yOffset = 0; + } + + clearData() { + this.data = {}; + } + + getData(format: string) { + return this.data[format]; + } + + setData(format: string, data: string) { + this.data[format] = data; + } + + setDragImage(img: Element, xOffset: number, yOffset: number) { + this.img = img; + this.xOffset = xOffset; + this.yOffset = yOffset; + } +} diff --git a/test/utils/pickers/calendar.ts b/test/utils/pickers/calendar.ts index c06241ae0dc16..93fd8107afe13 100644 --- a/test/utils/pickers/calendar.ts +++ b/test/utils/pickers/calendar.ts @@ -1,4 +1,5 @@ import { fireEvent, createEvent } from '@mui/internal-test-utils'; +import { DragEventTypes } from '../dragAndDrop'; export const rangeCalendarDayTouches = { '2018-01-01': { @@ -54,69 +55,3 @@ export const buildPickerDragInteractions = (getDataTransfer: () => DataTransfer return { executeDateDragWithoutDrop, executeDateDrag }; }; - -export type DragEventTypes = - | 'dragStart' - | 'dragOver' - | 'dragEnter' - | 'dragLeave' - | 'dragEnd' - | 'drop'; - -export class MockedDataTransfer implements DataTransfer { - data: Record; - - dropEffect: 'none' | 'copy' | 'move' | 'link'; - - effectAllowed: - | 'none' - | 'copy' - | 'copyLink' - | 'copyMove' - | 'link' - | 'linkMove' - | 'move' - | 'all' - | 'uninitialized'; - - files: FileList; - - img?: Element; - - items: DataTransferItemList; - - types: string[]; - - xOffset: number; - - yOffset: number; - - constructor() { - this.data = {}; - this.dropEffect = 'none'; - this.effectAllowed = 'all'; - this.files = [] as unknown as FileList; - this.items = [] as unknown as DataTransferItemList; - this.types = []; - this.xOffset = 0; - this.yOffset = 0; - } - - clearData() { - this.data = {}; - } - - getData(format: string) { - return this.data[format]; - } - - setData(format: string, data: string) { - this.data[format] = data; - } - - setDragImage(img: Element, xOffset: number, yOffset: number) { - this.img = img; - this.xOffset = xOffset; - this.yOffset = yOffset; - } -} diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.tsx b/test/utils/tree-view/describeTreeView/describeTreeView.tsx index 7cb6c9a9c8e82..8db988b0a6822 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.tsx +++ b/test/utils/tree-view/describeTreeView/describeTreeView.tsx @@ -6,6 +6,7 @@ import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem, treeItemClasses } from '@mui/x-tree-view/TreeItem'; import { TreeItem2 } from '@mui/x-tree-view/TreeItem2'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; import { TreeViewAnyPluginSignature, TreeViewPublicAPI } from '@mui/x-tree-view/internals/models'; import { MuiRenderResult } from '@mui/internal-test-utils/createRenderer'; import { @@ -14,6 +15,7 @@ import { DescribeTreeViewJSXRenderer, DescribeTreeViewItem, DescribeTreeViewRendererUtils, + TreeViewItemIdTreeElement, } from './describeTreeView.types'; const innerDescribeTreeView = ( @@ -22,12 +24,34 @@ const innerDescribeTreeView = ( ): void => { const { render } = createRenderer(); - const getUtils = (result: MuiRenderResult): DescribeTreeViewRendererUtils => { + const getUtils = ( + result: MuiRenderResult, + apiRef?: { current: TreeViewPublicAPI }, + ): DescribeTreeViewRendererUtils => { const getRoot = () => result.getByRole('tree'); const getAllTreeItemIds = () => result.queryAllByRole('treeitem').map((item) => item.dataset.testid!); + const getItemIdTree = (): TreeViewItemIdTreeElement[] => { + if (!apiRef) { + throw new Error( + 'Cannot use getItemIdTree in renderFromJSX because the apiRef is not defined', + ); + } + + const cleanItem = (item: TreeViewBaseItem) => { + if (item.children) { + return { id: item.id, children: item.children.map(cleanItem) }; + } + + return { id: item.id }; + }; + + // @ts-ignore + return apiRef.current!.getItemTree().map(cleanItem); + }; + const getFocusedItemId = () => { const activeElement = document.activeElement; if (!activeElement || !activeElement.classList.contains(treeItemClasses.root)) { @@ -77,6 +101,7 @@ const innerDescribeTreeView = ( isItemExpanded, isItemSelected, getSelectedTreeItems, + getItemIdTree, }; }; @@ -133,7 +158,7 @@ const innerDescribeTreeView = ( setProps: result.setProps, setItems: (newItems) => result.setProps({ items: newItems }), apiRef: apiRef as unknown as { current: TreeViewPublicAPI }, - ...getUtils(result), + ...getUtils(result, apiRef as unknown as { current: TreeViewPublicAPI }), }; }; diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts index daf2330aa115e..58b14a1f3fb61 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts +++ b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts @@ -2,8 +2,10 @@ import * as React from 'react'; import { MergePluginsProperty, TreeViewAnyPluginSignature, + TreeViewExperimentalFeatures, TreeViewPublicAPI, } from '@mui/x-tree-view/internals/models'; +import { TreeViewItemId } from '@mui/x-tree-view/models'; import { TreeItemProps } from '@mui/x-tree-view/TreeItem'; import { TreeItem2Props } from '@mui/x-tree-view/TreeItem2'; @@ -11,6 +13,11 @@ export type DescribeTreeViewTestRunner, ) => void; +export interface TreeViewItemIdTreeElement { + id: TreeViewItemId; + children?: TreeViewItemIdTreeElement[]; +} + export interface DescribeTreeViewRendererUtils { /** * Returns the `root` slot of the Tree View. @@ -83,6 +90,7 @@ export interface DescribeTreeViewRendererUtils { * @returns {HTMLElement[]} List of the item id of all the items currently selected. */ getSelectedTreeItems: () => string[]; + getItemIdTree: () => TreeViewItemIdTreeElement[]; } export interface DescribeTreeViewRendererReturnValue @@ -119,6 +127,7 @@ export type DescribeTreeViewRenderer & { item?: Partial | Partial; }; + experimentalFeatures?: TreeViewExperimentalFeatures; }, ) => DescribeTreeViewRendererReturnValue; From 7374239ff531cd85bb9c71603662403cb4f10391 Mon Sep 17 00:00:00 2001 From: delangle Date: Fri, 7 Jun 2024 08:31:05 +0200 Subject: [PATCH 02/16] Fix --- .../useTreeViewItemsReordering.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx index 95fa1bf2b47f5..5462bb5119cec 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx @@ -53,7 +53,7 @@ const buildTreeViewDragInteractions = (dataTransfer: DataTransfer) => { }; describeTreeView<[UseTreeViewItemsReorderingSignature, UseTreeViewItemsSignature]>( - 'useTreeViewKeyboardNavigation', + 'useTreeViewItemsReordering', ({ render, treeViewComponentName }) => { if (treeViewComponentName === 'SimpleTreeView' || treeViewComponentName === 'RichTreeView') { return; From ac8eb363508638cb4a12750c7481ea5360a4b1d6 Mon Sep 17 00:00:00 2001 From: delangle Date: Mon, 10 Jun 2024 10:53:39 +0200 Subject: [PATCH 03/16] Fix styled import --- .../TreeItem2DragAndDropOverlay.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.tsx b/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.tsx index 772230496a280..a435c62372f8b 100644 --- a/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.tsx +++ b/packages/x-tree-view/src/TreeItem2DragAndDropOverlay/TreeItem2DragAndDropOverlay.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { alpha, styled } from '@mui/material/styles'; +import { alpha } from '@mui/material/styles'; import { shouldForwardProp } from '@mui/system'; import { TreeItem2DragAndDropOverlayProps } from './TreeItem2DragAndDropOverlay.types'; import { TreeViewItemsReorderingAction } from '../models'; +import { styled } from '../internals/zero-styled'; const TreeItem2DragAndDropOverlayRoot = styled('div', { name: 'MuiTreeItem2DragAndDropOverlay', From ddd06f4675e23909efe2ff7d5945d580412f65c3 Mon Sep 17 00:00:00 2001 From: delangle Date: Thu, 27 Jun 2024 08:44:47 +0200 Subject: [PATCH 04/16] Try to fix iOS --- .../useTreeViewItemsReordering.itemPlugin.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts index e522f4c7bf0e8..4cd36c48ee46f 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts @@ -60,6 +60,9 @@ export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin Date: Thu, 27 Jun 2024 08:52:52 +0200 Subject: [PATCH 05/16] Fix TS error --- .../useTreeViewItemsReordering.types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts index cc11f3dae8218..4341a725e89bd 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts @@ -4,7 +4,6 @@ import { TreeViewPluginSignature, UseTreeViewItemsSignature, MuiCancellableEventHandler, - UseTreeViewIdSignature, } from '@mui/x-tree-view/internals'; import { TreeViewItemId, TreeViewItemsReorderingAction } from '@mui/x-tree-view/models'; import { TreeItem2DragAndDropOverlayProps } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'; @@ -129,7 +128,7 @@ export type UseTreeViewItemsReorderingSignature = TreeViewPluginSignature<{ instance: UseTreeViewItemsReorderingInstance; state: UseTreeViewItemsReorderingState; contextValue: UseTreeViewItemsReorderingContextValue; - dependencies: [UseTreeViewItemsSignature, UseTreeViewIdSignature]; + dependencies: [UseTreeViewItemsSignature]; }>; export interface UseTreeItem2RootSlotPropsFromItemsReordering { From da1bc851c918915e517fd204e93f2068d26c7bd5 Mon Sep 17 00:00:00 2001 From: delangle Date: Wed, 10 Jul 2024 14:49:43 +0200 Subject: [PATCH 06/16] Review: Nora --- docs/data/tree-view/rich-tree-view/ordering/ordering.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/data/tree-view/rich-tree-view/ordering/ordering.md b/docs/data/tree-view/rich-tree-view/ordering/ordering.md index 5c9a169649e24..67a04fa65e7dc 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/ordering.md +++ b/docs/data/tree-view/rich-tree-view/ordering/ordering.md @@ -12,11 +12,7 @@ waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/

Drag and drop your items to reorder them.

:::success -To be able to reorder items, you first have to enable the `indentationAtItemLevel` experimental feature. -When this flag is enabled, the indentation of nested items is applied by the item itself instead of its ancestors. -This allows correctly placing the drag & drop overlay. - -You can enable it as follows: +To be able to reorder items, you first have to enable the `indentationAtItemLevel` experimental feature: ```tsx ``` -If you are building your custom Tree Item, make sure that you do not apply any custom `padding-left` on your Tree Item Content component. -If you want to change the indentation value, you can use the `itemChildrenIndentation` prop on the Tree View component. +See [Tree Item Customization—Apply the nested item's indentation at the item level](/x/react-tree-view/tree-item-customization/#apply-the-nested-items-indentation-at-the-item-level) for more details. ::: ## Enable drag & drop re-ordering From 6e0546df694c1c6fc0054c07a0d05b0f77c4c7a1 Mon Sep 17 00:00:00 2001 From: delangle Date: Fri, 12 Jul 2024 09:06:19 +0200 Subject: [PATCH 07/16] Add file explorer demo --- .../rich-tree-view/ordering/FileExplorer.js | 261 ++++++++++++++++ .../rich-tree-view/ordering/FileExplorer.tsx | 291 ++++++++++++++++++ .../ordering/FileExplorer.tsx.preview | 13 + .../rich-tree-view/ordering/ordering.md | 9 + 4 files changed, 574 insertions(+) create mode 100644 docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js create mode 100644 docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx create mode 100644 docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx.preview diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js new file mode 100644 index 0000000000000..621e0b3f995e7 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js @@ -0,0 +1,261 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { styled, alpha } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import ArticleIcon from '@mui/icons-material/Article'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FolderRounded from '@mui/icons-material/FolderRounded'; +import ImageIcon from '@mui/icons-material/Image'; +import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; +import VideoCameraBackIcon from '@mui/icons-material/VideoCameraBack'; +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; +import { treeItemClasses } from '@mui/x-tree-view/TreeItem'; +import { unstable_useTreeItem2 as useTreeItem2 } from '@mui/x-tree-view/useTreeItem2'; +import { + TreeItem2Checkbox, + TreeItem2Content, + TreeItem2IconContainer, + TreeItem2Label, + TreeItem2Root, + TreeItem2GroupTransition, +} from '@mui/x-tree-view/TreeItem2'; +import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; +import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; +import { TreeItem2DragAndDropOverlay } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'; + +import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; + +const ITEMS = [ + { + id: '1', + label: 'Documents', + fileType: 'folder', + children: [ + { + id: '1.1', + label: 'Company', + fileType: 'folder', + children: [ + { id: '1.1.1', label: 'Invoice', fileType: 'pdf' }, + { id: '1.1.2', label: 'Meeting notes', fileType: 'doc' }, + { id: '1.1.3', label: 'Tasks list', fileType: 'doc' }, + { id: '1.1.4', label: 'Equipment', fileType: 'pdf' }, + { id: '1.1.5', label: 'Video conference', fileType: 'video' }, + ], + }, + { id: '1.2', label: 'Personal', fileType: 'folder' }, + { id: '1.3', label: 'Group photo', fileType: 'image' }, + ], + }, + { + id: '2', + label: 'Bookmarked', + fileType: 'folder', + children: [ + { id: '2.1', label: 'Learning materials', fileType: 'folder' }, + { id: '2.2', label: 'News', fileType: 'folder' }, + { id: '2.3', label: 'Forums', fileType: 'folder' }, + { id: '2.4', label: 'Travel documents', fileType: 'pdf' }, + ], + }, + { id: '3', label: 'History', fileType: 'folder' }, + { id: '4', label: 'Trash', fileType: 'trash' }, +]; + +function DotIcon() { + return ( + + ); +} + +const StyledTreeItemRoot = styled(TreeItem2Root)(({ theme }) => ({ + color: + theme.palette.mode === 'light' + ? theme.palette.grey[800] + : theme.palette.grey[400], + position: 'relative', + [`& .${treeItemClasses.groupTransition}`]: { + marginLeft: theme.spacing(3.5), + }, +})); + +const CustomTreeItemContent = styled(TreeItem2Content)(({ theme }) => ({ + flexDirection: 'row-reverse', + borderRadius: theme.spacing(0.7), + marginBottom: theme.spacing(0.5), + marginTop: theme.spacing(0.5), + paddingRight: theme.spacing(1), + fontWeight: 500, + [`&.Mui-expanded `]: { + '&:not(.Mui-focused, .Mui-selected, .Mui-selected.Mui-focused) .labelIcon': { + color: + theme.palette.mode === 'light' + ? theme.palette.primary.main + : theme.palette.primary.dark, + }, + '&::before': { + content: '""', + display: 'block', + position: 'absolute', + left: '16px', + top: '44px', + height: 'calc(100% - 48px)', + width: '1.5px', + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.grey[300] + : theme.palette.grey[700], + }, + }, + '&:hover': { + backgroundColor: alpha(theme.palette.primary.main, 0.1), + color: theme.palette.mode === 'light' ? theme.palette.primary.main : 'white', + }, + [`&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused`]: { + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.primary.main + : theme.palette.primary.dark, + color: theme.palette.primary.contrastText, + }, +})); + +const StyledTreeItemLabelText = styled(Typography)({ + color: 'inherit', + fontFamily: 'General Sans', + fontWeight: 500, +}); + +function CustomLabel({ icon: Icon, expandable, children, ...other }) { + return ( + + {Icon && ( + + )} + + {children} + {expandable && } + + ); +} + +const isExpandable = (reactChildren) => { + if (Array.isArray(reactChildren)) { + return reactChildren.length > 0 && reactChildren.some(isExpandable); + } + return Boolean(reactChildren); +}; + +const getIconFromFileType = (fileType) => { + switch (fileType) { + case 'image': + return ImageIcon; + case 'pdf': + return PictureAsPdfIcon; + case 'doc': + return ArticleIcon; + case 'video': + return VideoCameraBackIcon; + case 'folder': + return FolderRounded; + case 'trash': + return DeleteIcon; + default: + return ArticleIcon; + } +}; + +const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { + const { id, itemId, label, disabled, children, ...other } = props; + + const { + getRootProps, + getContentProps, + getIconContainerProps, + getCheckboxProps, + getLabelProps, + getGroupTransitionProps, + getDragAndDropOverlayProps, + status, + publicAPI, + } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); + + const item = publicAPI.getItem(itemId); + const expandable = isExpandable(children); + const icon = getIconFromFileType(item.fileType); + + return ( + + + + + + + + + + + {children && } + + + ); +}); + +export default function FileExplorer() { + const apiRef = useTreeViewApiRef(); + + return ( + { + return ( + params.newPosition.parentId === null || + ['folder', 'trash'].includes( + apiRef.current.getItem(params.newPosition.parentId).fileType, + ) + ); + }} + /> + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx new file mode 100644 index 0000000000000..e98321ee66ae6 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx @@ -0,0 +1,291 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { styled, alpha } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import ArticleIcon from '@mui/icons-material/Article'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FolderRounded from '@mui/icons-material/FolderRounded'; +import ImageIcon from '@mui/icons-material/Image'; +import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; +import VideoCameraBackIcon from '@mui/icons-material/VideoCameraBack'; +import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; +import { treeItemClasses } from '@mui/x-tree-view/TreeItem'; +import { + unstable_useTreeItem2 as useTreeItem2, + UseTreeItem2Parameters, +} from '@mui/x-tree-view/useTreeItem2'; +import { + TreeItem2Checkbox, + TreeItem2Content, + TreeItem2IconContainer, + TreeItem2Label, + TreeItem2Root, + TreeItem2GroupTransition, +} from '@mui/x-tree-view/TreeItem2'; +import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; +import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; +import { TreeItem2DragAndDropOverlay } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; +import {useTreeViewApiRef} from "@mui/x-tree-view/hooks"; + +type FileType = 'image' | 'pdf' | 'doc' | 'video' | 'folder' | 'pinned' | 'trash'; + +type ExtendedTreeItemProps = { + fileType: FileType; + id: string; + label: string; +}; + +const ITEMS: TreeViewBaseItem[] = [ + { + id: '1', + label: 'Documents', + fileType: 'folder', + children: [ + { + id: '1.1', + label: 'Company', + fileType: 'folder', + children: [ + { id: '1.1.1', label: 'Invoice', fileType: 'pdf' }, + { id: '1.1.2', label: 'Meeting notes', fileType: 'doc' }, + { id: '1.1.3', label: 'Tasks list', fileType: 'doc' }, + { id: '1.1.4', label: 'Equipment', fileType: 'pdf' }, + { id: '1.1.5', label: 'Video conference', fileType: 'video' }, + ], + }, + { id: '1.2', label: 'Personal', fileType: 'folder' }, + { id: '1.3', label: 'Group photo', fileType: 'image' }, + ], + }, + { + id: '2', + label: 'Bookmarked', + fileType: 'folder', + children: [ + { id: '2.1', label: 'Learning materials', fileType: 'folder' }, + { id: '2.2', label: 'News', fileType: 'folder' }, + { id: '2.3', label: 'Forums', fileType: 'folder' }, + { id: '2.4', label: 'Travel documents', fileType: 'pdf' }, + ], + }, + { id: '3', label: 'History', fileType: 'folder' }, + { id: '4', label: 'Trash', fileType: 'trash' }, +]; + +function DotIcon() { + return ( + + ); +} +declare module 'react' { + interface CSSProperties { + '--tree-view-color'?: string; + '--tree-view-bg-color'?: string; + } +} + +const StyledTreeItemRoot = styled(TreeItem2Root)(({ theme }) => ({ + color: + theme.palette.mode === 'light' + ? theme.palette.grey[800] + : theme.palette.grey[400], + position: 'relative', + [`& .${treeItemClasses.groupTransition}`]: { + marginLeft: theme.spacing(3.5), + }, +})) as unknown as typeof TreeItem2Root; + +const CustomTreeItemContent = styled(TreeItem2Content)(({ theme }) => ({ + flexDirection: 'row-reverse', + borderRadius: theme.spacing(0.7), + marginBottom: theme.spacing(0.5), + marginTop: theme.spacing(0.5), + paddingRight: theme.spacing(1), + fontWeight: 500, + [`&.Mui-expanded `]: { + '&:not(.Mui-focused, .Mui-selected, .Mui-selected.Mui-focused) .labelIcon': { + color: + theme.palette.mode === 'light' + ? theme.palette.primary.main + : theme.palette.primary.dark, + }, + '&::before': { + content: '""', + display: 'block', + position: 'absolute', + left: '16px', + top: '44px', + height: 'calc(100% - 48px)', + width: '1.5px', + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.grey[300] + : theme.palette.grey[700], + }, + }, + '&:hover': { + backgroundColor: alpha(theme.palette.primary.main, 0.1), + color: theme.palette.mode === 'light' ? theme.palette.primary.main : 'white', + }, + [`&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused`]: { + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.primary.main + : theme.palette.primary.dark, + color: theme.palette.primary.contrastText, + }, +})); + +const StyledTreeItemLabelText = styled(Typography)({ + color: 'inherit', + fontFamily: 'General Sans', + fontWeight: 500, +}) as unknown as typeof Typography; + +interface CustomLabelProps { + children: React.ReactNode; + icon?: React.ElementType; + expandable?: boolean; +} + +function CustomLabel({ + icon: Icon, + expandable, + children, + ...other + }: CustomLabelProps) { + return ( + + {Icon && ( + + )} + + {children} + {expandable && } + + ); +} + +const isExpandable = (reactChildren: React.ReactNode) => { + if (Array.isArray(reactChildren)) { + return reactChildren.length > 0 && reactChildren.some(isExpandable); + } + return Boolean(reactChildren); +}; + +const getIconFromFileType = (fileType: FileType) => { + switch (fileType) { + case 'image': + return ImageIcon; + case 'pdf': + return PictureAsPdfIcon; + case 'doc': + return ArticleIcon; + case 'video': + return VideoCameraBackIcon; + case 'folder': + return FolderRounded; + case 'trash': + return DeleteIcon; + default: + return ArticleIcon; + } +}; + +interface CustomTreeItemProps + extends Omit, + Omit, 'onFocus'> {} + +const CustomTreeItem = React.forwardRef(function CustomTreeItem( + props: CustomTreeItemProps, + ref: React.Ref, +) { + const { id, itemId, label, disabled, children, ...other } = props; + + const { + getRootProps, + getContentProps, + getIconContainerProps, + getCheckboxProps, + getLabelProps, + getGroupTransitionProps, + getDragAndDropOverlayProps, + status, + publicAPI, + } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); + + const item = publicAPI.getItem(itemId); + const expandable = isExpandable(children); + const icon = getIconFromFileType(item.fileType); + + return ( + + + + + + + + + + + { children && } + + + ); +}); + +export default function FileExplorer() { + const apiRef = useTreeViewApiRef() + + return ( + { + return params.newPosition.parentId === null || ['folder', 'trash'].includes(apiRef.current!.getItem(params.newPosition.parentId).fileType) + }} + /> + ); +} diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx.preview b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx.preview new file mode 100644 index 0000000000000..6b4fb9fea0dad --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx.preview @@ -0,0 +1,13 @@ + { + return params.newPosition.parentId === null || ['folder', 'trash'].includes(apiRef.current!.getItem(params.newPosition.parentId).fileType) + }} +/> \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/ordering/ordering.md b/docs/data/tree-view/rich-tree-view/ordering/ordering.md index 67a04fa65e7dc..6f4b49af5d888 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/ordering.md +++ b/docs/data/tree-view/rich-tree-view/ordering/ordering.md @@ -61,3 +61,12 @@ The following demo demonstrates it by synchronizing the first tree view with the You can create a custom Tree Item component to render a drag handle icon and only trigger the reordering when dragging from it: {{"demo": "OnlyReorderFromDragHandle.js"}} + +## Common examples + +### File explorer + +The example below is an improved version of the [File Explorer](/x/react-tree-view/rich-tree-view/customization/#file-explorer) example with drag & drop re-ordering. +You can re-order items but only inside folders (or inside the trash). + +{{"demo": "FileExplorer.js"}} \ No newline at end of file From 44b881a756ae9d31942cf044b7d38ba2f36b8b06 Mon Sep 17 00:00:00 2001 From: delangle Date: Fri, 12 Jul 2024 09:08:39 +0200 Subject: [PATCH 08/16] Work --- docs/data/tree-view/rich-tree-view/ordering/ordering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/tree-view/rich-tree-view/ordering/ordering.md b/docs/data/tree-view/rich-tree-view/ordering/ordering.md index 6f4b49af5d888..c684cce16a764 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/ordering.md +++ b/docs/data/tree-view/rich-tree-view/ordering/ordering.md @@ -66,7 +66,7 @@ You can create a custom Tree Item component to render a drag handle icon and onl ### File explorer -The example below is an improved version of the [File Explorer](/x/react-tree-view/rich-tree-view/customization/#file-explorer) example with drag & drop re-ordering. +The example below is a simplified version of the [File Explorer](/x/react-tree-view/rich-tree-view/customization/#file-explorer) example with drag & drop re-ordering. You can re-order items but only inside folders (or inside the trash). {{"demo": "FileExplorer.js"}} \ No newline at end of file From 0e56b1a55212608cd7fbe3f242d7a060c88c4f72 Mon Sep 17 00:00:00 2001 From: delangle Date: Fri, 12 Jul 2024 09:18:43 +0200 Subject: [PATCH 09/16] Fix CI --- .../rich-tree-view/ordering/FileExplorer.tsx | 439 +++++++++--------- .../ordering/FileExplorer.tsx.preview | 13 - .../rich-tree-view/ordering/ordering.md | 2 +- 3 files changed, 223 insertions(+), 231 deletions(-) delete mode 100644 docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx.preview diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx index e98321ee66ae6..be7e622c018b5 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx @@ -12,280 +12,285 @@ import VideoCameraBackIcon from '@mui/icons-material/VideoCameraBack'; import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; import { treeItemClasses } from '@mui/x-tree-view/TreeItem'; import { - unstable_useTreeItem2 as useTreeItem2, - UseTreeItem2Parameters, + unstable_useTreeItem2 as useTreeItem2, + UseTreeItem2Parameters, } from '@mui/x-tree-view/useTreeItem2'; import { - TreeItem2Checkbox, - TreeItem2Content, - TreeItem2IconContainer, - TreeItem2Label, - TreeItem2Root, - TreeItem2GroupTransition, + TreeItem2Checkbox, + TreeItem2Content, + TreeItem2IconContainer, + TreeItem2Label, + TreeItem2Root, + TreeItem2GroupTransition, } from '@mui/x-tree-view/TreeItem2'; import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; import { TreeItem2DragAndDropOverlay } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'; import { TreeViewBaseItem } from '@mui/x-tree-view/models'; -import {useTreeViewApiRef} from "@mui/x-tree-view/hooks"; +import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; type FileType = 'image' | 'pdf' | 'doc' | 'video' | 'folder' | 'pinned' | 'trash'; type ExtendedTreeItemProps = { - fileType: FileType; - id: string; - label: string; + fileType: FileType; + id: string; + label: string; }; const ITEMS: TreeViewBaseItem[] = [ - { - id: '1', - label: 'Documents', + { + id: '1', + label: 'Documents', + fileType: 'folder', + children: [ + { + id: '1.1', + label: 'Company', fileType: 'folder', children: [ - { - id: '1.1', - label: 'Company', - fileType: 'folder', - children: [ - { id: '1.1.1', label: 'Invoice', fileType: 'pdf' }, - { id: '1.1.2', label: 'Meeting notes', fileType: 'doc' }, - { id: '1.1.3', label: 'Tasks list', fileType: 'doc' }, - { id: '1.1.4', label: 'Equipment', fileType: 'pdf' }, - { id: '1.1.5', label: 'Video conference', fileType: 'video' }, - ], - }, - { id: '1.2', label: 'Personal', fileType: 'folder' }, - { id: '1.3', label: 'Group photo', fileType: 'image' }, + { id: '1.1.1', label: 'Invoice', fileType: 'pdf' }, + { id: '1.1.2', label: 'Meeting notes', fileType: 'doc' }, + { id: '1.1.3', label: 'Tasks list', fileType: 'doc' }, + { id: '1.1.4', label: 'Equipment', fileType: 'pdf' }, + { id: '1.1.5', label: 'Video conference', fileType: 'video' }, ], - }, - { - id: '2', - label: 'Bookmarked', - fileType: 'folder', - children: [ - { id: '2.1', label: 'Learning materials', fileType: 'folder' }, - { id: '2.2', label: 'News', fileType: 'folder' }, - { id: '2.3', label: 'Forums', fileType: 'folder' }, - { id: '2.4', label: 'Travel documents', fileType: 'pdf' }, - ], - }, - { id: '3', label: 'History', fileType: 'folder' }, - { id: '4', label: 'Trash', fileType: 'trash' }, + }, + { id: '1.2', label: 'Personal', fileType: 'folder' }, + { id: '1.3', label: 'Group photo', fileType: 'image' }, + ], + }, + { + id: '2', + label: 'Bookmarked', + fileType: 'folder', + children: [ + { id: '2.1', label: 'Learning materials', fileType: 'folder' }, + { id: '2.2', label: 'News', fileType: 'folder' }, + { id: '2.3', label: 'Forums', fileType: 'folder' }, + { id: '2.4', label: 'Travel documents', fileType: 'pdf' }, + ], + }, + { id: '3', label: 'History', fileType: 'folder' }, + { id: '4', label: 'Trash', fileType: 'trash' }, ]; function DotIcon() { - return ( - - ); + return ( + + ); } declare module 'react' { - interface CSSProperties { - '--tree-view-color'?: string; - '--tree-view-bg-color'?: string; - } + interface CSSProperties { + '--tree-view-color'?: string; + '--tree-view-bg-color'?: string; + } } const StyledTreeItemRoot = styled(TreeItem2Root)(({ theme }) => ({ - color: - theme.palette.mode === 'light' - ? theme.palette.grey[800] - : theme.palette.grey[400], - position: 'relative', - [`& .${treeItemClasses.groupTransition}`]: { - marginLeft: theme.spacing(3.5), - }, + color: + theme.palette.mode === 'light' + ? theme.palette.grey[800] + : theme.palette.grey[400], + position: 'relative', + [`& .${treeItemClasses.groupTransition}`]: { + marginLeft: theme.spacing(3.5), + }, })) as unknown as typeof TreeItem2Root; const CustomTreeItemContent = styled(TreeItem2Content)(({ theme }) => ({ - flexDirection: 'row-reverse', - borderRadius: theme.spacing(0.7), - marginBottom: theme.spacing(0.5), - marginTop: theme.spacing(0.5), - paddingRight: theme.spacing(1), - fontWeight: 500, - [`&.Mui-expanded `]: { - '&:not(.Mui-focused, .Mui-selected, .Mui-selected.Mui-focused) .labelIcon': { - color: - theme.palette.mode === 'light' - ? theme.palette.primary.main - : theme.palette.primary.dark, - }, - '&::before': { - content: '""', - display: 'block', - position: 'absolute', - left: '16px', - top: '44px', - height: 'calc(100% - 48px)', - width: '1.5px', - backgroundColor: - theme.palette.mode === 'light' - ? theme.palette.grey[300] - : theme.palette.grey[700], - }, - }, - '&:hover': { - backgroundColor: alpha(theme.palette.primary.main, 0.1), - color: theme.palette.mode === 'light' ? theme.palette.primary.main : 'white', + flexDirection: 'row-reverse', + borderRadius: theme.spacing(0.7), + marginBottom: theme.spacing(0.5), + marginTop: theme.spacing(0.5), + paddingRight: theme.spacing(1), + fontWeight: 500, + [`&.Mui-expanded `]: { + '&:not(.Mui-focused, .Mui-selected, .Mui-selected.Mui-focused) .labelIcon': { + color: + theme.palette.mode === 'light' + ? theme.palette.primary.main + : theme.palette.primary.dark, }, - [`&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused`]: { - backgroundColor: - theme.palette.mode === 'light' - ? theme.palette.primary.main - : theme.palette.primary.dark, - color: theme.palette.primary.contrastText, + '&::before': { + content: '""', + display: 'block', + position: 'absolute', + left: '16px', + top: '44px', + height: 'calc(100% - 48px)', + width: '1.5px', + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.grey[300] + : theme.palette.grey[700], }, + }, + '&:hover': { + backgroundColor: alpha(theme.palette.primary.main, 0.1), + color: theme.palette.mode === 'light' ? theme.palette.primary.main : 'white', + }, + [`&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused`]: { + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.primary.main + : theme.palette.primary.dark, + color: theme.palette.primary.contrastText, + }, })); const StyledTreeItemLabelText = styled(Typography)({ - color: 'inherit', - fontFamily: 'General Sans', - fontWeight: 500, + color: 'inherit', + fontFamily: 'General Sans', + fontWeight: 500, }) as unknown as typeof Typography; interface CustomLabelProps { - children: React.ReactNode; - icon?: React.ElementType; - expandable?: boolean; + children: React.ReactNode; + icon?: React.ElementType; + expandable?: boolean; } function CustomLabel({ - icon: Icon, - expandable, - children, - ...other - }: CustomLabelProps) { - return ( - - {Icon && ( - - )} + icon: Icon, + expandable, + children, + ...other +}: CustomLabelProps) { + return ( + + {Icon && ( + + )} - {children} - {expandable && } - - ); + {children} + {expandable && } + + ); } const isExpandable = (reactChildren: React.ReactNode) => { - if (Array.isArray(reactChildren)) { - return reactChildren.length > 0 && reactChildren.some(isExpandable); - } - return Boolean(reactChildren); + if (Array.isArray(reactChildren)) { + return reactChildren.length > 0 && reactChildren.some(isExpandable); + } + return Boolean(reactChildren); }; const getIconFromFileType = (fileType: FileType) => { - switch (fileType) { - case 'image': - return ImageIcon; - case 'pdf': - return PictureAsPdfIcon; - case 'doc': - return ArticleIcon; - case 'video': - return VideoCameraBackIcon; - case 'folder': - return FolderRounded; - case 'trash': - return DeleteIcon; - default: - return ArticleIcon; - } + switch (fileType) { + case 'image': + return ImageIcon; + case 'pdf': + return PictureAsPdfIcon; + case 'doc': + return ArticleIcon; + case 'video': + return VideoCameraBackIcon; + case 'folder': + return FolderRounded; + case 'trash': + return DeleteIcon; + default: + return ArticleIcon; + } }; interface CustomTreeItemProps - extends Omit, - Omit, 'onFocus'> {} + extends Omit, + Omit, 'onFocus'> {} const CustomTreeItem = React.forwardRef(function CustomTreeItem( - props: CustomTreeItemProps, - ref: React.Ref, + props: CustomTreeItemProps, + ref: React.Ref, ) { - const { id, itemId, label, disabled, children, ...other } = props; + const { id, itemId, label, disabled, children, ...other } = props; - const { - getRootProps, - getContentProps, - getIconContainerProps, - getCheckboxProps, - getLabelProps, - getGroupTransitionProps, - getDragAndDropOverlayProps, - status, - publicAPI, - } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); + const { + getRootProps, + getContentProps, + getIconContainerProps, + getCheckboxProps, + getLabelProps, + getGroupTransitionProps, + getDragAndDropOverlayProps, + status, + publicAPI, + } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); - const item = publicAPI.getItem(itemId); - const expandable = isExpandable(children); - const icon = getIconFromFileType(item.fileType); + const item = publicAPI.getItem(itemId); + const expandable = isExpandable(children); + const icon = getIconFromFileType(item.fileType); - return ( - - - - - - - - - - - { children && } - - - ); + return ( + + + + + + + + + + + {children && } + + + ); }); export default function FileExplorer() { - const apiRef = useTreeViewApiRef() + const apiRef = useTreeViewApiRef(); - return ( - { - return params.newPosition.parentId === null || ['folder', 'trash'].includes(apiRef.current!.getItem(params.newPosition.parentId).fileType) - }} - /> - ); + return ( + { + return ( + params.newPosition.parentId === null || + ['folder', 'trash'].includes( + apiRef.current!.getItem(params.newPosition.parentId).fileType, + ) + ); + }} + /> + ); } diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx.preview b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx.preview deleted file mode 100644 index 6b4fb9fea0dad..0000000000000 --- a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx.preview +++ /dev/null @@ -1,13 +0,0 @@ - { - return params.newPosition.parentId === null || ['folder', 'trash'].includes(apiRef.current!.getItem(params.newPosition.parentId).fileType) - }} -/> \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/ordering/ordering.md b/docs/data/tree-view/rich-tree-view/ordering/ordering.md index c684cce16a764..c94cbbbb77709 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/ordering.md +++ b/docs/data/tree-view/rich-tree-view/ordering/ordering.md @@ -69,4 +69,4 @@ You can create a custom Tree Item component to render a drag handle icon and onl The example below is a simplified version of the [File Explorer](/x/react-tree-view/rich-tree-view/customization/#file-explorer) example with drag & drop re-ordering. You can re-order items but only inside folders (or inside the trash). -{{"demo": "FileExplorer.js"}} \ No newline at end of file +{{"demo": "FileExplorer.js"}} From 867c1a21a8c5024db0a854a982cd48f19ea2b730 Mon Sep 17 00:00:00 2001 From: delangle Date: Fri, 12 Jul 2024 12:44:41 +0200 Subject: [PATCH 10/16] Fix --- docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx index be7e622c018b5..189c1b307c034 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx @@ -278,7 +278,6 @@ export default function FileExplorer() { items={ITEMS} apiRef={apiRef} defaultExpandedItems={['1', '1.1']} - defaultSelectedItems="1.1" sx={{ height: 'fit-content', flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} slots={{ item: CustomTreeItem }} experimentalFeatures={{ indentationAtItemLevel: true }} @@ -291,6 +290,7 @@ export default function FileExplorer() { ) ); }} + onItemPositionChange={(params) => {}} /> ); } From ee76e4e68c2499f8ab6f1cf08b66a6f4e0fddeb2 Mon Sep 17 00:00:00 2001 From: delangle Date: Fri, 12 Jul 2024 13:03:20 +0200 Subject: [PATCH 11/16] Fix --- docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js | 1 - docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js index 621e0b3f995e7..b6d078901c605 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js @@ -243,7 +243,6 @@ export default function FileExplorer() { items={ITEMS} apiRef={apiRef} defaultExpandedItems={['1', '1.1']} - defaultSelectedItems="1.1" sx={{ height: 'fit-content', flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} slots={{ item: CustomTreeItem }} experimentalFeatures={{ indentationAtItemLevel: true }} diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx index 189c1b307c034..2f8fd1d61520b 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx @@ -290,7 +290,6 @@ export default function FileExplorer() { ) ); }} - onItemPositionChange={(params) => {}} /> ); } From 861c69d29ba2adf4a86905185d5e63c5dff0675e Mon Sep 17 00:00:00 2001 From: delangle Date: Thu, 18 Jul 2024 09:56:57 +0200 Subject: [PATCH 12/16] Add experimental feature --- .../rich-tree-view/ordering/DragAndDrop.js | 5 ++++- .../rich-tree-view/ordering/DragAndDrop.tsx | 5 ++++- .../ordering/DragAndDrop.tsx.preview | 5 ++++- .../rich-tree-view/ordering/FileExplorer.js | 2 +- .../rich-tree-view/ordering/FileExplorer.tsx | 2 +- .../ordering/OnItemPositionChange.js | 9 ++++++--- .../ordering/OnItemPositionChange.tsx | 9 ++++++--- .../ordering/OnlyReorderFromDragHandle.js | 5 ++++- .../ordering/OnlyReorderFromDragHandle.tsx | 5 ++++- .../OnlyReorderFromDragHandle.tsx.preview | 5 ++++- .../ordering/OnlyReorderInSameParent.js | 5 ++++- .../ordering/OnlyReorderInSameParent.tsx | 5 ++++- .../OnlyReorderInSameParent.tsx.preview | 5 ++++- .../ordering/OnlyReorderLeaves.js | 5 ++++- .../ordering/OnlyReorderLeaves.tsx | 5 ++++- .../ordering/OnlyReorderLeaves.tsx.preview | 5 ++++- .../ordering/SendAllItemsToServer.js | 5 ++++- .../ordering/SendAllItemsToServer.tsx | 5 ++++- .../rich-tree-view/ordering/ordering.md | 4 ++-- .../useTreeViewItemsReordering.test.tsx | 14 +++++++------- .../useTreeViewItemsReordering.ts | 18 ++++++++++++------ .../useTreeViewItemsReordering.types.ts | 1 + 22 files changed, 92 insertions(+), 37 deletions(-) diff --git a/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.js b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.js index 8ef54b5774a29..7832e545034ff 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.js +++ b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.js @@ -40,7 +40,10 @@ export default function DragAndDrop() { items={ITEMS} itemsReordering defaultExpandedItems={['grid', 'pickers']} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ + indentationAtItemLevel: true, + itemsReordering: true, + }} /> ); diff --git a/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx index 8e4801fd27788..1c59beb40bc9b 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx @@ -40,7 +40,10 @@ export default function DragAndDrop() { items={ITEMS} itemsReordering defaultExpandedItems={['grid', 'pickers']} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ + indentationAtItemLevel: true, + itemsReordering: true, + }} /> ); diff --git a/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx.preview b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx.preview index b1fe1bcaede71..b29cac8f227a9 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx.preview +++ b/docs/data/tree-view/rich-tree-view/ordering/DragAndDrop.tsx.preview @@ -2,5 +2,8 @@ items={ITEMS} itemsReordering defaultExpandedItems={['grid', 'pickers']} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ + indentationAtItemLevel: true, + itemsReordering: true, + }} /> \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js index b6d078901c605..2e27635af25fc 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js @@ -245,7 +245,7 @@ export default function FileExplorer() { defaultExpandedItems={['1', '1.1']} sx={{ height: 'fit-content', flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} slots={{ item: CustomTreeItem }} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ indentationAtItemLevel: true, itemsReordering: true }} itemsReordering canMoveItemToNewPosition={(params) => { return ( diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx index 2f8fd1d61520b..77d9336531421 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx @@ -280,7 +280,7 @@ export default function FileExplorer() { defaultExpandedItems={['1', '1.1']} sx={{ height: 'fit-content', flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} slots={{ item: CustomTreeItem }} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ indentationAtItemLevel: true, itemsReordering: true }} itemsReordering canMoveItemToNewPosition={(params) => { return ( diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.js b/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.js index 9b37121a4a2c5..c9cfef10d6a0e 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.js +++ b/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.js @@ -44,7 +44,10 @@ export default function OnItemPositionChange() { setLastReorder(params)} /> @@ -56,8 +59,8 @@ export default function OnItemPositionChange() { Last reordered item: {lastReorder.itemId}
Position before: {lastReorder.oldPosition.parentId ?? 'root'} (index{' '} - {lastReorder.oldPosition.index})
- Position after: {lastReorder.newPosition.parentId ?? 'root'} (index{' '} + {lastReorder.oldPosition.index})
F Position after:{' '} + {lastReorder.newPosition.parentId ?? 'root'} (index{' '} {lastReorder.newPosition.index}) )} diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.tsx b/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.tsx index 27977b8564215..52f5ff5474bc4 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/OnItemPositionChange.tsx @@ -52,7 +52,10 @@ export default function OnItemPositionChange() { setLastReorder(params)} /> @@ -64,8 +67,8 @@ export default function OnItemPositionChange() { Last reordered item: {lastReorder.itemId}
Position before: {lastReorder.oldPosition.parentId ?? 'root'} (index{' '} - {lastReorder.oldPosition.index})
- Position after: {lastReorder.newPosition.parentId ?? 'root'} (index{' '} + {lastReorder.oldPosition.index})
F Position after:{' '} + {lastReorder.newPosition.parentId ?? 'root'} (index{' '} {lastReorder.newPosition.index}) )} diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js index 5fa432ee96f49..8aa17d6dd5a2b 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js @@ -103,7 +103,10 @@ export default function OnlyReorderFromDragHandle() { diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx index 01fcff4f73ecf..d01d81f8f4c37 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx @@ -117,7 +117,10 @@ export default function OnlyReorderFromDragHandle() { diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx.preview b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx.preview index 12753c6f59fe6..632176dce3e32 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx.preview +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx.preview @@ -1,7 +1,10 @@ \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.js b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.js index e971e8d858560..c105172ea3e94 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.js +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.js @@ -40,7 +40,10 @@ export default function OnlyReorderInSameParent() { items={MUI_X_PRODUCTS} itemsReordering defaultExpandedItems={['grid', 'pickers']} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ + indentationAtItemLevel: true, + itemsReordering: true, + }} canMoveItemToNewPosition={(params) => params.oldPosition.parentId === params.newPosition.parentId } diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx index ab82c5561b9bd..c69e56fdfde47 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx @@ -40,7 +40,10 @@ export default function OnlyReorderInSameParent() { items={MUI_X_PRODUCTS} itemsReordering defaultExpandedItems={['grid', 'pickers']} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ + indentationAtItemLevel: true, + itemsReordering: true, + }} canMoveItemToNewPosition={(params) => params.oldPosition.parentId === params.newPosition.parentId } diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx.preview b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx.preview index cfde0e44e822f..726c958558b19 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx.preview +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderInSameParent.tsx.preview @@ -2,7 +2,10 @@ items={MUI_X_PRODUCTS} itemsReordering defaultExpandedItems={['grid', 'pickers']} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ + indentationAtItemLevel: true, + itemsReordering: true, + }} canMoveItemToNewPosition={(params) => params.oldPosition.parentId === params.newPosition.parentId } diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.js b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.js index 4d2dba8d20e69..ac3e351da44be 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.js +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.js @@ -43,7 +43,10 @@ export default function OnlyReorderLeaves() { items={MUI_X_PRODUCTS} itemsReordering defaultExpandedItems={['grid', 'pickers']} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ + indentationAtItemLevel: true, + itemsReordering: true, + }} apiRef={apiRef} isItemReorderable={(itemId) => apiRef.current.getItemOrderedChildrenIds(itemId).length === 0 diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx index 7fd556d782253..551b2a805ef48 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx @@ -43,7 +43,10 @@ export default function OnlyReorderLeaves() { items={MUI_X_PRODUCTS} itemsReordering defaultExpandedItems={['grid', 'pickers']} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ + indentationAtItemLevel: true, + itemsReordering: true, + }} apiRef={apiRef} isItemReorderable={(itemId) => apiRef.current!.getItemOrderedChildrenIds(itemId).length === 0 diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx.preview b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx.preview index a3271fa077200..8057c22fe961b 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx.preview +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx.preview @@ -2,7 +2,10 @@ items={MUI_X_PRODUCTS} itemsReordering defaultExpandedItems={['grid', 'pickers']} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ + indentationAtItemLevel: true, + itemsReordering: true, + }} apiRef={apiRef} isItemReorderable={(itemId) => apiRef.current!.getItemOrderedChildrenIds(itemId).length === 0 diff --git a/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.js b/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.js index 5c596f1eeaedf..1481b77bb83d1 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.js +++ b/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.js @@ -69,7 +69,10 @@ export default function SendAllItemsToServer() { items={MUI_X_PRODUCTS} itemsReordering defaultExpandedItems={['grid', 'pickers']} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ + indentationAtItemLevel: true, + itemsReordering: true, + }} onItemPositionChange={handleItemPositionChangeTreeViewA} />
diff --git a/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.tsx b/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.tsx index 78152018396a7..6f39404daf702 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.tsx @@ -69,7 +69,10 @@ export default function SendAllItemsToServer() { items={MUI_X_PRODUCTS} itemsReordering defaultExpandedItems={['grid', 'pickers']} - experimentalFeatures={{ indentationAtItemLevel: true }} + experimentalFeatures={{ + indentationAtItemLevel: true, + itemsReordering: true, + }} onItemPositionChange={handleItemPositionChangeTreeViewA} />
diff --git a/docs/data/tree-view/rich-tree-view/ordering/ordering.md b/docs/data/tree-view/rich-tree-view/ordering/ordering.md index c94cbbbb77709..bdd78d0ff9ada 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/ordering.md +++ b/docs/data/tree-view/rich-tree-view/ordering/ordering.md @@ -12,12 +12,12 @@ waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/

Drag and drop your items to reorder them.

:::success -To be able to reorder items, you first have to enable the `indentationAtItemLevel` experimental feature: +To be able to reorder items, you first have to enable the `indentationAtItemLevel` and the `itemsReordering` experimental features: ```tsx ``` diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx index 5462bb5119cec..1f4fbfe29afc6 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx @@ -74,7 +74,7 @@ describeTreeView<[UseTreeViewItemsReorderingSignature, UseTreeViewItemsSignature describe('itemReordering prop', () => { it('should allow to drag and drop items when props.itemsReordering={true}', () => { const response = render({ - experimentalFeatures: { indentationAtItemLevel: true }, + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, items: [{ id: '1' }, { id: '2' }, { id: '3' }], itemsReordering: true, }); @@ -88,7 +88,7 @@ describeTreeView<[UseTreeViewItemsReorderingSignature, UseTreeViewItemsSignature it('should not allow to drag and drop items when props.itemsReordering={false}', () => { const response = render({ - experimentalFeatures: { indentationAtItemLevel: true }, + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, items: [{ id: '1' }, { id: '2' }, { id: '3' }], itemsReordering: false, }); @@ -112,7 +112,7 @@ describeTreeView<[UseTreeViewItemsReorderingSignature, UseTreeViewItemsSignature it('should call onItemPositionChange when an item is moved', () => { const onItemPositionChange = spy(); const response = render({ - experimentalFeatures: { indentationAtItemLevel: true }, + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, items: [{ id: '1' }, { id: '2' }, { id: '3' }], itemsReordering: true, onItemPositionChange, @@ -131,7 +131,7 @@ describeTreeView<[UseTreeViewItemsReorderingSignature, UseTreeViewItemsSignature describe('isItemReorderable prop', () => { it('should not allow to drag an item when isItemReorderable returns false', () => { const response = render({ - experimentalFeatures: { indentationAtItemLevel: true }, + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, items: [{ id: '1' }, { id: '2' }, { id: '3' }], itemsReordering: true, canMoveItemToNewPosition: () => false, @@ -143,7 +143,7 @@ describeTreeView<[UseTreeViewItemsReorderingSignature, UseTreeViewItemsSignature it('should allow to drag an item when isItemReorderable returns true', () => { const response = render({ - experimentalFeatures: { indentationAtItemLevel: true }, + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, items: [{ id: '1' }, { id: '2' }, { id: '3' }], itemsReordering: true, canMoveItemToNewPosition: () => true, @@ -160,7 +160,7 @@ describeTreeView<[UseTreeViewItemsReorderingSignature, UseTreeViewItemsSignature describe('canMoveItemToNewPosition prop', () => { it('should not allow to drop an item when canMoveItemToNewPosition returns false', () => { const response = render({ - experimentalFeatures: { indentationAtItemLevel: true }, + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, items: [{ id: '1' }, { id: '2' }, { id: '3' }], itemsReordering: true, canMoveItemToNewPosition: () => false, @@ -172,7 +172,7 @@ describeTreeView<[UseTreeViewItemsReorderingSignature, UseTreeViewItemsSignature it('should allow to drop an item when canMoveItemToNewPosition returns true', () => { const response = render({ - experimentalFeatures: { indentationAtItemLevel: true }, + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, items: [{ id: '1' }, { id: '2' }, { id: '3' }], itemsReordering: true, canMoveItemToNewPosition: () => true, diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts index e378fca5c73d2..e2faee4960bcd 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts @@ -15,8 +15,8 @@ import { import { useTreeViewItemsReorderingItemPlugin } from './useTreeViewItemsReordering.itemPlugin'; const wrongIndentationStrategyWarning = buildWarning([ - 'MUI X: The drag and drop feature requires the `indentationAtItemLevel` experimental feature to be enabled.', - 'You can do it by passing `experimentalFeatures={{ indentationAtItemLevel: true }}` to the `RichTreeViewPro` component.', + 'MUI X: The items reordering feature requires the `indentationAtItemLevel` and `itemsReordering` experimental features to be enabled.', + 'You can do it by passing `experimentalFeatures={{ indentationAtItemLevel: true, itemsReordering: true }}` to the `RichTreeViewPro` component.', 'Check the documentation for more details: https://mui.com/x/react-tree-view/rich-tree-view/items/', ]); @@ -27,15 +27,21 @@ export const useTreeViewItemsReordering: TreeViewPlugin { + const isItemsReorderingEnabled = + params.itemsReordering && !!experimentalFeatures?.itemsReordering; + if (process.env.NODE_END !== 'production') { - if (params.itemsReordering && !experimentalFeatures?.indentationAtItemLevel) { + if ( + params.itemsReordering && + (!experimentalFeatures?.indentationAtItemLevel || !experimentalFeatures?.itemsReordering) + ) { wrongIndentationStrategyWarning(); } } const canItemBeDragged = React.useCallback( (itemId: string) => { - if (!params.itemsReordering) { + if (!isItemsReorderingEnabled) { return false; } @@ -46,7 +52,7 @@ export const useTreeViewItemsReordering: TreeViewPlugin; From 1e0c96939c101f5950ed8252838e29edb19e1a91 Mon Sep 17 00:00:00 2001 From: delangle Date: Mon, 22 Jul 2024 09:21:59 +0200 Subject: [PATCH 13/16] Work --- .../useTreeViewItemsReordering.ts | 3 ++- .../useTreeViewItemsReordering.types.ts | 2 ++ .../useTreeViewItemsReordering.utils.ts | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts index e2faee4960bcd..116e7eb6f7612 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts @@ -163,12 +163,12 @@ export const useTreeViewItemsReordering: TreeViewPlugin ({ ...prevState, itemsReordering: null })); if ( state.itemsReordering.draggedItemId === state.itemsReordering.targetItemId || state.itemsReordering.action == null || state.itemsReordering.newPosition == null ) { + setState((prevState) => ({ ...prevState, itemsReordering: null })); return; } @@ -183,6 +183,7 @@ export const useTreeViewItemsReordering: TreeViewPlugin ({ ...prevState, + itemsReordering: null, items: moveItemInTree({ itemToMoveId: itemId, newPosition, diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts index b7635f4955e3f..2f2e909cc31e5 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts @@ -64,6 +64,8 @@ export type TreeViewItemItemReorderingValidActions = { export interface UseTreeViewItemsReorderingParameters { /** * If `true`, the reordering of items is enabled. + * Make sure to also enable the `itemsReordering` experimental feature: + * ``. * @default false */ itemsReordering?: boolean; diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts index 7330da2869a3d..a20f5a73e4568 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts @@ -31,6 +31,11 @@ export const isAncestor = ( return isAncestor(instance, itemMetaA.parentId, itemIdB); }; +/** + * Transforms a CSS string `itemChildrenIndentation` into a number representing the indentation in number. + * @param {string | null} itemChildrenIndentation The indentation as passed to the `itemChildrenIndentation` prop. + * @param {HTMLElement} contentElement The DOM element to which the indentation will be applied. + */ const parseItemChildrenIndentation = ( itemChildrenIndentation: string | number, contentElement: HTMLElement, @@ -44,6 +49,7 @@ const parseItemChildrenIndentation = ( return parseFloat(pixelExec[1]); } + // If the format is neither `px` nor a number, we need to measure the indentation using an actual DOM element. const tempElement = document.createElement('div'); tempElement.style.width = itemChildrenIndentation; tempElement.style.position = 'absolute'; From d0d147ffa1f6aefea5b1a137f7012a4d38132145 Mon Sep 17 00:00:00 2001 From: delangle Date: Mon, 22 Jul 2024 11:48:10 +0200 Subject: [PATCH 14/16] Review: Nora --- .../useTreeViewItemsReordering.test.tsx | 245 ++++++++++-------- .../useTreeViewItemsReordering.utils.ts | 11 + 2 files changed, 144 insertions(+), 112 deletions(-) diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx index 1f4fbfe29afc6..b316969dbc1fb 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx @@ -4,7 +4,10 @@ import { spy } from 'sinon'; import { fireEvent, createEvent } from '@mui/internal-test-utils'; import { UseTreeViewItemsReorderingSignature } from '@mui/x-tree-view-pro/internals'; import { DragEventTypes, MockedDataTransfer } from 'test/utils/dragAndDrop'; -import { UseTreeViewItemsSignature } from '@mui/x-tree-view/internals'; +import { + UseTreeViewExpansionSignature, + UseTreeViewItemsSignature, +} from '@mui/x-tree-view/internals'; import { chooseActionToApply } from './useTreeViewItemsReordering.utils'; import { TreeViewItemItemReorderingValidActions } from './useTreeViewItemsReordering.types'; @@ -52,141 +55,159 @@ const buildTreeViewDragInteractions = (dataTransfer: DataTransfer) => { }; }; -describeTreeView<[UseTreeViewItemsReorderingSignature, UseTreeViewItemsSignature]>( - 'useTreeViewItemsReordering', - ({ render, treeViewComponentName }) => { - if (treeViewComponentName === 'SimpleTreeView' || treeViewComponentName === 'RichTreeView') { - return; - } - - let dragEvents: ReturnType; - // eslint-disable-next-line mocha/no-top-level-hooks - beforeEach(() => { - const dataTransfer = new MockedDataTransfer(); - dragEvents = buildTreeViewDragInteractions(dataTransfer); - }); +describeTreeView< + [UseTreeViewItemsReorderingSignature, UseTreeViewItemsSignature, UseTreeViewExpansionSignature] +>('useTreeViewItemsReordering', ({ render, treeViewComponentName }) => { + if (treeViewComponentName === 'SimpleTreeView' || treeViewComponentName === 'RichTreeView') { + return; + } + + let dragEvents: ReturnType; + // eslint-disable-next-line mocha/no-top-level-hooks + beforeEach(() => { + const dataTransfer = new MockedDataTransfer(); + dragEvents = buildTreeViewDragInteractions(dataTransfer); + }); + + // eslint-disable-next-line mocha/no-top-level-hooks + afterEach(() => { + dragEvents = {} as typeof dragEvents; + }); + + describe('itemReordering prop', () => { + it('should allow to drag and drop items when props.itemsReordering={true}', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + }); - // eslint-disable-next-line mocha/no-top-level-hooks - afterEach(() => { - dragEvents = {} as typeof dragEvents; + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([ + { id: '2', children: [{ id: '1' }] }, + { id: '3' }, + ]); }); - describe('itemReordering prop', () => { - it('should allow to drag and drop items when props.itemsReordering={true}', () => { - const response = render({ - experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, - items: [{ id: '1' }, { id: '2' }, { id: '3' }], - itemsReordering: true, - }); - - dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); - expect(response.getItemIdTree()).to.deep.equal([ - { id: '2', children: [{ id: '1' }] }, - { id: '3' }, - ]); + it('should not allow to drag and drop items when props.itemsReordering={false}', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: false, }); - it('should not allow to drag and drop items when props.itemsReordering={false}', () => { - const response = render({ - experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, - items: [{ id: '1' }, { id: '2' }, { id: '3' }], - itemsReordering: false, - }); + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + }); - dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); - expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + it('should not allow to drag and drop items when props.itemsReordering is not defined', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], }); - it('should not allow to drag and drop items when props.itemsReordering is not defined', () => { - const response = render({ - experimentalFeatures: { indentationAtItemLevel: true }, - items: [{ id: '1' }, { id: '2' }, { id: '3' }], - }); + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + }); - dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); - expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + it('should allow to expand the new parent of the dragged item when it was not expandable before', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, + items: [{ id: '1', children: [{ id: '1.1' }] }, { id: '2' }], + itemsReordering: true, + defaultExpandedItems: ['1'], }); + + dragEvents.fullDragSequence(response.getItemRoot('1.1'), response.getItemContent('2')); + + fireEvent.focus(response.getItemRoot('2')); + fireEvent.keyDown(response.getItemRoot('2'), { key: 'Enter' }); + + expect(response.getItemIdTree()).to.deep.equal([ + { id: '1', children: [] }, + { id: '2', children: [{ id: '1.1' }] }, + ]); }); + }); - describe('onItemPositionChange prop', () => { - it('should call onItemPositionChange when an item is moved', () => { - const onItemPositionChange = spy(); - const response = render({ - experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, - items: [{ id: '1' }, { id: '2' }, { id: '3' }], - itemsReordering: true, - onItemPositionChange, - }); - - dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); - expect(onItemPositionChange.callCount).to.equal(1); - expect(onItemPositionChange.lastCall.firstArg).to.deep.equal({ - itemId: '1', - oldPosition: { parentId: null, index: 0 }, - newPosition: { parentId: '2', index: 0 }, - }); + describe('onItemPositionChange prop', () => { + it('should call onItemPositionChange when an item is moved', () => { + const onItemPositionChange = spy(); + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + onItemPositionChange, + }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(onItemPositionChange.callCount).to.equal(1); + expect(onItemPositionChange.lastCall.firstArg).to.deep.equal({ + itemId: '1', + oldPosition: { parentId: null, index: 0 }, + newPosition: { parentId: '2', index: 0 }, }); }); + }); - describe('isItemReorderable prop', () => { - it('should not allow to drag an item when isItemReorderable returns false', () => { - const response = render({ - experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, - items: [{ id: '1' }, { id: '2' }, { id: '3' }], - itemsReordering: true, - canMoveItemToNewPosition: () => false, - }); - - dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); - expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + describe('isItemReorderable prop', () => { + it('should not allow to drag an item when isItemReorderable returns false', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + canMoveItemToNewPosition: () => false, }); - it('should allow to drag an item when isItemReorderable returns true', () => { - const response = render({ - experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, - items: [{ id: '1' }, { id: '2' }, { id: '3' }], - itemsReordering: true, - canMoveItemToNewPosition: () => true, - }); - - dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); - expect(response.getItemIdTree()).to.deep.equal([ - { id: '2', children: [{ id: '1' }] }, - { id: '3' }, - ]); + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + }); + + it('should allow to drag an item when isItemReorderable returns true', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + canMoveItemToNewPosition: () => true, }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([ + { id: '2', children: [{ id: '1' }] }, + { id: '3' }, + ]); }); + }); - describe('canMoveItemToNewPosition prop', () => { - it('should not allow to drop an item when canMoveItemToNewPosition returns false', () => { - const response = render({ - experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, - items: [{ id: '1' }, { id: '2' }, { id: '3' }], - itemsReordering: true, - canMoveItemToNewPosition: () => false, - }); - - dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); - expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + describe('canMoveItemToNewPosition prop', () => { + it('should not allow to drop an item when canMoveItemToNewPosition returns false', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + canMoveItemToNewPosition: () => false, }); - it('should allow to drop an item when canMoveItemToNewPosition returns true', () => { - const response = render({ - experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, - items: [{ id: '1' }, { id: '2' }, { id: '3' }], - itemsReordering: true, - canMoveItemToNewPosition: () => true, - }); - - dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); - expect(response.getItemIdTree()).to.deep.equal([ - { id: '2', children: [{ id: '1' }] }, - { id: '3' }, - ]); + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([{ id: '1' }, { id: '2' }, { id: '3' }]); + }); + + it('should allow to drop an item when canMoveItemToNewPosition returns true', () => { + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + canMoveItemToNewPosition: () => true, }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(response.getItemIdTree()).to.deep.equal([ + { id: '2', children: [{ id: '1' }] }, + { id: '3' }, + ]); }); - }, -); + }); +}); describe('getNewPosition util', () => { // The actions use the following tree when dropping "1.1" on "1.2": diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts index a20f5a73e4568..bfd7f19a3ba86 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts @@ -163,6 +163,16 @@ export const moveItemInTree = ({ // 3. Update the `itemMetaMap` const itemMetaMap = { ...prevState.itemMetaMap }; + + // 3.1 Update the `expandable` property of the old and the new parent + if (oldParentId !== TREE_VIEW_ROOT_PARENT_ID && oldParentId !== newParentId) { + itemMetaMap[oldParentId].expandable = itemOrderedChildrenIds[oldParentId].length > 0; + } + if (newParentId !== TREE_VIEW_ROOT_PARENT_ID && newParentId !== oldParentId) { + itemMetaMap[newParentId].expandable = itemOrderedChildrenIds[newParentId].length > 0; + } + + // 3.2 Update the `parentId` and `depth` properties of the item to move // The depth is always defined because drag&drop is only usable with Rich Tree View components. const itemToMoveDepth = newPosition.parentId == null ? 0 : itemMetaMap[newParentId].depth! + 1; itemMetaMap[itemToMoveId] = { @@ -171,6 +181,7 @@ export const moveItemInTree = ({ depth: itemToMoveDepth, }; + // 3.3 Update the depth of all the children of the item to move const updateItemDepth = (itemId: string, depth: number) => { itemMetaMap[itemId] = { ...itemMetaMap[itemId], depth }; itemOrderedChildrenIds[itemId]?.forEach((childId) => updateItemDepth(childId, depth + 1)); From 4fb31f3264df9f143b9963f60d17cd9b1ee1c920 Mon Sep 17 00:00:00 2001 From: delangle Date: Tue, 23 Jul 2024 16:21:36 +0200 Subject: [PATCH 15/16] Fix --- .../useTreeViewItemsReordering.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts index 116e7eb6f7612..e9065681e8761 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { buildWarning, TreeViewPlugin } from '@mui/x-tree-view/internals'; +import { warnOnce, TreeViewPlugin } from '@mui/x-tree-view/internals'; import { TreeViewItemsReorderingAction } from '@mui/x-tree-view/models'; import { TreeViewItemItemReorderingValidActions, @@ -14,12 +14,6 @@ import { } from './useTreeViewItemsReordering.utils'; import { useTreeViewItemsReorderingItemPlugin } from './useTreeViewItemsReordering.itemPlugin'; -const wrongIndentationStrategyWarning = buildWarning([ - 'MUI X: The items reordering feature requires the `indentationAtItemLevel` and `itemsReordering` experimental features to be enabled.', - 'You can do it by passing `experimentalFeatures={{ indentationAtItemLevel: true, itemsReordering: true }}` to the `RichTreeViewPro` component.', - 'Check the documentation for more details: https://mui.com/x/react-tree-view/rich-tree-view/items/', -]); - export const useTreeViewItemsReordering: TreeViewPlugin = ({ params, instance, @@ -35,7 +29,11 @@ export const useTreeViewItemsReordering: TreeViewPlugin Date: Fri, 26 Jul 2024 16:06:03 +0200 Subject: [PATCH 16/16] Empty