diff --git a/docs/data/tree-view/rich-tree-view/editing/ApiMethodSetEditedItem.js b/docs/data/tree-view/rich-tree-view/editing/ApiMethodSetEditedItem.js index 9a25acfc4374b..dd142f4cae50c 100644 --- a/docs/data/tree-view/rich-tree-view/editing/ApiMethodSetEditedItem.js +++ b/docs/data/tree-view/rich-tree-view/editing/ApiMethodSetEditedItem.js @@ -3,13 +3,13 @@ import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodSetEditedItem() { const [items, setItems] = React.useState([ { id: '1', label: 'Jane Doe', editable: true }, ]); - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleAddFolder = () => { const newId = String(items.length + 1); diff --git a/docs/data/tree-view/rich-tree-view/editing/ApiMethodSetEditedItem.tsx b/docs/data/tree-view/rich-tree-view/editing/ApiMethodSetEditedItem.tsx index 2d198047ef5e0..20e6c394265a1 100644 --- a/docs/data/tree-view/rich-tree-view/editing/ApiMethodSetEditedItem.tsx +++ b/docs/data/tree-view/rich-tree-view/editing/ApiMethodSetEditedItem.tsx @@ -3,13 +3,13 @@ import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodSetEditedItem() { const [items, setItems] = React.useState([ { id: '1', label: 'Jane Doe', editable: true }, ]); - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleAddFolder = () => { const newId = String(items.length + 1); diff --git a/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.js b/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.js index 6d8aff8043d64..6693922164014 100644 --- a/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.js +++ b/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.js @@ -3,12 +3,12 @@ import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; import { MUI_X_PRODUCTS } from './products'; export default function ApiMethodUpdateItemLabel() { const [isLabelUpdated, setIsLabelUpdated] = React.useState(false); - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleUpdateLabel = () => { if (isLabelUpdated) { diff --git a/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.tsx b/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.tsx index 69a4fb59308fa..6d90fbb32b95d 100644 --- a/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.tsx +++ b/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.tsx @@ -3,12 +3,12 @@ import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; import { MUI_X_PRODUCTS } from './products'; export default function ApiMethodUpdateItemLabel() { const [isLabelUpdated, setIsLabelUpdated] = React.useState(false); - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleUpdateLabel = () => { if (isLabelUpdated) { diff --git a/docs/data/tree-view/rich-tree-view/editing/EditLeaves.js b/docs/data/tree-view/rich-tree-view/editing/EditLeaves.js index 857f7d04249bc..f8a3e744cfb00 100644 --- a/docs/data/tree-view/rich-tree-view/editing/EditLeaves.js +++ b/docs/data/tree-view/rich-tree-view/editing/EditLeaves.js @@ -1,6 +1,6 @@ import Box from '@mui/material/Box'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS = [ { @@ -36,7 +36,7 @@ const MUI_X_PRODUCTS = [ ]; export default function EditLeaves() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); return ( [] = [ ]; export default function EditLeaves() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); return ( ; +return ; + +// Pro package +const apiRef = useRichTreeViewProApiRef(); + +return ; ``` -When your component first renders, `apiRef` will be `undefined`. +When your component first renders, `apiRef.current` will be `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: diff --git a/docs/data/tree-view/rich-tree-view/expansion/ApiMethodIsItemExpanded.js b/docs/data/tree-view/rich-tree-view/expansion/ApiMethodIsItemExpanded.js index f447ecb2f9d5d..4621fa16a519c 100644 --- a/docs/data/tree-view/rich-tree-view/expansion/ApiMethodIsItemExpanded.js +++ b/docs/data/tree-view/rich-tree-view/expansion/ApiMethodIsItemExpanded.js @@ -5,7 +5,7 @@ import Button from '@mui/material/Button'; import Snackbar from '@mui/material/Snackbar'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS = [ { @@ -38,7 +38,7 @@ const MUI_X_PRODUCTS = [ ]; export default function ApiMethodIsItemExpanded() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const [isGridExpanded, setIsGridExpanded] = React.useState(false); const [isSnackbarOpen, setIsSnackbarOpen] = React.useState(false); diff --git a/docs/data/tree-view/rich-tree-view/expansion/ApiMethodIsItemExpanded.tsx b/docs/data/tree-view/rich-tree-view/expansion/ApiMethodIsItemExpanded.tsx index 6d37b33414bb0..6977e2670838d 100644 --- a/docs/data/tree-view/rich-tree-view/expansion/ApiMethodIsItemExpanded.tsx +++ b/docs/data/tree-view/rich-tree-view/expansion/ApiMethodIsItemExpanded.tsx @@ -5,7 +5,7 @@ import Button from '@mui/material/Button'; import Snackbar from '@mui/material/Snackbar'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; import { TreeViewBaseItem } from '@mui/x-tree-view/models'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ { @@ -38,7 +38,7 @@ const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ ]; export default function ApiMethodIsItemExpanded() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const [isGridExpanded, setIsGridExpanded] = React.useState(false); const [isSnackbarOpen, setIsSnackbarOpen] = React.useState(false); diff --git a/docs/data/tree-view/rich-tree-view/expansion/ApiMethodSetItemExpansion.js b/docs/data/tree-view/rich-tree-view/expansion/ApiMethodSetItemExpansion.js index d8d5028280bf9..f6375dd6cd400 100644 --- a/docs/data/tree-view/rich-tree-view/expansion/ApiMethodSetItemExpansion.js +++ b/docs/data/tree-view/rich-tree-view/expansion/ApiMethodSetItemExpansion.js @@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS = [ ]; export default function ApiMethodSetItemExpansion() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleExpandClick = (event) => { apiRef.current.setItemExpansion({ diff --git a/docs/data/tree-view/rich-tree-view/expansion/ApiMethodSetItemExpansion.tsx b/docs/data/tree-view/rich-tree-view/expansion/ApiMethodSetItemExpansion.tsx index 5b0a40a19e601..fce8c49fed0dc 100644 --- a/docs/data/tree-view/rich-tree-view/expansion/ApiMethodSetItemExpansion.tsx +++ b/docs/data/tree-view/rich-tree-view/expansion/ApiMethodSetItemExpansion.tsx @@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; import { TreeViewBaseItem } from '@mui/x-tree-view/models'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ ]; export default function ApiMethodSetItemExpansion() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleExpandClick = (event: React.MouseEvent) => { apiRef.current!.setItemExpansion({ diff --git a/docs/data/tree-view/rich-tree-view/expansion/expansion.md b/docs/data/tree-view/rich-tree-view/expansion/expansion.md index 0fd81dbc8f425..25979caf93e06 100644 --- a/docs/data/tree-view/rich-tree-view/expansion/expansion.md +++ b/docs/data/tree-view/rich-tree-view/expansion/expansion.md @@ -41,15 +41,21 @@ You can use the `expansionTrigger` prop to decide if the expansion interaction s ## Imperative API :::success -To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows: +To use the `apiRef` object, you need to initialize it using the `useRichTreeViewApiRef` or `useRichTreeViewProApiRef` hook as follows: ```tsx -const apiRef = useTreeViewApiRef(); +// Community package +const apiRef = useRichTreeViewApiRef(); -return ; +return ; + +// Pro package +const apiRef = useRichTreeViewProApiRef(); + +return ; ``` -When your component first renders, `apiRef` will be `undefined`. +When your component first renders, `apiRef.current` will be `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: diff --git a/docs/data/tree-view/rich-tree-view/focus/ApiMethodFocusItem.js b/docs/data/tree-view/rich-tree-view/focus/ApiMethodFocusItem.js index e462fb6a565cc..7838bf0fc3118 100644 --- a/docs/data/tree-view/rich-tree-view/focus/ApiMethodFocusItem.js +++ b/docs/data/tree-view/rich-tree-view/focus/ApiMethodFocusItem.js @@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS = [ ]; export default function ApiMethodFocusItem() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleButtonClick = (event) => { apiRef.current?.focusItem(event, 'pickers'); }; diff --git a/docs/data/tree-view/rich-tree-view/focus/ApiMethodFocusItem.tsx b/docs/data/tree-view/rich-tree-view/focus/ApiMethodFocusItem.tsx index 2e95d0930817e..9c59d6efb8813 100644 --- a/docs/data/tree-view/rich-tree-view/focus/ApiMethodFocusItem.tsx +++ b/docs/data/tree-view/rich-tree-view/focus/ApiMethodFocusItem.tsx @@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; import { TreeViewBaseItem } from '@mui/x-tree-view/models'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ ]; export default function ApiMethodFocusItem() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleButtonClick = (event: React.SyntheticEvent) => { apiRef.current?.focusItem(event, 'pickers'); }; diff --git a/docs/data/tree-view/rich-tree-view/focus/focus.md b/docs/data/tree-view/rich-tree-view/focus/focus.md index f5f14ea32eb94..288d5f1518c73 100644 --- a/docs/data/tree-view/rich-tree-view/focus/focus.md +++ b/docs/data/tree-view/rich-tree-view/focus/focus.md @@ -13,15 +13,21 @@ waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ ## Imperative API :::success -To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows: +To use the `apiRef` object, you need to initialize it using the `useRichTreeViewApiRef` or `useRichTreeViewProApiRef` hook as follows: ```tsx -const apiRef = useTreeViewApiRef(); +// Community package +const apiRef = useRichTreeViewApiRef(); -return ; +return ; + +// Pro package +const apiRef = useRichTreeViewProApiRef(); + +return ; ``` -When your component first renders, `apiRef` will be `undefined`. +When your component first renders, `apiRef.current` will be `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItem.js b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItem.js index 088f827593f06..1301bfce35939 100644 --- a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItem.js +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItem.js @@ -4,7 +4,7 @@ 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'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS = [ ]; export default function ApiMethodGetItem() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const [selectedItem, setSelectedItem] = React.useState(null); const handleSelectedItemsChange = (event, itemId) => { diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItem.tsx b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItem.tsx index 6f41d83598034..c272a3b7e94f6 100644 --- a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItem.tsx +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItem.tsx @@ -4,7 +4,7 @@ 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'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ ]; export default function ApiMethodGetItem() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const [selectedItem, setSelectedItem] = React.useState( null, ); diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemDOMElement.js b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemDOMElement.js index 43534ca2d6080..07e76a19c8f53 100644 --- a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemDOMElement.js +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemDOMElement.js @@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS = [ ]; export default function ApiMethodGetItemDOMElement() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleScrollToChartsCommunity = (event) => { apiRef.current.focusItem(event, 'charts-community'); apiRef.current diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemDOMElement.tsx b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemDOMElement.tsx index be0df0843498c..1953faccb54fb 100644 --- a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemDOMElement.tsx +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemDOMElement.tsx @@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; import { TreeViewBaseItem } from '@mui/x-tree-view/models'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ ]; export default function ApiMethodGetItemDOMElement() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleScrollToChartsCommunity = (event: React.SyntheticEvent) => { apiRef.current!.focusItem(event, 'charts-community'); apiRef diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.js b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.js index f42a0bee4ab08..a972da74388f0 100644 --- a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.js +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.js @@ -4,7 +4,7 @@ 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'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS = [ ]; export default function ApiMethodGetItemOrderedChildrenIds() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const [isSelectedItemLeaf, setIsSelectedItemLeaf] = React.useState(null); const handleSelectedItemsChange = (event, itemId) => { diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx index 28f6ef33bcdfc..8ebdedf79083c 100644 --- a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemOrderedChildrenIds.tsx @@ -4,7 +4,7 @@ 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'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ ]; export default function ApiMethodGetItemOrderedChildrenIds() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const [isSelectedItemLeaf, setIsSelectedItemLeaf] = React.useState( null, ); diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.js b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.js index b30d1fab59217..98ce47a614bf2 100644 --- a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.js +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.js @@ -5,7 +5,7 @@ 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'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS = [ { @@ -38,7 +38,7 @@ const MUI_X_PRODUCTS = [ ]; export default function ApiMethodGetItemTree() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const [items, setItems] = React.useState(MUI_X_PRODUCTS); const [itemOnTop, setItemOnTop] = React.useState(items[0].label); diff --git a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx index 33c261e024514..a0a1ff0bbdb87 100644 --- a/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx +++ b/docs/data/tree-view/rich-tree-view/items/ApiMethodGetItemTree.tsx @@ -5,7 +5,7 @@ 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'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ { @@ -38,7 +38,7 @@ const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ ]; export default function ApiMethodGetItemTree() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const [items, setItems] = React.useState(MUI_X_PRODUCTS); const [itemOnTop, setItemOnTop] = React.useState(items[0].label); diff --git a/docs/data/tree-view/rich-tree-view/items/GetParentIdPublicAPI.js b/docs/data/tree-view/rich-tree-view/items/GetParentIdPublicAPI.js index 24dce72f8392a..1a75ca186e365 100644 --- a/docs/data/tree-view/rich-tree-view/items/GetParentIdPublicAPI.js +++ b/docs/data/tree-view/rich-tree-view/items/GetParentIdPublicAPI.js @@ -3,11 +3,11 @@ import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; import { MUI_X_PRODUCTS } from './products'; export default function GetParentIdPublicAPI() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const [selectedItemParent, setSelectedItemParent] = React.useState(); const handleSelectedItemsChange = (_event, id) => { diff --git a/docs/data/tree-view/rich-tree-view/items/GetParentIdPublicAPI.tsx b/docs/data/tree-view/rich-tree-view/items/GetParentIdPublicAPI.tsx index bc0a7230643fd..a333baeb3ab92 100644 --- a/docs/data/tree-view/rich-tree-view/items/GetParentIdPublicAPI.tsx +++ b/docs/data/tree-view/rich-tree-view/items/GetParentIdPublicAPI.tsx @@ -3,11 +3,11 @@ import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; import { MUI_X_PRODUCTS } from './products'; export default function GetParentIdPublicAPI() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const [selectedItemParent, setSelectedItemParent] = React.useState< string | null >(); 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 90619582fcc75..a8db5ef6b7b9b 100644 --- a/docs/data/tree-view/rich-tree-view/items/items.md +++ b/docs/data/tree-view/rich-tree-view/items/items.md @@ -190,15 +190,21 @@ Use the `onItemClick` prop to track the clicked item: ## Imperative API :::success -To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows: +To use the `apiRef` object, you need to initialize it using the `useRichTreeViewApiRef` or `useRichTreeViewProApiRef` hook as follows: ```tsx -const apiRef = useTreeViewApiRef(); +// Community package +const apiRef = useRichTreeViewApiRef(); -return ; +return ; + +// Pro package +const apiRef = useRichTreeViewProApiRef(); + +return ; ``` -When your component first renders, `apiRef` will be `undefined`. +When your component first renders, `apiRef.current` will be `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: diff --git a/docs/data/tree-view/rich-tree-view/lazy-loading/LazyLoadingAndLabelEditing.js b/docs/data/tree-view/rich-tree-view/lazy-loading/LazyLoadingAndLabelEditing.js index 9152e6e934772..45da344fde838 100644 --- a/docs/data/tree-view/rich-tree-view/lazy-loading/LazyLoadingAndLabelEditing.js +++ b/docs/data/tree-view/rich-tree-view/lazy-loading/LazyLoadingAndLabelEditing.js @@ -3,7 +3,7 @@ import { randomInt, randomName, randomId } from '@mui/x-data-grid-generator'; import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; import { DataSourceCacheDefault } from '@mui/x-tree-view/utils'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewProApiRef } from '@mui/x-tree-view-pro/hooks'; const fetchData = async () => { const rows = Array.from({ length: 10 }, () => ({ @@ -22,7 +22,7 @@ const fetchData = async () => { const customCache = new DataSourceCacheDefault({}); // 10 seconds export default function LazyLoadingAndLabelEditing() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewProApiRef(); const handleItemLabelChange = (itemId, newLabel) => { const parentId = apiRef.current?.getParentId(itemId) || 'root'; diff --git a/docs/data/tree-view/rich-tree-view/lazy-loading/LazyLoadingAndLabelEditing.tsx b/docs/data/tree-view/rich-tree-view/lazy-loading/LazyLoadingAndLabelEditing.tsx index 2a5c17bbec001..d5a2c82128e02 100644 --- a/docs/data/tree-view/rich-tree-view/lazy-loading/LazyLoadingAndLabelEditing.tsx +++ b/docs/data/tree-view/rich-tree-view/lazy-loading/LazyLoadingAndLabelEditing.tsx @@ -3,7 +3,7 @@ import { randomInt, randomName, randomId } from '@mui/x-data-grid-generator'; import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro'; import { TreeViewBaseItem, TreeViewItemId } from '@mui/x-tree-view/models'; import { DataSourceCacheDefault } from '@mui/x-tree-view/utils'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewProApiRef } from '@mui/x-tree-view-pro/hooks'; const fetchData = async (): Promise< TreeViewBaseItem<{ @@ -28,7 +28,7 @@ const fetchData = async (): Promise< const customCache = new DataSourceCacheDefault({}); // 10 seconds export default function LazyLoadingAndLabelEditing() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewProApiRef(); const handleItemLabelChange = (itemId: TreeViewItemId, newLabel: string) => { const parentId = apiRef.current?.getParentId(itemId) || 'root'; diff --git a/docs/data/tree-view/rich-tree-view/lazy-loading/lazy-loading.md b/docs/data/tree-view/rich-tree-view/lazy-loading/lazy-loading.md index 70966542dfc15..7cc9cf0af514f 100644 --- a/docs/data/tree-view/rich-tree-view/lazy-loading/lazy-loading.md +++ b/docs/data/tree-view/rich-tree-view/lazy-loading/lazy-loading.md @@ -65,15 +65,15 @@ Changes to the label are not automatically updated in the `dataSourceCache` and ## Imperative API :::success -To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows: +To use the `apiRef` object, you need to initialize it using the `useRichTreeViewProApiRef` hook as follows: ```tsx -const apiRef = useTreeViewApiRef(); +const apiRef = useRichTreeViewProApiRef(); -return ; +return ; ``` -When your component first renders, `apiRef` is `undefined`. +When your component first renders, `apiRef.current` will be `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: 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 a4ccede01d5b4..6cd44b75523ce 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js @@ -19,7 +19,8 @@ import { import { TreeItemIcon } from '@mui/x-tree-view/TreeItemIcon'; import { TreeItemProvider } from '@mui/x-tree-view/TreeItemProvider'; import { TreeItemDragAndDropOverlay } from '@mui/x-tree-view/TreeItemDragAndDropOverlay'; -import { useTreeItemModel, useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useTreeItemModel } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewProApiRef } from '@mui/x-tree-view-pro/hooks'; const ITEMS = [ { @@ -234,7 +235,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { }); export default function FileExplorer() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewProApiRef(); return ( [] = [ +const ITEMS: FileItem[] = [ { id: '1', label: 'Documents', @@ -235,7 +236,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( status, } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); - const item = useTreeItemModel(itemId)!; + const item = useTreeItemModel(itemId)!; let icon; if (status.expandable) { @@ -267,7 +268,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( }); export default function FileExplorer() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewProApiRef(); return ( 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 49a15db2c67cc..8854a2c865720 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderLeaves.tsx @@ -1,7 +1,7 @@ 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'; +import { useRichTreeViewProApiRef } from '@mui/x-tree-view-pro/hooks'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ { @@ -34,7 +34,7 @@ const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ ]; export default function OnlyReorderLeaves() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewProApiRef(); return ( 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 a75dab67dd237..12d02c67671a3 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.js +++ b/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.js @@ -3,7 +3,7 @@ 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'; +import { useRichTreeViewProApiRef } from '@mui/x-tree-view-pro/hooks'; const MUI_X_PRODUCTS = [ { @@ -50,7 +50,7 @@ const getAllItemsWithChildrenItemIds = (items) => { }; export default function SendAllItemsToServer() { - const apiRefTreeViewA = useTreeViewApiRef(); + const apiRefTreeViewA = useRichTreeViewProApiRef(); const [itemsTreeViewB, setItemsTreeViewB] = React.useState(MUI_X_PRODUCTS); const 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 726f1377e2804..49f252b338516 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/SendAllItemsToServer.tsx @@ -3,7 +3,7 @@ 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'; +import { useRichTreeViewProApiRef } from '@mui/x-tree-view-pro/hooks'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ { @@ -50,7 +50,7 @@ const getAllItemsWithChildrenItemIds = (items: TreeViewBaseItem[]) => { }; export default function SendAllItemsToServer() { - const apiRefTreeViewA = useTreeViewApiRef(); + const apiRefTreeViewA = useRichTreeViewProApiRef(); const [itemsTreeViewB, setItemsTreeViewB] = React.useState(MUI_X_PRODUCTS); const handleItemPositionChangeTreeViewA = () => { diff --git a/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelection.js b/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelection.js index 01ce56e559330..c44400a84fd86 100644 --- a/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelection.js +++ b/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelection.js @@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS = [ ]; export default function ApiMethodSetItemSelection() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleSelectGridPro = (event) => { apiRef.current?.setItemSelection({ event, itemId: 'grid-pro' }); }; diff --git a/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelection.tsx b/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelection.tsx index 3314e62cbecb3..8662043b53bf2 100644 --- a/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelection.tsx +++ b/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelection.tsx @@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { TreeViewBaseItem } from '@mui/x-tree-view/models'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ ]; export default function ApiMethodSetItemSelection() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleSelectGridPro = (event: React.SyntheticEvent) => { apiRef.current?.setItemSelection({ event, itemId: 'grid-pro' }); }; diff --git a/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.js b/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.js index dac04e3a3fe8f..c1a5e0a72179d 100644 --- a/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.js +++ b/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.js @@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS = [ ]; export default function ApiMethodSetItemSelectionKeepExistingSelection() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleSelectGridPro = (event) => { apiRef.current?.setItemSelection({ event, diff --git a/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.tsx b/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.tsx index a57bf72dfc798..61d65c77318f6 100644 --- a/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.tsx +++ b/docs/data/tree-view/rich-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.tsx @@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { TreeViewBaseItem } from '@mui/x-tree-view/models'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useRichTreeViewApiRef } from '@mui/x-tree-view/hooks'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ { @@ -37,7 +37,7 @@ const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ ]; export default function ApiMethodSetItemSelectionKeepExistingSelection() { - const apiRef = useTreeViewApiRef(); + const apiRef = useRichTreeViewApiRef(); const handleSelectGridPro = (event: React.SyntheticEvent) => { apiRef.current?.setItemSelection({ event, diff --git a/docs/data/tree-view/rich-tree-view/selection/selection.md b/docs/data/tree-view/rich-tree-view/selection/selection.md index 85e1731d48526..a6a75876543af 100644 --- a/docs/data/tree-view/rich-tree-view/selection/selection.md +++ b/docs/data/tree-view/rich-tree-view/selection/selection.md @@ -163,15 +163,21 @@ The `useApplyPropagationToSelectedItemsOnMount()` must receive the following pro ## Imperative API :::success -To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows: +To use the `apiRef` object, you need to initialize it using the `useRichTreeViewApiRef` or `useRichTreeViewProApiRef` hook as follows: ```tsx -const apiRef = useTreeViewApiRef(); +// Community package +const apiRef = useRichTreeViewApiRef(); -return ; +return ; + +// Pro package +const apiRef = useRichTreeViewProApiRef(); + +return ; ``` -When your component first renders, `apiRef` is `undefined`. +When your component first renders, `apiRef.current` will be `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: diff --git a/docs/data/tree-view/simple-tree-view/expansion/ApiMethodIsItemExpanded.js b/docs/data/tree-view/simple-tree-view/expansion/ApiMethodIsItemExpanded.js index cef0475fcd9e6..ed526cfea22e7 100644 --- a/docs/data/tree-view/simple-tree-view/expansion/ApiMethodIsItemExpanded.js +++ b/docs/data/tree-view/simple-tree-view/expansion/ApiMethodIsItemExpanded.js @@ -5,10 +5,10 @@ import Button from '@mui/material/Button'; import Snackbar from '@mui/material/Snackbar'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodIsItemExpanded() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const [isGridExpanded, setIsGridExpanded] = React.useState(false); const [isSnackbarOpen, setIsSnackbarOpen] = React.useState(false); diff --git a/docs/data/tree-view/simple-tree-view/expansion/ApiMethodIsItemExpanded.tsx b/docs/data/tree-view/simple-tree-view/expansion/ApiMethodIsItemExpanded.tsx index 93aeb79e4e805..83558d5c9b4e5 100644 --- a/docs/data/tree-view/simple-tree-view/expansion/ApiMethodIsItemExpanded.tsx +++ b/docs/data/tree-view/simple-tree-view/expansion/ApiMethodIsItemExpanded.tsx @@ -5,10 +5,10 @@ import Button from '@mui/material/Button'; import Snackbar from '@mui/material/Snackbar'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodIsItemExpanded() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const [isGridExpanded, setIsGridExpanded] = React.useState(false); const [isSnackbarOpen, setIsSnackbarOpen] = React.useState(false); diff --git a/docs/data/tree-view/simple-tree-view/expansion/ApiMethodSetItemExpansion.js b/docs/data/tree-view/simple-tree-view/expansion/ApiMethodSetItemExpansion.js index 4b18a34960d8c..f2548c84d7ca8 100644 --- a/docs/data/tree-view/simple-tree-view/expansion/ApiMethodSetItemExpansion.js +++ b/docs/data/tree-view/simple-tree-view/expansion/ApiMethodSetItemExpansion.js @@ -4,10 +4,10 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodSetItemExpansion() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const handleExpandClick = (event) => { apiRef.current.setItemExpansion({ diff --git a/docs/data/tree-view/simple-tree-view/expansion/ApiMethodSetItemExpansion.tsx b/docs/data/tree-view/simple-tree-view/expansion/ApiMethodSetItemExpansion.tsx index 8b23f369e5b93..af856d4aab509 100644 --- a/docs/data/tree-view/simple-tree-view/expansion/ApiMethodSetItemExpansion.tsx +++ b/docs/data/tree-view/simple-tree-view/expansion/ApiMethodSetItemExpansion.tsx @@ -4,10 +4,10 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodSetItemExpansion() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const handleExpandClick = (event: React.MouseEvent) => { apiRef.current!.setItemExpansion({ diff --git a/docs/data/tree-view/simple-tree-view/expansion/expansion.md b/docs/data/tree-view/simple-tree-view/expansion/expansion.md index a0b714c6665b6..595cf30c6264c 100644 --- a/docs/data/tree-view/simple-tree-view/expansion/expansion.md +++ b/docs/data/tree-view/simple-tree-view/expansion/expansion.md @@ -41,15 +41,15 @@ You can use the `expansionTrigger` prop to decide if the expansion interaction s ## Imperative API :::success -To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows: +To use the `apiRef` object, you need to initialize it using the `useSimpleTreeViewApiRef` hook as follows: ```tsx -const apiRef = useTreeViewApiRef(); +const apiRef = useSimpleTreeViewApiRef(); -return {children}; +return ; ``` -When your component first renders, `apiRef` will be `undefined`. +When your component first renders, `apiRef.current` will be `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: diff --git a/docs/data/tree-view/simple-tree-view/focus/ApiMethodFocusItem.js b/docs/data/tree-view/simple-tree-view/focus/ApiMethodFocusItem.js index 20017c9b2cd0f..f45e3a3eb484a 100644 --- a/docs/data/tree-view/simple-tree-view/focus/ApiMethodFocusItem.js +++ b/docs/data/tree-view/simple-tree-view/focus/ApiMethodFocusItem.js @@ -4,10 +4,10 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodFocusItem() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const handleButtonClick = (event) => { apiRef.current?.focusItem(event, 'pickers'); }; diff --git a/docs/data/tree-view/simple-tree-view/focus/ApiMethodFocusItem.tsx b/docs/data/tree-view/simple-tree-view/focus/ApiMethodFocusItem.tsx index 0fdad00933ed1..1ae44f50d81ab 100644 --- a/docs/data/tree-view/simple-tree-view/focus/ApiMethodFocusItem.tsx +++ b/docs/data/tree-view/simple-tree-view/focus/ApiMethodFocusItem.tsx @@ -4,10 +4,10 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodFocusItem() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const handleButtonClick = (event: React.SyntheticEvent) => { apiRef.current?.focusItem(event, 'pickers'); }; diff --git a/docs/data/tree-view/simple-tree-view/focus/focus.md b/docs/data/tree-view/simple-tree-view/focus/focus.md index 07805de5d3171..c45f1a9abf212 100644 --- a/docs/data/tree-view/simple-tree-view/focus/focus.md +++ b/docs/data/tree-view/simple-tree-view/focus/focus.md @@ -14,15 +14,15 @@ waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ ## Imperative API :::success -To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows: +To use the `apiRef` object, you need to initialize it using the `useSimpleTreeViewApiRef` hook as follows: ```tsx -const apiRef = useTreeViewApiRef(); +const apiRef = useSimpleTreeViewApiRef(); -return {children}; +return ; ``` -When your component first renders, `apiRef` will be `undefined`. +When your component first renders, `apiRef.current` will be `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: diff --git a/docs/data/tree-view/simple-tree-view/items/ApiMethodGetItemDOMElement.js b/docs/data/tree-view/simple-tree-view/items/ApiMethodGetItemDOMElement.js index e39e0ea5a549a..25a5507176d99 100644 --- a/docs/data/tree-view/simple-tree-view/items/ApiMethodGetItemDOMElement.js +++ b/docs/data/tree-view/simple-tree-view/items/ApiMethodGetItemDOMElement.js @@ -4,10 +4,10 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodGetItemDOMElement() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const handleScrollToChartsCommunity = (event) => { apiRef.current.focusItem(event, 'charts-community'); apiRef.current diff --git a/docs/data/tree-view/simple-tree-view/items/ApiMethodGetItemDOMElement.tsx b/docs/data/tree-view/simple-tree-view/items/ApiMethodGetItemDOMElement.tsx index 926f0dba4ac4a..fdfa61f8bd6f8 100644 --- a/docs/data/tree-view/simple-tree-view/items/ApiMethodGetItemDOMElement.tsx +++ b/docs/data/tree-view/simple-tree-view/items/ApiMethodGetItemDOMElement.tsx @@ -4,10 +4,10 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodGetItemDOMElement() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const handleScrollToChartsCommunity = (event: React.SyntheticEvent) => { apiRef.current!.focusItem(event, 'charts-community'); apiRef diff --git a/docs/data/tree-view/simple-tree-view/items/items.md b/docs/data/tree-view/simple-tree-view/items/items.md index 93c2b5b16d1b6..8fff5efefe655 100644 --- a/docs/data/tree-view/simple-tree-view/items/items.md +++ b/docs/data/tree-view/simple-tree-view/items/items.md @@ -83,15 +83,15 @@ Use the `onItemClick` prop to track the clicked item: ## Imperative API :::success -To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows: +To use the `apiRef` object, you need to initialize it using the `useSimpleTreeViewApiRef` hook as follows: ```tsx -const apiRef = useTreeViewApiRef(); +const apiRef = useSimpleTreeViewApiRef(); -return {children}; +return ; ``` -When your component first renders, `apiRef` is `undefined`. +When your component first renders, `apiRef.current` will be `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: diff --git a/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelection.js b/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelection.js index 94b016b0c5c3c..9282498187048 100644 --- a/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelection.js +++ b/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelection.js @@ -4,10 +4,10 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodSetItemSelection() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const handleSelectGridPro = (event) => { apiRef.current?.setItemSelection({ event, itemId: 'grid-pro' }); }; diff --git a/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelection.tsx b/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelection.tsx index ddd1886dab32c..4b247d214c609 100644 --- a/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelection.tsx +++ b/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelection.tsx @@ -4,10 +4,10 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodSetItemSelection() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const handleSelectGridPro = (event: React.SyntheticEvent) => { apiRef.current?.setItemSelection({ event, itemId: 'grid-pro' }); }; diff --git a/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.js b/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.js index 0fcc269b156a8..05f591e067804 100644 --- a/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.js +++ b/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.js @@ -4,10 +4,10 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodSetItemSelectionKeepExistingSelection() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const handleSelectGridPro = (event) => { apiRef.current?.setItemSelection({ event, diff --git a/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.tsx b/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.tsx index c71716bb94ff9..c072b82b4c169 100644 --- a/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.tsx +++ b/docs/data/tree-view/simple-tree-view/selection/ApiMethodSetItemSelectionKeepExistingSelection.tsx @@ -4,10 +4,10 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useSimpleTreeViewApiRef } from '@mui/x-tree-view/hooks'; export default function ApiMethodSetItemSelectionKeepExistingSelection() { - const apiRef = useTreeViewApiRef(); + const apiRef = useSimpleTreeViewApiRef(); const handleSelectGridPro = (event: React.SyntheticEvent) => { apiRef.current?.setItemSelection({ event, diff --git a/docs/data/tree-view/simple-tree-view/selection/selection.md b/docs/data/tree-view/simple-tree-view/selection/selection.md index f90cbdaf128c9..d76dd28c377d9 100644 --- a/docs/data/tree-view/simple-tree-view/selection/selection.md +++ b/docs/data/tree-view/simple-tree-view/selection/selection.md @@ -77,15 +77,15 @@ Use the `onItemSelectionToggle` prop if you want to react to an item selection c ## Imperative API :::success -To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows: +To use the `apiRef` object, you need to initialize it using the `useSimpleTreeViewApiRef` hook as follows: ```tsx -const apiRef = useTreeViewApiRef(); +const apiRef = useSimpleTreeViewApiRef(); -return {children}; +return ; ``` -When your component first renders, `apiRef` will be `undefined`. +When your component first renders, `apiRef.current` will be `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: diff --git a/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json b/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json index ebed0808a0419..1e2de0352eb82 100644 --- a/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json +++ b/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json @@ -2,7 +2,7 @@ "componentDescription": "", "propDescriptions": { "apiRef": { - "description": "The ref object that allows Tree View manipulation. Can be instantiated with useTreeViewApiRef()." + "description": "The ref object that allows Tree View manipulation. Can be instantiated with useRichTreeViewApiProRef()." }, "canMoveItemToNewPosition": { "description": "Used to determine if a given item can move to some new position.", diff --git a/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json b/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json index 2c0ba24ef78e3..d48d942b9be77 100644 --- a/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json +++ b/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json @@ -2,7 +2,7 @@ "componentDescription": "", "propDescriptions": { "apiRef": { - "description": "The ref object that allows Tree View manipulation. Can be instantiated with useTreeViewApiRef()." + "description": "The ref object that allows Tree View manipulation. Can be instantiated with useRichTreeViewApiRef()." }, "checkboxSelection": { "description": "Whether the Tree View renders a checkbox at the left of its label that allows selecting it." diff --git a/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json b/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json index 1fa5df1cc6922..92cc3bebdbaed 100644 --- a/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json +++ b/docs/translations/api-docs/tree-view/simple-tree-view/simple-tree-view.json @@ -2,7 +2,7 @@ "componentDescription": "", "propDescriptions": { "apiRef": { - "description": "The ref object that allows Tree View manipulation. Can be instantiated with useTreeViewApiRef()." + "description": "The ref object that allows Tree View manipulation. Can be instantiated with useSimpleTreeViewApiRef()." }, "checkboxSelection": { "description": "Whether the Tree View renders a checkbox at the left of its label that allows selecting it." diff --git a/eslint.config.mjs b/eslint.config.mjs index 7aca8704a4fbd..a961d8123320e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -330,7 +330,6 @@ export default defineConfig( // Exceptions (QUESTION: Keep or remove?) '!@mui/x-data-grid/internals/demo', '!@mui/x-date-pickers/internals/demo', - '!@mui/x-tree-view/hooks/useTreeViewApiRef', // TODO: export this from /ButtonBase in core. This will break after we move to package exports '!@mui/material/ButtonBase/TouchRipple', ], diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.plugins.ts b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.plugins.ts deleted file mode 100644 index 2dcd39644b67b..0000000000000 --- a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.plugins.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - useTreeViewItems, - UseTreeViewItemsParameters, - useTreeViewExpansion, - UseTreeViewExpansionParameters, - useTreeViewSelection, - UseTreeViewSelectionParameters, - useTreeViewFocus, - UseTreeViewFocusParameters, - useTreeViewKeyboardNavigation, - ConvertPluginsIntoSignatures, - TreeViewCorePluginParameters, - useTreeViewLabel, - UseTreeViewLabelParameters, - UseTreeViewLazyLoadingParameters, -} from '@mui/x-tree-view/internals'; -import { - useTreeViewItemsReordering, - UseTreeViewItemsReorderingParameters, -} from '../internals/plugins/useTreeViewItemsReordering'; -import { useTreeViewLazyLoading } from '../internals/plugins/useTreeViewLazyLoading'; - -export const RICH_TREE_VIEW_PRO_PLUGINS = [ - useTreeViewItems, - useTreeViewExpansion, - useTreeViewSelection, - useTreeViewFocus, - useTreeViewKeyboardNavigation, - useTreeViewLabel, - useTreeViewLazyLoading, - useTreeViewItemsReordering, -] as const; - -export type RichTreeViewProPluginSignatures = ConvertPluginsIntoSignatures< - typeof RICH_TREE_VIEW_PRO_PLUGINS ->; - -// We can't infer this type from the plugin, otherwise we would lose the generics. -export interface RichTreeViewProPluginParameters - extends TreeViewCorePluginParameters, - UseTreeViewItemsParameters, - UseTreeViewExpansionParameters, - UseTreeViewFocusParameters, - UseTreeViewSelectionParameters, - UseTreeViewLabelParameters, - UseTreeViewLazyLoadingParameters, - UseTreeViewItemsReorderingParameters {} diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx index 811d9027a5c0b..cef6642bdbfbb 100644 --- a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx +++ b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx @@ -1,18 +1,24 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs'; import composeClasses from '@mui/utils/composeClasses'; import { useLicenseVerifier, Watermark } from '@mui/x-license'; import useSlotProps from '@mui/utils/useSlotProps'; -import { useTreeView, TreeViewProvider, RichTreeViewItems } from '@mui/x-tree-view/internals'; +import { + TreeViewProvider, + RichTreeViewItems, + TreeViewItemDepthContext, + itemsSelectors, + useTreeViewRootProps, + useTreeViewStore, +} from '@mui/x-tree-view/internals'; import { warnOnce } from '@mui/x-internals/warning'; import { styled, createUseThemeProps } from '../internals/zero-styled'; import { getRichTreeViewProUtilityClass } from './richTreeViewProClasses'; import { RichTreeViewProProps } from './RichTreeViewPro.types'; -import { - RICH_TREE_VIEW_PRO_PLUGINS, - RichTreeViewProPluginSignatures, -} from './RichTreeViewPro.plugins'; +import { useExtractRichTreeViewProParameters } from './useExtractRichTreeViewProParameters'; +import { RichTreeViewProStore } from '../internals/RichTreeViewProStore'; const useThemeProps = createUseThemeProps('MuiRichTreeViewPro'); @@ -69,9 +75,8 @@ const releaseInfo = '__RELEASE_INFO__'; const RichTreeViewPro = React.forwardRef(function RichTreeViewPro< R extends {}, Multiple extends boolean | undefined = undefined, ->(inProps: RichTreeViewProProps, ref: React.Ref) { +>(inProps: RichTreeViewProProps, forwardedRef: React.Ref) { const props = useThemeProps({ props: inProps, name: 'MuiRichTreeViewPro' }); - const { slots, slotProps, ...other } = props; useLicenseVerifier('x-tree-view-pro', releaseInfo); @@ -85,14 +90,13 @@ const RichTreeViewPro = React.forwardRef(function RichTreeViewPro< } } - const { getRootProps, contextValue } = useTreeView( - { - plugins: RICH_TREE_VIEW_PRO_PLUGINS, - rootRef: ref, - props: other, - }, - ); + const { slots, slotProps, apiRef, parameters, forwardedProps } = + useExtractRichTreeViewProParameters(props); + const store = useTreeViewStore(RichTreeViewProStore, parameters); + const ref = React.useRef(null); + const handleRef = useMergedRefs(forwardedRef, ref); + const getRootProps = useTreeViewRootProps(store, forwardedProps, handleRef); const classes = useUtilityClasses(props); const Root = slots?.root ?? RichTreeViewProRoot; @@ -106,15 +110,19 @@ const RichTreeViewPro = React.forwardRef(function RichTreeViewPro< return ( - - - - + + + + + + ); }) as RichTreeViewProComponent; @@ -125,7 +133,7 @@ RichTreeViewPro.propTypes = { // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- /** - * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`. + * The ref object that allows Tree View manipulation. Can be instantiated with `useRichTreeViewApiProRef()`. */ apiRef: PropTypes.shape({ current: PropTypes.shape({ diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.types.ts b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.types.ts index 381d9e566d4a1..b89fa1fa2eddb 100644 --- a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.types.ts +++ b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.types.ts @@ -2,18 +2,17 @@ import * as React from 'react'; import { Theme } from '@mui/material/styles'; import { SxProps } from '@mui/system/styleFunctionSx'; import { SlotComponentProps } from '@mui/utils/types'; +import { TreeViewValidItem } from '@mui/x-tree-view/models'; import { - TreeViewPublicAPI, RichTreeViewItemsSlots, RichTreeViewItemsSlotProps, TreeViewSlots, TreeViewSlotProps, + UseTreeViewStoreParameters, + TreeViewPublicAPI, } from '@mui/x-tree-view/internals'; import { RichTreeViewProClasses } from './richTreeViewProClasses'; -import { - RichTreeViewProPluginParameters, - RichTreeViewProPluginSignatures, -} from './RichTreeViewPro.plugins'; +import { RichTreeViewProStore } from '../internals/RichTreeViewProStore'; export interface RichTreeViewProSlots extends TreeViewSlots, RichTreeViewItemsSlots { /** @@ -29,9 +28,10 @@ export interface RichTreeViewProSlotProps>; } -export type RichTreeViewProApiRef = React.RefObject< - Partial> | undefined ->; +export type RichTreeViewProApiRef< + R extends TreeViewValidItem = any, + Multiple extends boolean | undefined = any, +> = React.RefObject>> | undefined>; export interface RichTreeViewProPropsBase extends React.HTMLAttributes { className?: string; @@ -46,7 +46,7 @@ export interface RichTreeViewProPropsBase extends React.HTMLAttributes - extends RichTreeViewProPluginParameters, + extends UseTreeViewStoreParameters>, RichTreeViewProPropsBase { /** * Overridable component slots. @@ -59,7 +59,7 @@ export interface RichTreeViewProProps; /** - * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`. + * The ref object that allows Tree View manipulation. Can be instantiated with `useRichTreeViewApiProRef()`. */ apiRef?: RichTreeViewProApiRef; } diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/index.ts b/packages/x-tree-view-pro/src/RichTreeViewPro/index.ts index 4c055be9c5ed7..bd82bf5609c48 100644 --- a/packages/x-tree-view-pro/src/RichTreeViewPro/index.ts +++ b/packages/x-tree-view-pro/src/RichTreeViewPro/index.ts @@ -7,4 +7,3 @@ export type { RichTreeViewProSlotProps, RichTreeViewProApiRef, } from './RichTreeViewPro.types'; -export type { RichTreeViewProPluginSignatures } from './RichTreeViewPro.plugins'; diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/useExtractRichTreeViewProParameters.ts b/packages/x-tree-view-pro/src/RichTreeViewPro/useExtractRichTreeViewProParameters.ts new file mode 100644 index 0000000000000..1aaee34c16b2a --- /dev/null +++ b/packages/x-tree-view-pro/src/RichTreeViewPro/useExtractRichTreeViewProParameters.ts @@ -0,0 +1,144 @@ +import * as React from 'react'; +import { TreeViewValidItem } from '@mui/x-tree-view/models'; +import { UseTreeViewStoreParameters } from '@mui/x-tree-view/internals'; +import { RichTreeViewProStore } from '../internals/RichTreeViewProStore'; +import { RichTreeViewProProps } from './RichTreeViewPro.types'; + +export function useExtractRichTreeViewProParameters< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +>(props: RichTreeViewProProps) { + const { + // Props for Provider + apiRef, + slots, + slotProps, + + // Shared parameters + disabledItemsFocusable, + items, + isItemDisabled, + getItemLabel, + getItemChildren, + getItemId, + onItemClick, + itemChildrenIndentation, + id, + expandedItems, + defaultExpandedItems, + onExpandedItemsChange, + onItemExpansionToggle, + expansionTrigger, + disableSelection, + selectedItems, + defaultSelectedItems, + multiSelect, + checkboxSelection, + selectionPropagation, + onSelectedItemsChange, + onItemSelectionToggle, + onItemFocus, + + // RichTreeViewStore parameters + onItemLabelChange, + isItemEditable, + + // RichTreeViewProStore parameters + dataSource, + dataSourceCache, + itemsReordering, + isItemReorderable, + canMoveItemToNewPosition, + onItemPositionChange, + + // Forwarded props + ...forwardedProps + } = props; + + const parameters: UseTreeViewStoreParameters> = React.useMemo( + () => ({ + // Shared parameters + disabledItemsFocusable, + items, + isItemDisabled, + getItemLabel, + getItemChildren, + getItemId, + onItemClick, + itemChildrenIndentation, + id, + expandedItems, + defaultExpandedItems, + onExpandedItemsChange, + onItemExpansionToggle, + expansionTrigger, + disableSelection, + selectedItems, + defaultSelectedItems, + multiSelect, + checkboxSelection, + selectionPropagation, + onSelectedItemsChange, + onItemSelectionToggle, + onItemFocus, + + // RichTreeViewStore parameters + onItemLabelChange, + isItemEditable, + + // RichTreeViewProStore parameters + dataSource, + dataSourceCache, + itemsReordering, + isItemReorderable, + canMoveItemToNewPosition, + onItemPositionChange, + }), + [ + // Shared parameters + disabledItemsFocusable, + items, + isItemDisabled, + getItemLabel, + getItemChildren, + getItemId, + onItemClick, + itemChildrenIndentation, + id, + expandedItems, + defaultExpandedItems, + onExpandedItemsChange, + onItemExpansionToggle, + expansionTrigger, + disableSelection, + selectedItems, + defaultSelectedItems, + multiSelect, + checkboxSelection, + selectionPropagation, + onSelectedItemsChange, + onItemSelectionToggle, + onItemFocus, + + // RichTreeViewStore parameters + onItemLabelChange, + isItemEditable, + + // RichTreeViewProStore parameters + dataSource, + dataSourceCache, + itemsReordering, + isItemReorderable, + canMoveItemToNewPosition, + onItemPositionChange, + ], + ); + + return { + apiRef, + slots, + slotProps, + parameters, + forwardedProps, + }; +} diff --git a/packages/x-tree-view-pro/src/hooks/index.ts b/packages/x-tree-view-pro/src/hooks/index.ts new file mode 100644 index 0000000000000..294059118c4a4 --- /dev/null +++ b/packages/x-tree-view-pro/src/hooks/index.ts @@ -0,0 +1 @@ +export { useRichTreeViewProApiRef } from './useRichTreeViewProApiRef'; diff --git a/packages/x-tree-view-pro/src/hooks/useRichTreeViewProApiRef.ts b/packages/x-tree-view-pro/src/hooks/useRichTreeViewProApiRef.ts new file mode 100644 index 0000000000000..4a953a9bd2cf2 --- /dev/null +++ b/packages/x-tree-view-pro/src/hooks/useRichTreeViewProApiRef.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { TreeViewPublicAPI } from '@mui/x-tree-view/internals'; +import { TreeViewDefaultItemModelProperties, TreeViewValidItem } from '@mui/x-tree-view/models'; +import { RichTreeViewProStore } from '../internals/RichTreeViewProStore'; + +/** + * Creates the ref to pass to the `apiRef` prop of the `RichTreeViewPro` component. + */ +export function useRichTreeViewProApiRef< + R extends TreeViewValidItem = TreeViewDefaultItemModelProperties, +>() { + return React.useRef(undefined) as React.RefObject< + TreeViewPublicAPI> | undefined + >; +} diff --git a/packages/x-tree-view-pro/src/index.ts b/packages/x-tree-view-pro/src/index.ts index 98eb0f18be9fa..6e7296ea8bce7 100644 --- a/packages/x-tree-view-pro/src/index.ts +++ b/packages/x-tree-view-pro/src/index.ts @@ -10,8 +10,7 @@ export * from '@mui/x-tree-view/TreeItemProvider'; export * from '@mui/x-tree-view/TreeItemDragAndDropOverlay'; export * from '@mui/x-tree-view/TreeItemLabelInput'; -export { unstable_resetCleanupTracking } from '@mui/x-tree-view/internals'; - export * from '@mui/x-tree-view/models'; export * from '@mui/x-tree-view/icons'; export * from '@mui/x-tree-view/hooks'; +export * from './hooks'; diff --git a/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/RichTreeViewProStore.ts b/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/RichTreeViewProStore.ts new file mode 100644 index 0000000000000..536ce214c0ffd --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/RichTreeViewProStore.ts @@ -0,0 +1,33 @@ +import { ExtendableRichTreeViewStore } from '@mui/x-tree-view/internals'; +import { TreeViewValidItem } from '@mui/x-tree-view/models'; +import { RichTreeViewProStoreParameters, RichTreeViewProState } from './RichTreeViewProStore.types'; +import { TreeViewLazyLoadingPlugin } from '../plugins/lazyLoading'; +import { TreeViewItemsReorderingPlugin } from '../plugins/itemsReordering'; +import { parametersToStateMapper } from './RichTreeViewProStore.utils'; + +export class RichTreeViewProStore< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +> extends ExtendableRichTreeViewStore< + R, + Multiple, + RichTreeViewProState, + RichTreeViewProStoreParameters +> { + public lazyLoading: TreeViewLazyLoadingPlugin; + + public itemsReordering = new TreeViewItemsReorderingPlugin(this); + + public constructor(parameters: RichTreeViewProStoreParameters) { + super(parameters, 'RichTreeViewPro', parametersToStateMapper); + + this.lazyLoading = new TreeViewLazyLoadingPlugin(this); + } + + public buildPublicAPI() { + return { + ...super.buildPublicAPI(), + ...this.lazyLoading.buildPublicAPI(), + }; + } +} diff --git a/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/RichTreeViewProStore.types.ts b/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/RichTreeViewProStore.types.ts new file mode 100644 index 0000000000000..10c3946fffff1 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/RichTreeViewProStore.types.ts @@ -0,0 +1,81 @@ +import { RichTreeViewStoreParameters, RichTreeViewState } from '@mui/x-tree-view/internals'; +import { + TreeViewItemId, + TreeViewItemsReorderingAction, + TreeViewValidItem, +} from '@mui/x-tree-view/models'; +import { DataSourceCache } from '@mui/x-tree-view/utils'; +import { TreeViewItemReorderPosition } from '../plugins/itemsReordering'; +import { DataSource } from '../plugins/lazyLoading'; + +export interface RichTreeViewProState< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +> extends RichTreeViewState { + /** + * Determine if a given item can be reordered. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item can be reordered. + */ + isItemReorderable: (itemId: TreeViewItemId) => boolean; + /** + * The current ongoing reordering operation. + */ + currentReorder: { + draggedItemId: TreeViewItemId; + targetItemId: TreeViewItemId; + newPosition: TreeViewItemReorderPosition | null; + action: TreeViewItemsReorderingAction | null; + } | null; +} + +export interface RichTreeViewProStoreParameters< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +> extends RichTreeViewStoreParameters { + /** + * The data source object. + */ + dataSource?: DataSource; + /** + * The data source cache object. + */ + dataSourceCache?: DataSourceCache; + /** + * If `true`, the reordering of items is enabled. + * @default false + */ + itemsReordering?: boolean; + /** + * Determine if a given item can be reordered. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item can be reordered. + * @default () => true + */ + isItemReorderable?: (itemId: TreeViewItemId) => boolean; + /** + * Used to determine if a given item can move to some new position. + * @param {object} parameters The params describing the item re-ordering. + * @param {TreeViewItemId} parameters.itemId The id of the item that is being moved to a new position. + * @param {TreeViewItemReorderPosition} parameters.oldPosition The old position of the item. + * @param {TreeViewItemReorderPosition} parameters.newPosition The new position of the item. + * @returns {boolean} `true` if the item can move to the new position. + */ + canMoveItemToNewPosition?: (parameters: { + itemId: TreeViewItemId; + oldPosition: TreeViewItemReorderPosition; + newPosition: TreeViewItemReorderPosition; + }) => boolean; + /** + * Callback fired when a Tree Item is moved in the tree. + * @param {object} parameters The params describing the item re-ordering. + * @param {TreeViewItemId} parameters.itemId The id of the item moved. + * @param {TreeViewItemReorderPosition} parameters.oldPosition The old position of the item. + * @param {TreeViewItemReorderPosition} parameters.newPosition The new position of the item. + */ + onItemPositionChange?: (parameters: { + itemId: TreeViewItemId; + oldPosition: TreeViewItemReorderPosition; + newPosition: TreeViewItemReorderPosition; + }) => void; +} diff --git a/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/RichTreeViewProStore.utils.ts b/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/RichTreeViewProStore.utils.ts new file mode 100644 index 0000000000000..dded2ea8385ed --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/RichTreeViewProStore.utils.ts @@ -0,0 +1,42 @@ +import { + ExtendableRichTreeViewStore, + TreeViewParametersToStateMapper, +} from '@mui/x-tree-view/internals'; +import { RichTreeViewProStoreParameters, RichTreeViewProState } from './RichTreeViewProStore.types'; +import { TREE_VIEW_LAZY_LOADED_ITEMS_INITIAL_STATE } from '../plugins/lazyLoading'; + +const DEFAULT_IS_ITEM_REORDERABLE_WHEN_ENABLED = () => true; +const DEFAULT_IS_ITEM_REORDERABLE_WHEN_DISABLED = () => false; + +const deriveStateFromParameters = (parameters: RichTreeViewProStoreParameters) => ({ + lazyLoadedItems: parameters.dataSource ? TREE_VIEW_LAZY_LOADED_ITEMS_INITIAL_STATE : null, + currentReorder: null, + isItemReorderable: parameters.itemsReordering + ? (parameters.isItemReorderable ?? DEFAULT_IS_ITEM_REORDERABLE_WHEN_ENABLED) + : DEFAULT_IS_ITEM_REORDERABLE_WHEN_DISABLED, +}); + +export const parametersToStateMapper: TreeViewParametersToStateMapper< + any, + any, + RichTreeViewProState, + RichTreeViewProStoreParameters +> = { + getInitialState: (minimalInitialState, parameters) => ({ + ...ExtendableRichTreeViewStore.rawMapper.getInitialState(minimalInitialState, parameters), + ...deriveStateFromParameters(parameters), + }), + updateStateFromParameters: (newMinimalState, parameters, updateModel) => { + const newState: Partial> = { + ...ExtendableRichTreeViewStore.rawMapper.updateStateFromParameters( + newMinimalState, + parameters, + updateModel, + ), + ...deriveStateFromParameters(parameters), + }; + + return newState; + }, + shouldIgnoreItemsStateUpdate: (parameters) => !!parameters.dataSource, +}; diff --git a/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/index.ts b/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/index.ts new file mode 100644 index 0000000000000..3d46b182c80d3 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/RichTreeViewProStore/index.ts @@ -0,0 +1,2 @@ +export * from './RichTreeViewProStore'; +export * from './RichTreeViewProStore.types'; diff --git a/packages/x-tree-view-pro/src/internals/index.ts b/packages/x-tree-view-pro/src/internals/index.ts deleted file mode 100644 index e79ec117aff50..0000000000000 --- a/packages/x-tree-view-pro/src/internals/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { UseTreeViewItemsReorderingSignature } from './plugins/useTreeViewItemsReordering'; diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/TreeViewItemsReorderingPlugin.test.tsx similarity index 97% rename from packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx rename to packages/x-tree-view-pro/src/internals/plugins/itemsReordering/TreeViewItemsReorderingPlugin.test.tsx index b320c6fd8cf80..2e39356559602 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx +++ b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/TreeViewItemsReorderingPlugin.test.tsx @@ -2,12 +2,10 @@ import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; import { spy } from 'sinon'; import { fireEvent, createEvent } from '@mui/internal-test-utils'; import { DragEventTypes, MockedDataTransfer } from 'test/utils/dragAndDrop'; -import { chooseActionToApply } from './useTreeViewItemsReordering.utils'; -import { TreeViewItemItemReorderingValidActions } from './useTreeViewItemsReordering.types'; - -// TODO #20051: Replace with imported type -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type RichTreeViewProStore = any; +import {} from '@mui/x-tree-view/internals'; +import { chooseActionToApply } from './utils'; +import { TreeViewItemItemReorderingValidActions } from './types'; +import { RichTreeViewProStore } from '../../RichTreeViewProStore'; interface DragEventOptions { /** diff --git a/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/TreeViewItemsReorderingPlugin.ts b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/TreeViewItemsReorderingPlugin.ts new file mode 100644 index 0000000000000..1c6a973d7f3a5 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/TreeViewItemsReorderingPlugin.ts @@ -0,0 +1,267 @@ +import { TreeViewItemId, TreeViewItemsReorderingAction } from '@mui/x-tree-view/models'; +import { itemsSelectors, labelSelectors } from '@mui/x-tree-view/internals'; +import { TreeViewItemItemReorderingValidActions, TreeViewItemReorderPosition } from './types'; +import { RichTreeViewProStore } from '../../RichTreeViewProStore/RichTreeViewProStore'; +import { itemsReorderingSelectors } from './selectors'; +import { chooseActionToApply, isAncestor, moveItemInTree } from './utils'; +import { useTreeViewItemsReorderingItemPlugin } from './itemPlugin'; + +export class TreeViewItemsReorderingPlugin { + private store: RichTreeViewProStore; + + constructor(store: RichTreeViewProStore) { + this.store = store; + store.itemPluginManager.register(useTreeViewItemsReorderingItemPlugin, null); + } + + /** + * 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. + */ + public canItemBeDragged = (itemId: TreeViewItemId) => { + if (!this.store.parameters.itemsReordering) { + return false; + } + + const isItemReorderable = this.store.parameters.isItemReorderable; + if (isItemReorderable) { + return isItemReorderable(itemId); + } + + return true; + }; + + /** + * 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 the item. + */ + public getDroppingTargetValidActions = (itemId: string) => { + const currentReorder = itemsReorderingSelectors.currentReorder(this.store.state); + if (!currentReorder) { + throw new Error('There is no ongoing reordering.'); + } + + if (itemId === currentReorder.draggedItemId) { + return {}; + } + + const canMoveItemToNewPosition = this.store.parameters.canMoveItemToNewPosition; + const targetItemMeta = itemsSelectors.itemMeta(this.store.state, itemId)!; + const targetItemIndex = itemsSelectors.itemIndex(this.store.state, targetItemMeta.id); + const draggedItemMeta = itemsSelectors.itemMeta( + this.store.state, + currentReorder.draggedItemId, + )!; + const draggedItemIndex = itemsSelectors.itemIndex(this.store.state, draggedItemMeta.id); + const isTargetLastSibling = + targetItemIndex === + itemsSelectors.itemOrderedChildrenIds(this.store.state, targetItemMeta.parentId).length - 1; + + 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: currentReorder.draggedItemId, + 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 || isTargetLastSibling + ? { + parentId: targetItemMeta.parentId, + index: + targetItemMeta.parentId === draggedItemMeta.parentId && + targetItemIndex > draggedItemIndex + ? targetItemIndex + : targetItemIndex + 1, + } + : null, + 'move-to-parent': + targetItemMeta.parentId == null + ? null + : { + parentId: targetItemMeta.parentId, + index: itemsSelectors.itemOrderedChildrenIds( + this.store.state, + 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; + }; + + /** + * Start a reordering for the given item. + * @param {TreeViewItemId} itemId The id of the item to start the reordering for. + */ + public startDraggingItem = (itemId: string) => { + const isItemBeingEdited = labelSelectors.isItemBeingEdited(this.store.state, itemId); + if (isItemBeingEdited) { + return; + } + + this.store.set('currentReorder', { + targetItemId: itemId, + draggedItemId: itemId, + action: null, + newPosition: null, + }); + }; + + /** + * Cancel the current reordering operation and reset the state. + */ + public cancelDraggingItem = () => { + this.store.set('currentReorder', null); + }; + + /** + * Complete the reordering of a given item. + * @param {TreeViewItemId} itemId The id of the item to complete the reordering for. + */ + public completeDraggingItem = (itemId: string) => { + const currentReorder = itemsReorderingSelectors.currentReorder(this.store.state); + if (currentReorder == null || currentReorder.draggedItemId !== itemId) { + return; + } + + if ( + currentReorder.draggedItemId === currentReorder.targetItemId || + currentReorder.action == null || + currentReorder.newPosition == null + ) { + this.cancelDraggingItem(); + return; + } + + const draggedItemMeta = itemsSelectors.itemMeta( + this.store.state, + currentReorder.draggedItemId, + )!; + + const oldPosition: TreeViewItemReorderPosition = { + parentId: draggedItemMeta.parentId, + index: itemsSelectors.itemIndex(this.store.state, draggedItemMeta.id), + }; + + const newPosition = currentReorder.newPosition; + + this.store.update({ + currentReorder: null, + ...moveItemInTree({ + itemToMoveId: itemId, + newPosition, + oldPosition, + prevState: this.store.state, + }), + }); + + const onItemPositionChange = this.store.parameters.onItemPositionChange; + onItemPositionChange?.({ + itemId, + newPosition, + oldPosition, + }); + }; + + /** + * 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. + */ + public setDragTargetItem = ({ + itemId, + validActions, + targetHeight, + cursorY, + cursorX, + contentElement, + }: { + itemId: TreeViewItemId; + validActions: TreeViewItemItemReorderingValidActions; + targetHeight: number; + cursorY: number; + cursorX: number; + contentElement: HTMLDivElement; + }) => { + const prevItemReorder = this.store.state.currentReorder; + if (prevItemReorder == null || isAncestor(this.store, itemId, prevItemReorder.draggedItemId)) { + return; + } + + const action = chooseActionToApply({ + itemChildrenIndentation: this.store.state.itemChildrenIndentation, + validActions, + targetHeight, + targetDepth: this.store.state.itemMetaLookup[itemId].depth!, + cursorY, + cursorX, + contentElement, + }); + + const newPosition = action == null ? null : validActions[action]!; + + if ( + prevItemReorder.targetItemId === itemId && + prevItemReorder.action === action && + prevItemReorder.newPosition?.parentId === newPosition?.parentId && + prevItemReorder.newPosition?.index === newPosition?.index + ) { + return; + } + + this.store.set('currentReorder', { + ...prevItemReorder, + targetItemId: itemId, + newPosition, + action, + }); + }; +} diff --git a/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/index.ts b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/index.ts new file mode 100644 index 0000000000000..92882a2bfd684 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/index.ts @@ -0,0 +1,3 @@ +export * from './TreeViewItemsReorderingPlugin'; +export * from './selectors'; +export type { TreeViewItemReorderPosition } from './types'; diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/itemPlugin.ts b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/itemPlugin.ts similarity index 89% rename from packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/itemPlugin.ts rename to packages/x-tree-view-pro/src/internals/plugins/itemsReordering/itemPlugin.ts index 95e4fbaa4115f..2477681df8da2 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/itemPlugin.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/itemPlugin.ts @@ -4,24 +4,17 @@ import { TreeViewCancellableEvent, TreeViewCancellableEventHandler } from '@mui/ import { TreeViewItemPlugin, useTreeViewContext, - UseTreeViewItemsSignature, isTargetInDescendants, - UseTreeViewLabelSignature, } from '@mui/x-tree-view/internals'; import { TreeItemDragAndDropOverlayProps } from '@mui/x-tree-view/TreeItemDragAndDropOverlay'; -import { - UseTreeViewItemsReorderingSignature, - TreeViewItemItemReorderingValidActions, -} from './useTreeViewItemsReordering.types'; -import { itemsReorderingSelectors } from './useTreeViewItemsReordering.selectors'; +import { TreeViewItemItemReorderingValidActions } from './types'; +import { itemsReorderingSelectors } from './selectors'; +import { RichTreeViewProStore } from '../../RichTreeViewProStore'; export const isAndroid = () => navigator.userAgent.toLowerCase().includes('android'); export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ props }) => { - const { instance, store } = useTreeViewContext< - [UseTreeViewItemsSignature, UseTreeViewItemsReorderingSignature], - [UseTreeViewLabelSignature] - >(); + const { store } = useTreeViewContext>(); const { itemId } = props; const validActionsRef = React.useRef(null); @@ -70,7 +63,7 @@ export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ props // iOS requires a media type to be defined event.dataTransfer.setData('application/mui-x', ''); - instance.startDraggingItem(itemId); + store.itemsReordering.startDraggingItem(itemId); }; const handleRootDragOver = (event: React.DragEvent & TreeViewCancellableEvent) => { @@ -90,11 +83,11 @@ export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ props // Check if the drag-and-drop was cancelled, possibly by pressing Escape if (event.dataTransfer.dropEffect === 'none') { - instance.cancelDraggingItem(); + store.itemsReordering.cancelDraggingItem(); return; } - instance.completeDraggingItem(itemId); + store.itemsReordering.completeDraggingItem(itemId); }; return { @@ -121,7 +114,7 @@ export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ props const rect = (event.target as HTMLDivElement).getBoundingClientRect(); const y = event.clientY - rect.top; const x = event.clientX - rect.left; - instance.setDragTargetItem({ + store.itemsReordering.setDragTargetItem({ itemId, validActions: validActionsRef.current, targetHeight: rect.height, @@ -137,7 +130,7 @@ export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ props return; } - validActionsRef.current = instance.getDroppingTargetValidActions(itemId); + validActionsRef.current = store.itemsReordering.getDroppingTargetValidActions(itemId); }; return { diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.selectors.ts b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/selectors.ts similarity index 65% rename from packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.selectors.ts rename to packages/x-tree-view-pro/src/internals/plugins/itemsReordering/selectors.ts index 2904db4b525cf..d491dc9afe8a6 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.selectors.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/selectors.ts @@ -1,22 +1,18 @@ import { createSelector } from '@mui/x-internals/store'; -import { TreeViewState, itemsSelectors, labelSelectors } from '@mui/x-tree-view/internals'; +import { itemsSelectors, labelSelectors } from '@mui/x-tree-view/internals'; import { TreeViewItemId } from '@mui/x-tree-view/models'; -import { UseTreeViewItemsReorderingSignature } from './useTreeViewItemsReordering.types'; +import { RichTreeViewProState } from '../../RichTreeViewProStore'; export const itemsReorderingSelectors = { /** * Gets the properties of the current reordering. */ - currentReorder: createSelector( - (state: TreeViewState<[UseTreeViewItemsReorderingSignature]>) => - state.itemsReordering.currentReorder, - ), + currentReorder: createSelector((state: RichTreeViewProState) => state.currentReorder), /** * Gets the properties of the dragged item. */ draggedItemProperties: createSelector( - (state: TreeViewState<[UseTreeViewItemsReorderingSignature]>) => - state.itemsReordering.currentReorder, + (state: RichTreeViewProState) => state.currentReorder, itemsSelectors.itemMetaLookup, (currentReorder, itemMetaLookup, itemId: TreeViewItemId) => { if ( @@ -44,8 +40,8 @@ export const itemsReorderingSelectors = { * Checks whether an item is a valid target for the dragged item. */ isItemValidDropTarget: createSelector( - (state: TreeViewState<[UseTreeViewItemsReorderingSignature]>, itemId: TreeViewItemId) => { - const draggedItemId = state.itemsReordering.currentReorder?.draggedItemId; + (state: RichTreeViewProState, itemId: TreeViewItemId) => { + const draggedItemId = state.currentReorder?.draggedItemId; return draggedItemId != null && draggedItemId !== itemId; }, ), @@ -53,8 +49,7 @@ export const itemsReorderingSelectors = { * Checks whether an item can be reordered. */ canItemBeReordered: createSelector( - (state: TreeViewState<[UseTreeViewItemsReorderingSignature]>) => - state.itemsReordering.isItemReorderable, + (state: RichTreeViewProState) => state.isItemReorderable, labelSelectors.isAnyItemBeingEdited, (isItemReorderable, isEditing, itemId: TreeViewItemId) => !isEditing && isItemReorderable(itemId), diff --git a/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/types.ts b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/types.ts new file mode 100644 index 0000000000000..3e052e6e0732f --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/types.ts @@ -0,0 +1,10 @@ +import { TreeViewItemsReorderingAction } from '@mui/x-tree-view/models'; + +export interface TreeViewItemReorderPosition { + parentId: string | null; + index: number; +} + +export type TreeViewItemItemReorderingValidActions = { + [key in TreeViewItemsReorderingAction]?: TreeViewItemReorderPosition; +}; diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/utils.ts similarity index 92% rename from packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts rename to packages/x-tree-view-pro/src/internals/plugins/itemsReordering/utils.ts index 90ffc846f9e37..3a8fd45ac0a64 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/itemsReordering/utils.ts @@ -1,22 +1,21 @@ import { - TreeViewUsedStore, - UseTreeViewItemsState, buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID, itemsSelectors, } from '@mui/x-tree-view/internals'; -import { TreeViewItemId, TreeViewItemsReorderingAction } from '@mui/x-tree-view/models'; import { - TreeViewItemItemReorderingValidActions, - TreeViewItemReorderPosition, - UseTreeViewItemsReorderingSignature, -} from './useTreeViewItemsReordering.types'; + TreeViewItemId, + TreeViewItemsReorderingAction, + TreeViewValidItem, +} from '@mui/x-tree-view/models'; +import { TreeViewItemItemReorderingValidActions, TreeViewItemReorderPosition } from './types'; +import { RichTreeViewProState, RichTreeViewProStore } from '../../RichTreeViewProStore'; /** * Checks if the item with the id itemIdB is an ancestor of the item with the id itemIdA. */ export const isAncestor = ( - store: TreeViewUsedStore, + store: RichTreeViewProStore, itemIdA: string, itemIdB: string, ): boolean => { @@ -122,7 +121,7 @@ export const chooseActionToApply = ({ return action; }; -export const moveItemInTree = ({ +export const moveItemInTree = >({ itemToMoveId, oldPosition, newPosition, @@ -131,8 +130,11 @@ export const moveItemInTree = ({ itemToMoveId: TreeViewItemId; oldPosition: TreeViewItemReorderPosition; newPosition: TreeViewItemReorderPosition; - prevState: UseTreeViewItemsState['items']; -}): UseTreeViewItemsState['items'] => { + prevState: RichTreeViewProState; +}): Pick< + RichTreeViewProState, + 'itemOrderedChildrenIdsLookup' | 'itemChildrenIndexesLookup' | 'itemMetaLookup' +> => { const itemToMoveMeta = prevState.itemMetaLookup[itemToMoveId]; const oldParentId = oldPosition.parentId ?? TREE_VIEW_ROOT_PARENT_ID; @@ -199,7 +201,6 @@ export const moveItemInTree = ({ ); return { - ...prevState, itemOrderedChildrenIdsLookup: itemOrderedChildrenIds, itemChildrenIndexesLookup: itemChildrenIndexes, itemMetaLookup, diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.test.tsx b/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/TreeViewLazyLoadingPlugin.test.tsx similarity index 97% rename from packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.test.tsx rename to packages/x-tree-view-pro/src/internals/plugins/lazyLoading/TreeViewLazyLoadingPlugin.test.tsx index 4136d95043c07..b501edffda0a3 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.test.tsx +++ b/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/TreeViewLazyLoadingPlugin.test.tsx @@ -1,9 +1,6 @@ import { act, fireEvent } from '@mui/internal-test-utils'; import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; - -// TODO #20051: Replace with imported type -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type RichTreeViewProStore = any; +import { RichTreeViewProStore } from '../../RichTreeViewProStore'; interface ItemType { id: string; diff --git a/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/TreeViewLazyLoadingPlugin.ts b/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/TreeViewLazyLoadingPlugin.ts new file mode 100644 index 0000000000000..0639f61639217 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/TreeViewLazyLoadingPlugin.ts @@ -0,0 +1,273 @@ +import { + itemsSelectors, + lazyLoadingSelectors, + TREE_VIEW_ROOT_PARENT_ID, + expansionSelectors, + selectionSelectors, + TreeViewEventParameters, + TreeViewEventEvent, +} from '@mui/x-tree-view/internals'; +import { TreeViewItemId } from '@mui/x-tree-view/models'; +import { DataSourceCache, DataSourceCacheDefault } from '@mui/x-tree-view/utils'; +import { RichTreeViewProStore } from '../../RichTreeViewProStore/RichTreeViewProStore'; +import { NestedDataManager } from './utils'; +import { DataSource } from './types'; + +export const TREE_VIEW_LAZY_LOADED_ITEMS_INITIAL_STATE = { + loading: {}, + errors: {}, +}; + +export class TreeViewLazyLoadingPlugin { + private store: RichTreeViewProStore; + + private nestedDataManager = new NestedDataManager(this); + + private cache: DataSourceCache; + + constructor(store: RichTreeViewProStore) { + this.store = store; + this.cache = store.parameters.dataSourceCache ?? new DataSourceCacheDefault({}); + + if (store.parameters.dataSource != null) { + this.init(); + store.subscribeEvent('beforeItemToggleExpansion', this.handleBeforeItemToggleExpansion); + } + } + + private init = () => { + const store = this.store; + // eslint-disable-next-line consistent-this + const plugin = this; + + const fetchAllExpandedItems = async () => { + async function fetchChildrenIfExpanded(parentIds: TreeViewItemId[]) { + const expandedItems = parentIds.filter((id) => + expansionSelectors.isItemExpanded(store.state, id), + ); + if (expandedItems.length > 0) { + const itemsToLazyLoad = expandedItems.filter( + (id) => itemsSelectors.itemOrderedChildrenIds(store.state, id).length === 0, + ); + if (itemsToLazyLoad.length > 0) { + await plugin.fetchItems(itemsToLazyLoad); + } + const childrenIds = expandedItems.flatMap((id) => + itemsSelectors.itemOrderedChildrenIds(store.state, id), + ); + await fetchChildrenIfExpanded(childrenIds); + } + } + + if (store.parameters.items.length) { + const newlyExpandableItems = getExpandableItemsFromDataSource( + store, + store.parameters.dataSource!, + ); + + if (newlyExpandableItems.length > 0) { + store.expansion.addExpandableItems(newlyExpandableItems); + } + } else { + await plugin.fetchItemChildren({ itemId: null }); + } + await fetchChildrenIfExpanded(itemsSelectors.itemOrderedChildrenIds(store.state, null)); + }; + + fetchAllExpandedItems(); + }; + + private handleBeforeItemToggleExpansion = async ( + eventParameters: TreeViewEventParameters<'beforeItemToggleExpansion'>, + event: TreeViewEventEvent<'beforeItemToggleExpansion'>, + ) => { + if (!this.store.parameters.dataSource || !eventParameters.shouldBeExpanded) { + return; + } + + // prevent the default expansion behavior + eventParameters.isExpansionPrevented = true; + await this.fetchItems([eventParameters.itemId]); + const hasError = lazyLoadingSelectors.itemHasError(this.store.state, eventParameters.itemId); + if (!hasError) { + this.store.expansion.applyItemExpansion({ + itemId: eventParameters.itemId, + shouldBeExpanded: true, + event, + }); + if (selectionSelectors.isItemSelected(this.store.state, eventParameters.itemId)) { + // make sure selection propagation works correctly + this.store.selection.setItemSelection({ + event, + itemId: eventParameters.itemId, + keepExistingSelection: true, + shouldBeSelected: true, + }); + } + } + }; + + private setItemLoading = (itemId: TreeViewItemId | null, isLoading: boolean) => { + if (!this.store.parameters.dataSource || !this.store.state.lazyLoadedItems) { + return; + } + + if (lazyLoadingSelectors.isItemLoading(this.store.state, itemId) === isLoading) { + return; + } + + const itemIdWithDefault = itemId ?? TREE_VIEW_ROOT_PARENT_ID; + const loading = { ...this.store.state.lazyLoadedItems.loading }; + if (isLoading === false) { + delete loading[itemIdWithDefault]; + } else { + loading[itemIdWithDefault] = isLoading; + } + + this.store.set('lazyLoadedItems', { ...this.store.state.lazyLoadedItems, loading }); + }; + + private setItemError = (itemId: TreeViewItemId | null, error: Error | null) => { + if (!this.store.parameters.dataSource || !this.store.state.lazyLoadedItems) { + return; + } + + if (lazyLoadingSelectors.itemError(this.store.state, itemId) === error) { + return; + } + + const itemIdWithDefault = itemId ?? TREE_VIEW_ROOT_PARENT_ID; + const errors = { ...this.store.state.lazyLoadedItems.errors }; + if (error === null && errors[itemIdWithDefault] !== undefined) { + delete errors[itemIdWithDefault]; + } else { + errors[itemIdWithDefault] = error; + } + + this.store.set('lazyLoadedItems', { ...this.store.state.lazyLoadedItems, errors }); + }; + + public buildPublicAPI = () => { + return { + updateItemChildren: this.updateItemChildren, + }; + }; + + /** + * Method used for fetching multiple items concurrently. + * Only relevant for lazy-loaded tree views. + * + * @param {TreeViewItemId[]} parentIds The ids of the items to fetch the children of. + * @returns {Promise} The promise resolved when the items are fetched. + */ + public fetchItems = (parentIds: TreeViewItemId[]) => this.nestedDataManager.queue(parentIds); + + /** + * Method used for updating an item's children. + * Only relevant for lazy-loaded tree views. + * + * @param {TreeViewItemId} itemId The The id of the item to update the children of. + * @returns {Promise} The promise resolved when the items are fetched. + */ + public updateItemChildren = (itemId: TreeViewItemId) => + this.fetchItemChildren({ itemId, forceRefresh: true }); + + /** + * Method used for fetching an item's children. + * Only relevant for lazy-loaded tree views. + * + * @param {object} parameters The parameters of the method. + * @param {TreeViewItemId} parameters.itemId The The id of the item to fetch the children of. + * @param {boolean} [parameters.forceRefresh] Whether to force a refresh of the children when the cache already contains some data. + * @returns {Promise} The promise resolved when the items are fetched. + */ + public fetchItemChildren = async ({ + itemId, + forceRefresh, + }: { + itemId: TreeViewItemId | null; + forceRefresh?: boolean; + }) => { + if (!this.store.parameters.dataSource) { + return; + } + const { getChildrenCount, getTreeItems } = this.store.parameters.dataSource; + // clear the request if the item is not in the tree + if (itemId != null && !itemsSelectors.itemMeta(this.store.state, itemId)) { + this.nestedDataManager.clearPendingRequest(itemId); + return; + } + + // reset the state if we are fetching the root items + if (itemId == null && !lazyLoadingSelectors.isEmpty(this.store.state)) { + this.store.set('lazyLoadedItems', TREE_VIEW_LAZY_LOADED_ITEMS_INITIAL_STATE); + } + + const cacheKey = itemId ?? TREE_VIEW_ROOT_PARENT_ID; + + if (!forceRefresh) { + // reads from the value from the cache + const cachedData = this.cache.get(cacheKey); + if (cachedData !== undefined && cachedData !== -1) { + if (itemId != null) { + this.nestedDataManager.setRequestSettled(itemId); + } + this.store.items.setItemChildren({ items: cachedData, parentId: itemId, getChildrenCount }); + this.setItemLoading(itemId, false); + return; + } + + // set the item loading status to true + this.setItemLoading(itemId, true); + + if (cachedData === -1) { + this.store.items.removeChildren(itemId); + } + } + + // reset existing error if any + if (lazyLoadingSelectors.itemError(this.store.state, itemId)) { + this.setItemError(itemId, null); + } + + try { + let response: any[]; + if (itemId == null) { + response = await getTreeItems(); + } else { + response = await getTreeItems(itemId); + this.nestedDataManager.setRequestSettled(itemId); + } + // save the response in the cache + this.cache.set(cacheKey, response); + // update the items in the state + this.store.items.setItemChildren({ items: response, parentId: itemId, getChildrenCount }); + } catch (error) { + const childrenFetchError = error as Error; + // set the item error in the state + this.setItemError(itemId, childrenFetchError); + if (forceRefresh) { + this.store.items.removeChildren(itemId); + } + } finally { + // set the item loading status to false + this.setItemLoading(itemId, false); + if (itemId != null) { + this.nestedDataManager.setRequestSettled(itemId); + } + } + }; +} + +function getExpandableItemsFromDataSource( + store: RichTreeViewProStore, + dataSource: DataSource, +): TreeViewItemId[] { + return Object.values(store.state.itemMetaLookup) + .filter( + (itemMeta) => + !itemMeta.expandable && + dataSource.getChildrenCount(store.state.itemModelLookup[itemMeta.id]) > 0, + ) + .map((item) => item.id); +} diff --git a/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/index.ts b/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/index.ts new file mode 100644 index 0000000000000..5063b63bc7edf --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/index.ts @@ -0,0 +1,2 @@ +export * from './TreeViewLazyLoadingPlugin'; +export * from './types'; diff --git a/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/types.ts b/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/types.ts new file mode 100644 index 0000000000000..7c962dbd25f53 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/types.ts @@ -0,0 +1,22 @@ +import { TreeViewItemId } from '@mui/x-tree-view/models'; + +export type DataSource = { + /** + * Used to determine the number of children the item has. + * Only relevant for lazy-loaded trees. + * + * @template R + * @param {R} item The item to check. + * @returns {number} The number of children. + */ + getChildrenCount: (item: R) => number; + /** + * Method used for fetching the items. + * Only relevant for lazy-loaded tree views. + * + * @template R + * @param {TreeViewItemId} parentId The id of the item the children belong to. + * @returns { Promise} The children of the item. + */ + getTreeItems: (parentId?: TreeViewItemId) => Promise; +}; diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/utils.ts b/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/utils.ts similarity index 77% rename from packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/utils.ts rename to packages/x-tree-view-pro/src/internals/plugins/lazyLoading/utils.ts index c8c8064286f20..ddad6c8ba1347 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/utils.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/lazyLoading/utils.ts @@ -1,5 +1,5 @@ -import { TreeViewInstance, UseTreeViewLazyLoadingSignature } from '@mui/x-tree-view/internals'; import { TreeViewItemId } from '@mui/x-tree-view/models'; +import { TreeViewLazyLoadingPlugin } from './TreeViewLazyLoadingPlugin'; const MAX_CONCURRENT_REQUESTS = Infinity; @@ -10,17 +10,6 @@ export enum RequestStatus { UNKNOWN, } -/** - * Plugins that need to be present in the Tree View in order for the `NestedDataManager` class to work correctly. - */ -type NestedDataManagerMinimalPlugins = readonly [UseTreeViewLazyLoadingSignature]; - -/** - * Plugins that the `NestedDataManager` class can use if they are present, but are not required. - */ - -export type NestedDataManagerOptionalPlugins = readonly []; - /** * Fetches row children from the server with option to limit the number of concurrent requests * Determines the status of a request based on the enum `RequestStatus` @@ -33,18 +22,15 @@ export class NestedDataManager { private settledRequests: Set = new Set(); - private instance: TreeViewInstance< - NestedDataManagerMinimalPlugins, - NestedDataManagerOptionalPlugins - >; + private lazyLoadingPlugin: TreeViewLazyLoadingPlugin; private maxConcurrentRequests: number; constructor( - instance: TreeViewInstance<[UseTreeViewLazyLoadingSignature]>, + lazyLoadingPlugin: TreeViewLazyLoadingPlugin, maxConcurrentRequests = MAX_CONCURRENT_REQUESTS, ) { - this.instance = instance; + this.lazyLoadingPlugin = lazyLoadingPlugin; this.maxConcurrentRequests = maxConcurrentRequests; } @@ -66,7 +52,7 @@ export class NestedDataManager { this.queuedRequests.delete(id); this.pendingRequests.add(id); - fetchPromises.push(this.instance.fetchItemChildren({ itemId: id })); + fetchPromises.push(this.lazyLoadingPlugin.fetchItemChildren({ itemId: id })); } await Promise.all(fetchPromises); }; 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 deleted file mode 100644 index e4ad3edbc62fc..0000000000000 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { useTreeViewItemsReordering } from './useTreeViewItemsReordering'; -export type { - UseTreeViewItemsReorderingSignature, - UseTreeViewItemsReorderingParameters, - UseTreeViewItemsReorderingParametersWithDefaults, - TreeViewItemReorderPosition, -} from './useTreeViewItemsReordering.types'; -export { itemsReorderingSelectors } from './useTreeViewItemsReordering.selectors'; 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 deleted file mode 100644 index dac5f09dca5b6..0000000000000 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts +++ /dev/null @@ -1,294 +0,0 @@ -import * as React from 'react'; -import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; -import { TreeViewPlugin, itemsSelectors, labelSelectors } 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 './itemPlugin'; -import { itemsReorderingSelectors } from './useTreeViewItemsReordering.selectors'; - -export const useTreeViewItemsReordering: TreeViewPlugin = ({ - params, - store, -}) => { - 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) => { - const currentReorder = itemsReorderingSelectors.currentReorder(store.state); - if (!currentReorder) { - throw new Error('There is no ongoing reordering.'); - } - - if (itemId === currentReorder.draggedItemId) { - return {}; - } - - const canMoveItemToNewPosition = params.canMoveItemToNewPosition; - const targetItemMeta = itemsSelectors.itemMeta(store.state, itemId)!; - const targetItemIndex = itemsSelectors.itemIndex(store.state, targetItemMeta.id); - const draggedItemMeta = itemsSelectors.itemMeta(store.state, currentReorder.draggedItemId)!; - const draggedItemIndex = itemsSelectors.itemIndex(store.state, draggedItemMeta.id); - const isTargetLastSibling = - targetItemIndex === - itemsSelectors.itemOrderedChildrenIds(store.state, targetItemMeta.parentId).length - 1; - - 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: currentReorder.draggedItemId, - 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 || isTargetLastSibling - ? { - parentId: targetItemMeta.parentId, - index: - targetItemMeta.parentId === draggedItemMeta.parentId && - targetItemIndex > draggedItemIndex - ? targetItemIndex - : targetItemIndex + 1, - } - : null, - 'move-to-parent': - targetItemMeta.parentId == null - ? null - : { - parentId: targetItemMeta.parentId, - index: itemsSelectors.itemOrderedChildrenIds(store.state, 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; - }, - [store, params.canMoveItemToNewPosition], - ); - - const startDraggingItem = React.useCallback( - (itemId: string) => { - const isItemBeingEdited = labelSelectors.isItemBeingEdited(store.state, itemId); - if (isItemBeingEdited) { - return; - } - - store.set('itemsReordering', { - ...store.state.itemsReordering, - currentReorder: { - targetItemId: itemId, - draggedItemId: itemId, - action: null, - newPosition: null, - }, - }); - }, - [store], - ); - - const cancelDraggingItem = React.useCallback(() => { - const currentReorder = itemsReorderingSelectors.currentReorder(store.state); - if (currentReorder == null) { - return; - } - - store.set('itemsReordering', { ...store.state.itemsReordering, currentReorder: null }); - }, [store]); - - const completeDraggingItem = React.useCallback( - (itemId: string) => { - const currentReorder = itemsReorderingSelectors.currentReorder(store.state); - if (currentReorder == null || currentReorder.draggedItemId !== itemId) { - return; - } - - if ( - currentReorder.draggedItemId === currentReorder.targetItemId || - currentReorder.action == null || - currentReorder.newPosition == null - ) { - store.set('itemsReordering', { ...store.state.itemsReordering, currentReorder: null }); - return; - } - - const draggedItemMeta = itemsSelectors.itemMeta(store.state, currentReorder.draggedItemId)!; - - const oldPosition: TreeViewItemReorderPosition = { - parentId: draggedItemMeta.parentId, - index: itemsSelectors.itemIndex(store.state, draggedItemMeta.id), - }; - - const newPosition = currentReorder.newPosition; - - store.update({ - itemsReordering: { - ...store.state.itemsReordering, - currentReorder: null, - }, - items: moveItemInTree({ - itemToMoveId: itemId, - newPosition, - oldPosition, - prevState: store.state.items, - }), - }); - - const onItemPositionChange = params.onItemPositionChange; - onItemPositionChange?.({ - itemId, - newPosition, - oldPosition, - }); - }, - [store, params.onItemPositionChange], - ); - - const setDragTargetItem = React.useCallback< - UseTreeViewItemsReorderingInstance['setDragTargetItem'] - >( - ({ itemId, validActions, targetHeight, cursorY, cursorX, contentElement }) => { - const prevItemReorder = store.state.itemsReordering.currentReorder; - if (prevItemReorder == null || isAncestor(store, itemId, prevItemReorder.draggedItemId)) { - return; - } - - const action = chooseActionToApply({ - itemChildrenIndentation: params.itemChildrenIndentation, - validActions, - targetHeight, - targetDepth: store.state.items.itemMetaLookup[itemId].depth!, - cursorY, - cursorX, - contentElement, - }); - - const newPosition = action == null ? null : validActions[action]!; - - if ( - prevItemReorder.targetItemId === itemId && - prevItemReorder.action === action && - prevItemReorder.newPosition?.parentId === newPosition?.parentId && - prevItemReorder.newPosition?.index === newPosition?.index - ) { - return; - } - - store.set('itemsReordering', { - ...store.state.itemsReordering, - currentReorder: { - ...prevItemReorder, - targetItemId: itemId, - newPosition, - action, - }, - }); - }, - [store, params.itemChildrenIndentation], - ); - - useIsoLayoutEffect(() => { - store.set('itemsReordering', { - ...store.state.itemsReordering, - isItemReorderable: params.itemsReordering - ? (params.isItemReorderable ?? (() => true)) - : () => false, - }); - }, [store, params.itemsReordering, params.isItemReorderable]); - - return { - instance: { - canItemBeDragged, - getDroppingTargetValidActions, - startDraggingItem, - cancelDraggingItem, - completeDraggingItem, - setDragTargetItem, - }, - }; -}; - -useTreeViewItemsReordering.itemPlugin = useTreeViewItemsReorderingItemPlugin; - -useTreeViewItemsReordering.applyDefaultValuesToParams = ({ params }) => ({ - ...params, - itemsReordering: params.itemsReordering ?? false, -}); - -useTreeViewItemsReordering.getInitialState = (params) => ({ - itemsReordering: { - currentReorder: null, - isItemReorderable: params.itemsReordering - ? (params.isItemReorderable ?? (() => true)) - : () => false, - }, -}); - -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 deleted file mode 100644 index 66159dd310de0..0000000000000 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.types.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { DefaultizedProps } from '@mui/x-internals/types'; -import { TreeViewPluginSignature, UseTreeViewItemsSignature } from '@mui/x-tree-view/internals'; -import { TreeViewItemId, TreeViewItemsReorderingAction } from '@mui/x-tree-view/models'; - -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 the 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; - /** - * Complete the reordering of a given item. - * @param {TreeViewItemId} itemId The id of the item to complete the reordering for. - */ - completeDraggingItem: (itemId: TreeViewItemId) => void; - /** - * Cancel the current reordering operation and reset the state. - */ - cancelDraggingItem: () => 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: TreeViewItemId | 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; - /** - * Determine if a given item can be reordered. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean} `true` if the item can be reordered. - * @default () => true - */ - isItemReorderable?: (itemId: TreeViewItemId) => boolean; - /** - * Used to determine if a given item can move to some new position. - * @param {object} parameters The params describing the item re-ordering. - * @param {TreeViewItemId} parameters.itemId The id of the item that is being moved to a new position. - * @param {TreeViewItemReorderPosition} parameters.oldPosition The old position of the item. - * @param {TreeViewItemReorderPosition} parameters.newPosition The new position of the item. - * @returns {boolean} `true` if the item can move to the new position. - */ - canMoveItemToNewPosition?: (parameters: { - itemId: TreeViewItemId; - oldPosition: TreeViewItemReorderPosition; - newPosition: TreeViewItemReorderPosition; - }) => boolean; - /** - * Callback fired when a Tree Item is moved in the tree. - * @param {object} parameters The params describing the item re-ordering. - * @param {TreeViewItemId} parameters.itemId The id of the item moved. - * @param {TreeViewItemReorderPosition} parameters.oldPosition The old position of the item. - * @param {TreeViewItemReorderPosition} parameters.newPosition The new position of the item. - */ - onItemPositionChange?: (parameters: { - itemId: TreeViewItemId; - oldPosition: TreeViewItemReorderPosition; - newPosition: TreeViewItemReorderPosition; - }) => void; -} - -export type UseTreeViewItemsReorderingParametersWithDefaults = DefaultizedProps< - UseTreeViewItemsReorderingParameters, - 'itemsReordering' ->; - -export interface UseTreeViewItemsReorderingState { - itemsReordering: { - isItemReorderable: (itemId: TreeViewItemId) => boolean; - currentReorder: { - draggedItemId: TreeViewItemId; - targetItemId: TreeViewItemId; - newPosition: TreeViewItemReorderPosition | null; - action: TreeViewItemsReorderingAction | null; - } | null; - }; -} - -export type UseTreeViewItemsReorderingSignature = TreeViewPluginSignature<{ - params: UseTreeViewItemsReorderingParameters; - paramsWithDefaults: UseTreeViewItemsReorderingParametersWithDefaults; - instance: UseTreeViewItemsReorderingInstance; - state: UseTreeViewItemsReorderingState; - dependencies: [UseTreeViewItemsSignature]; -}>; diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/index.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/index.ts deleted file mode 100644 index f14551c31054c..0000000000000 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useTreeViewLazyLoading } from './useTreeViewLazyLoading'; diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.ts deleted file mode 100644 index d73ad95ed8b09..0000000000000 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.ts +++ /dev/null @@ -1,298 +0,0 @@ -'use client'; -import * as React from 'react'; -import { useRefWithInit } from '@base-ui-components/utils/useRefWithInit'; -import { useEventCallback } from '@base-ui-components/utils/useEventCallback'; -import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; -import { - itemsSelectors, - expansionSelectors, - selectionSelectors, - lazyLoadingSelectors, - TreeViewPlugin, - UseTreeViewLazyLoadingInstance, - useInstanceEventHandler, - TREE_VIEW_ROOT_PARENT_ID, - TreeViewUsedStore, - TreeViewUsedInstance, - UseTreeViewLazyLoadingSignature, - TreeViewUsedParamsWithDefaults, - DataSource, -} from '@mui/x-tree-view/internals'; -import { TreeViewItemId } from '@mui/x-tree-view/models'; -import { DataSourceCacheDefault } from '@mui/x-tree-view/utils'; -import { NestedDataManager } from './utils'; - -const INITIAL_STATE = { - loading: {}, - errors: {}, -}; - -export const useTreeViewLazyLoading: TreeViewPlugin = ({ - instance, - params, - store, -}) => { - const nestedDataManager = useRefWithInit(() => new NestedDataManager(instance)).current; - const cache = useRefWithInit( - () => params.dataSourceCache ?? new DataSourceCacheDefault({}), - ).current; - - const setDataSourceLoading: UseTreeViewLazyLoadingInstance['setDataSourceLoading'] = - useEventCallback((itemId, isLoading) => { - if (!params.dataSource) { - return; - } - - const itemIdWithDefault = itemId ?? TREE_VIEW_ROOT_PARENT_ID; - if (lazyLoadingSelectors.isItemLoading(store.state, itemIdWithDefault) === isLoading) { - return; - } - - const loading = { ...store.state.lazyLoading.dataSource.loading }; - if (isLoading === false) { - delete loading[itemIdWithDefault]; - } else { - loading[itemIdWithDefault] = isLoading; - } - - store.set('lazyLoading', { - ...store.state.lazyLoading, - dataSource: { ...store.state.lazyLoading.dataSource, loading }, - }); - }); - - const setDataSourceError: UseTreeViewLazyLoadingInstance['setDataSourceError'] = useEventCallback( - (itemId, error) => { - if (!params.dataSource) { - return; - } - - if (lazyLoadingSelectors.itemError(store.state, itemId) === error) { - return; - } - - const stateId = itemId ?? TREE_VIEW_ROOT_PARENT_ID; - const errors = { ...store.state.lazyLoading.dataSource.errors }; - if (error === null && errors[stateId] !== undefined) { - delete errors[stateId]; - } else { - errors[stateId] = error; - } - - store.set('lazyLoading', { - ...store.state.lazyLoading, - dataSource: { ...store.state.lazyLoading.dataSource, errors }, - }); - }, - ); - - const fetchItems: UseTreeViewLazyLoadingInstance['fetchItems'] = useEventCallback( - async (parentIds) => nestedDataManager.queue(parentIds), - ); - - const fetchItemChildren: UseTreeViewLazyLoadingInstance['fetchItemChildren'] = useEventCallback( - async ({ itemId, forceRefresh }) => { - if (!params.dataSource) { - return; - } - const { getChildrenCount, getTreeItems } = params.dataSource; - // clear the request if the item is not in the tree - if (itemId != null && !itemsSelectors.itemMeta(store.state, itemId)) { - nestedDataManager.clearPendingRequest(itemId); - return; - } - - // reset the state if we are fetching the root items - if (itemId == null && lazyLoadingSelectors.dataSource(store.state) !== INITIAL_STATE) { - store.set('lazyLoading', { - ...store.state.lazyLoading, - dataSource: INITIAL_STATE, - }); - } - - const cacheKey = itemId ?? TREE_VIEW_ROOT_PARENT_ID; - - if (!forceRefresh) { - // reads from the value from the cache - const cachedData = cache.get(cacheKey); - if (cachedData !== undefined && cachedData !== -1) { - if (itemId != null) { - nestedDataManager.setRequestSettled(itemId); - } - instance.setItemChildren({ items: cachedData, parentId: itemId, getChildrenCount }); - instance.setDataSourceLoading(itemId, false); - return; - } - - // set the item loading status to true - instance.setDataSourceLoading(itemId, true); - - if (cachedData === -1) { - instance.removeChildren(itemId); - } - } - - // reset existing error if any - if (lazyLoadingSelectors.itemError(store.state, itemId)) { - instance.setDataSourceError(itemId, null); - } - - try { - let response: any[]; - if (itemId == null) { - response = await getTreeItems(); - } else { - response = await getTreeItems(itemId); - nestedDataManager.setRequestSettled(itemId); - } - // save the response in the cache - cache.set(cacheKey, response); - // update the items in the state - instance.setItemChildren({ items: response, parentId: itemId, getChildrenCount }); - } catch (error) { - const childrenFetchError = error as Error; - // set the item error in the state - instance.setDataSourceError(itemId, childrenFetchError); - if (forceRefresh) { - instance.removeChildren(itemId); - } - } finally { - // set the item loading status to false - instance.setDataSourceLoading(itemId, false); - if (itemId != null) { - nestedDataManager.setRequestSettled(itemId); - } - } - }, - ); - - const updateItemChildren: UseTreeViewLazyLoadingInstance['updateItemChildren'] = useEventCallback( - (itemId) => { - return instance.fetchItemChildren({ itemId, forceRefresh: true }); - }, - ); - - useInstanceEventHandler(instance, 'beforeItemToggleExpansion', async (eventParameters) => { - if (!params.dataSource || !eventParameters.shouldBeExpanded) { - return; - } - - // prevent the default expansion behavior - eventParameters.isExpansionPrevented = true; - await instance.fetchItems([eventParameters.itemId]); - const hasError = lazyLoadingSelectors.itemHasError(store.state, eventParameters.itemId); - if (!hasError) { - instance.applyItemExpansion({ - itemId: eventParameters.itemId, - shouldBeExpanded: true, - event: eventParameters.event, - }); - if (selectionSelectors.isItemSelected(store.state, eventParameters.itemId)) { - // make sure selection propagation works correctly - instance.setItemSelection({ - event: eventParameters.event as React.SyntheticEvent, - itemId: eventParameters.itemId, - keepExistingSelection: true, - shouldBeSelected: true, - }); - } - } - }); - - useLazyLoadOnMount({ instance, params, store }); - - if (params.dataSource) { - instance.preventItemUpdates(); - } - - return { - instance: { - fetchItemChildren, - fetchItems, - updateItemChildren, - setDataSourceLoading, - setDataSourceError, - }, - publicAPI: { - updateItemChildren, - }, - }; -}; - -useTreeViewLazyLoading.getInitialState = () => ({ - lazyLoading: { - enabled: false, - dataSource: INITIAL_STATE, - }, -}); - -useTreeViewLazyLoading.params = { - dataSource: true, - dataSourceCache: true, -}; - -function useLazyLoadOnMount({ - instance, - params, - store, -}: { - instance: TreeViewUsedInstance; - params: TreeViewUsedParamsWithDefaults; - store: TreeViewUsedStore; -}) { - const firstRenderRef = React.useRef(true); - useEnhancedEffect(() => { - if (!params.dataSource || !firstRenderRef.current) { - return; - } - - firstRenderRef.current = false; - store.set('lazyLoading', { ...store.state.lazyLoading, enabled: true }); - - async function fetchAllExpandedItems() { - async function fetchChildrenIfExpanded(parentIds: TreeViewItemId[]) { - const expandedItems = parentIds.filter((id) => - expansionSelectors.isItemExpanded(store.state, id), - ); - if (expandedItems.length > 0) { - const itemsToLazyLoad = expandedItems.filter( - (id) => itemsSelectors.itemOrderedChildrenIds(store.state, id).length === 0, - ); - if (itemsToLazyLoad.length > 0) { - await instance.fetchItems(itemsToLazyLoad); - } - const childrenIds = expandedItems.flatMap((id) => - itemsSelectors.itemOrderedChildrenIds(store.state, id), - ); - await fetchChildrenIfExpanded(childrenIds); - } - } - - if (params.items.length) { - const newlyExpandableItems = getExpandableItemsFromDataSource(store, params.dataSource); - - if (newlyExpandableItems.length > 0) { - instance.addExpandableItems(newlyExpandableItems); - } - } else { - await instance.fetchItemChildren({ itemId: null }); - } - await fetchChildrenIfExpanded(itemsSelectors.itemOrderedChildrenIds(store.state, null)); - } - - fetchAllExpandedItems(); - }, [instance, params.items, params.dataSource, store]); -} - -function getExpandableItemsFromDataSource( - store: TreeViewUsedStore, - dataSource: DataSource, -): TreeViewItemId[] { - return Object.values(store.state.items.itemMetaLookup) - .filter( - (itemMeta) => - !itemMeta.expandable && - dataSource.getChildrenCount(store.state.items.itemModelLookup[itemMeta.id]) > 0, - ) - .map((item) => item.id); -} diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.plugins.ts b/packages/x-tree-view/src/RichTreeView/RichTreeView.plugins.ts deleted file mode 100644 index d07f8ae91f3e2..0000000000000 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.plugins.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { TreeViewCorePluginParameters } from '../internals/corePlugins'; -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 { ConvertPluginsIntoSignatures } from '../internals/models'; -import { - useTreeViewLabel, - UseTreeViewLabelParameters, -} from '../internals/plugins/useTreeViewLabel'; - -export const RICH_TREE_VIEW_PLUGINS = [ - useTreeViewItems, - useTreeViewExpansion, - useTreeViewSelection, - useTreeViewFocus, - useTreeViewKeyboardNavigation, - useTreeViewLabel, -] as const; - -export type RichTreeViewPluginSignatures = ConvertPluginsIntoSignatures< - typeof RICH_TREE_VIEW_PLUGINS ->; - -// We can't infer this type from the plugin, otherwise we would lose the generics. -export interface RichTreeViewPluginParameters - extends TreeViewCorePluginParameters, - UseTreeViewItemsParameters, - UseTreeViewExpansionParameters, - UseTreeViewFocusParameters, - UseTreeViewSelectionParameters, - UseTreeViewLabelParameters {} diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx index b4858231dba88..31e207b76f531 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx @@ -1,6 +1,7 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs'; import { useStore } from '@mui/x-internals/store'; import Alert from '@mui/material/Alert'; import Typography from '@mui/material/Typography'; @@ -10,11 +11,16 @@ import { warnOnce } from '@mui/x-internals/warning'; import { getRichTreeViewUtilityClass } from './richTreeViewClasses'; import { RichTreeViewProps } from './RichTreeView.types'; import { styled, createUseThemeProps } from '../internals/zero-styled'; -import { useTreeView } from '../internals/useTreeView'; import { TreeViewProvider } from '../internals/TreeViewProvider'; -import { RICH_TREE_VIEW_PLUGINS, RichTreeViewPluginSignatures } from './RichTreeView.plugins'; import { RichTreeViewItems } from '../internals/components/RichTreeViewItems'; -import { lazyLoadingSelectors } from '../internals/plugins/useTreeViewLazyLoading'; +import { lazyLoadingSelectors } from '../internals/plugins/lazyLoading'; +import { TreeViewValidItem } from '../models'; +import { useTreeViewRootProps } from '../internals/hooks/useTreeViewRootProps'; +import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext'; +import { useExtractRichTreeViewParameters } from './useExtractRichTreeViewParameters'; +import { itemsSelectors } from '../internals/plugins/items'; +import { useTreeViewStore } from '../internals/hooks/useTreeViewStore'; +import { RichTreeViewStore } from '../internals/RichTreeViewStore'; const useThemeProps = createUseThemeProps('MuiRichTreeView'); @@ -67,12 +73,10 @@ type RichTreeViewComponent = (, Multiple extends boolean | undefined = undefined, ->(inProps: RichTreeViewProps, ref: React.Ref) { +>(inProps: RichTreeViewProps, forwardedRef: React.Ref) { const props = useThemeProps({ props: inProps, name: 'MuiRichTreeView' }); - const { slots, slotProps, ...other } = props; - if (process.env.NODE_ENV !== 'production') { if ((props as any).children != null) { warnOnce([ @@ -83,16 +87,18 @@ const RichTreeView = React.forwardRef(function RichTreeView< } } - const { getRootProps, contextValue } = useTreeView({ - plugins: RICH_TREE_VIEW_PLUGINS, - rootRef: ref, - props: other, - }); - const isLoading = useStore(contextValue.store, lazyLoadingSelectors.isItemLoading, null); - const error = useStore(contextValue.store, lazyLoadingSelectors.itemError, null); + const { slots, slotProps, apiRef, parameters, forwardedProps } = + useExtractRichTreeViewParameters(props); + const store = useTreeViewStore(RichTreeViewStore, parameters); + const ref = React.useRef(null); + const handleRef = useMergedRefs(forwardedRef, ref); + const getRootProps = useTreeViewRootProps(store, forwardedProps, handleRef); const classes = useUtilityClasses(props); + const isLoading = useStore(store, lazyLoadingSelectors.isItemLoading, null); + const error = useStore(store, lazyLoadingSelectors.itemError, null); + const Root = slots?.root ?? RichTreeViewRoot; const rootProps = useSlotProps({ elementType: Root, @@ -112,14 +118,18 @@ const RichTreeView = React.forwardRef(function RichTreeView< return ( - - - + + + + + ); }) as RichTreeViewComponent; @@ -130,7 +140,7 @@ RichTreeView.propTypes = { // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- /** - * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`. + * The ref object that allows Tree View manipulation. Can be instantiated with `useRichTreeViewApiRef()`. */ apiRef: PropTypes.shape({ current: PropTypes.shape({ diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts b/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts index 4e083d5202aad..dbcac4c489311 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts @@ -3,8 +3,6 @@ import { Theme } from '@mui/material/styles'; import { SxProps } from '@mui/system/styleFunctionSx'; import { SlotComponentProps } from '@mui/utils/types'; import { RichTreeViewClasses } from './richTreeViewClasses'; -import { RichTreeViewPluginParameters, RichTreeViewPluginSignatures } from './RichTreeView.plugins'; -import { TreeViewPublicAPI } from '../internals/models'; import { RichTreeViewItemsSlotProps, RichTreeViewItemsSlots, @@ -13,6 +11,10 @@ import { TreeViewSlotProps, TreeViewSlots, } from '../internals/TreeViewProvider/TreeViewStyleContext'; +import { RichTreeViewStore } from '../internals/RichTreeViewStore'; +import { TreeViewValidItem } from '../models/items'; +import { UseTreeViewStoreParameters } from '../internals/hooks/useTreeViewStore'; +import { TreeViewPublicAPI } from '../internals/models'; export interface RichTreeViewSlots extends TreeViewSlots, RichTreeViewItemsSlots { /** @@ -28,9 +30,10 @@ export interface RichTreeViewSlotProps>; } -export type RichTreeViewApiRef = React.RefObject< - Partial> | undefined ->; +export type RichTreeViewApiRef< + R extends TreeViewValidItem = any, + Multiple extends boolean | undefined = any, +> = React.RefObject>> | undefined>; export interface RichTreeViewPropsBase extends React.HTMLAttributes { className?: string; @@ -45,7 +48,7 @@ export interface RichTreeViewPropsBase extends React.HTMLAttributes - extends RichTreeViewPluginParameters, + extends UseTreeViewStoreParameters>, RichTreeViewPropsBase { /** * Overridable component slots. @@ -58,7 +61,7 @@ export interface RichTreeViewProps; /** - * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`. + * The ref object that allows Tree View manipulation. Can be instantiated with `useRichTreeViewApiRef()`. */ apiRef?: RichTreeViewApiRef; } diff --git a/packages/x-tree-view/src/RichTreeView/index.ts b/packages/x-tree-view/src/RichTreeView/index.ts index 500769bc889e6..9611c2b77d923 100644 --- a/packages/x-tree-view/src/RichTreeView/index.ts +++ b/packages/x-tree-view/src/RichTreeView/index.ts @@ -7,6 +7,3 @@ export type { RichTreeViewSlotProps, RichTreeViewApiRef, } from './RichTreeView.types'; - -export { RICH_TREE_VIEW_PLUGINS } from './RichTreeView.plugins'; -export type { RichTreeViewPluginParameters } from './RichTreeView.plugins'; diff --git a/packages/x-tree-view/src/RichTreeView/useExtractRichTreeViewParameters.ts b/packages/x-tree-view/src/RichTreeView/useExtractRichTreeViewParameters.ts new file mode 100644 index 0000000000000..63162a5b85c6a --- /dev/null +++ b/packages/x-tree-view/src/RichTreeView/useExtractRichTreeViewParameters.ts @@ -0,0 +1,120 @@ +import * as React from 'react'; +import { TreeViewValidItem } from '../models'; +import { RichTreeViewProps } from './RichTreeView.types'; +import { UseTreeViewStoreParameters } from '../internals/hooks/useTreeViewStore'; +import { RichTreeViewStore } from '../internals/RichTreeViewStore'; + +export function useExtractRichTreeViewParameters< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +>(props: RichTreeViewProps) { + const { + // Props for Provider + apiRef, + slots, + slotProps, + + // Shared parameters + disabledItemsFocusable, + items, + isItemDisabled, + getItemLabel, + getItemChildren, + getItemId, + onItemClick, + itemChildrenIndentation, + id, + expandedItems, + defaultExpandedItems, + onExpandedItemsChange, + onItemExpansionToggle, + expansionTrigger, + disableSelection, + selectedItems, + defaultSelectedItems, + multiSelect, + checkboxSelection, + selectionPropagation, + onSelectedItemsChange, + onItemSelectionToggle, + onItemFocus, + + // RichTreeViewStore parameters + onItemLabelChange, + isItemEditable, + + // Forwarded props + ...forwardedProps + } = props; + + const parameters: UseTreeViewStoreParameters> = React.useMemo( + () => ({ + // Shared parameters + disabledItemsFocusable, + items, + isItemDisabled, + getItemLabel, + getItemChildren, + getItemId, + onItemClick, + itemChildrenIndentation, + id, + expandedItems, + defaultExpandedItems, + onExpandedItemsChange, + onItemExpansionToggle, + expansionTrigger, + disableSelection, + selectedItems, + defaultSelectedItems, + multiSelect, + checkboxSelection, + selectionPropagation, + onSelectedItemsChange, + onItemSelectionToggle, + onItemFocus, + + // RichTreeViewStore parameters + onItemLabelChange, + isItemEditable, + }), + [ + // Shared parameters + disabledItemsFocusable, + items, + isItemDisabled, + getItemLabel, + getItemChildren, + getItemId, + onItemClick, + itemChildrenIndentation, + id, + expandedItems, + defaultExpandedItems, + onExpandedItemsChange, + onItemExpansionToggle, + expansionTrigger, + disableSelection, + selectedItems, + defaultSelectedItems, + multiSelect, + checkboxSelection, + selectionPropagation, + onSelectedItemsChange, + onItemSelectionToggle, + onItemFocus, + + // RichTreeViewStore parameters + onItemLabelChange, + isItemEditable, + ], + ); + + return { + apiRef, + slots, + slotProps, + parameters, + forwardedProps, + }; +} diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.plugins.ts b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.plugins.ts deleted file mode 100644 index d82943525642f..0000000000000 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.plugins.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { TreeViewCorePluginParameters } from '../internals/corePlugins'; -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 { useTreeViewJSXItems } from '../internals/plugins/useTreeViewJSXItems'; -import { ConvertPluginsIntoSignatures } from '../internals/models'; - -export const SIMPLE_TREE_VIEW_PLUGINS = [ - useTreeViewItems, - useTreeViewExpansion, - useTreeViewSelection, - useTreeViewFocus, - useTreeViewKeyboardNavigation, - useTreeViewJSXItems, -] as const; - -export type SimpleTreeViewPluginSignatures = ConvertPluginsIntoSignatures< - typeof SIMPLE_TREE_VIEW_PLUGINS ->; - -// We can't infer this type from the plugin, otherwise we would lose the generics. -export interface SimpleTreeViewPluginParameters - extends TreeViewCorePluginParameters, - Omit< - UseTreeViewItemsParameters, - 'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemChildren' | 'getItemId' - >, - UseTreeViewExpansionParameters, - UseTreeViewFocusParameters, - UseTreeViewSelectionParameters {} diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx index 98bbae35879ef..5b3bdfd60ac68 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx @@ -1,15 +1,20 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs'; import composeClasses from '@mui/utils/composeClasses'; import useSlotProps from '@mui/utils/useSlotProps'; import { warnOnce } from '@mui/x-internals/warning'; import { styled, createUseThemeProps } from '../internals/zero-styled'; import { getSimpleTreeViewUtilityClass } from './simpleTreeViewClasses'; import { SimpleTreeViewProps } from './SimpleTreeView.types'; -import { useTreeView } from '../internals/useTreeView'; import { TreeViewProvider } from '../internals/TreeViewProvider'; -import { SIMPLE_TREE_VIEW_PLUGINS, SimpleTreeViewPluginSignatures } from './SimpleTreeView.plugins'; +import { useExtractSimpleTreeViewParameters } from './useExtractSimpleTreeViewParameters'; +import { useTreeViewRootProps } from '../internals/hooks/useTreeViewRootProps'; +import { TreeViewChildrenItemProvider } from '../internals/TreeViewProvider/TreeViewChildrenItemProvider'; +import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext'; +import { useTreeViewStore } from '../internals/hooks/useTreeViewStore'; +import { SimpleTreeViewStore } from '../internals/SimpleTreeViewStore'; const useThemeProps = createUseThemeProps('MuiSimpleTreeView'); @@ -51,8 +56,6 @@ type SimpleTreeViewComponent = ( & React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; -const EMPTY_ITEMS: any[] = []; - /** * * Demos: @@ -65,10 +68,8 @@ const EMPTY_ITEMS: any[] = []; */ const SimpleTreeView = React.forwardRef(function SimpleTreeView< Multiple extends boolean | undefined = undefined, ->(inProps: SimpleTreeViewProps, ref: React.Ref) { +>(inProps: SimpleTreeViewProps, forwardedRef: React.Ref) { const props = useThemeProps({ props: inProps, name: 'MuiSimpleTreeView' }); - const { slots, slotProps, ...other } = props; - if (process.env.NODE_ENV !== 'production') { if ((props as any).items != null) { warnOnce([ @@ -79,15 +80,13 @@ const SimpleTreeView = React.forwardRef(function SimpleTreeView< } } - const { getRootProps, contextValue } = useTreeView< - SimpleTreeViewPluginSignatures, - typeof props & { items: any[] } - >({ - plugins: SIMPLE_TREE_VIEW_PLUGINS, - rootRef: ref, - props: { ...other, items: EMPTY_ITEMS }, - }); + const { slots, slotProps, apiRef, parameters, forwardedProps } = + useExtractSimpleTreeViewParameters(props); + const store = useTreeViewStore(SimpleTreeViewStore, parameters); + const ref = React.useRef(null); + const handleRef = useMergedRefs(forwardedRef, ref); + const getRootProps = useTreeViewRootProps(store, forwardedProps, handleRef); const classes = useUtilityClasses(props); const Root = slots?.root ?? SimpleTreeViewRoot; @@ -101,12 +100,18 @@ const SimpleTreeView = React.forwardRef(function SimpleTreeView< return ( - + + + + + ); }) as SimpleTreeViewComponent; @@ -117,7 +122,7 @@ SimpleTreeView.propTypes = { // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- /** - * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`. + * The ref object that allows Tree View manipulation. Can be instantiated with `useSimpleTreeViewApiRef()`. */ apiRef: PropTypes.shape({ current: PropTypes.shape({ diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts index a3a6bf0ffa3e0..2923695543938 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts @@ -3,15 +3,13 @@ import { Theme } from '@mui/material/styles'; import { SlotComponentProps } from '@mui/utils/types'; import { SxProps } from '@mui/system/styleFunctionSx'; import { SimpleTreeViewClasses } from './simpleTreeViewClasses'; -import { - SimpleTreeViewPluginParameters, - SimpleTreeViewPluginSignatures, -} from './SimpleTreeView.plugins'; -import { TreeViewPublicAPI } from '../internals/models'; import { TreeViewSlotProps, TreeViewSlots, } from '../internals/TreeViewProvider/TreeViewStyleContext'; +import { SimpleTreeViewStore } from '../internals/SimpleTreeViewStore'; +import { UseTreeViewStoreParameters } from '../internals/hooks/useTreeViewStore'; +import { TreeViewPublicAPI } from '../internals/models'; export interface SimpleTreeViewSlots extends TreeViewSlots { /** @@ -25,12 +23,12 @@ export interface SimpleTreeViewSlotProps extends TreeViewSlotProps { root?: SlotComponentProps<'ul', {}, {}>; } -export type SimpleTreeViewApiRef = React.RefObject< - Partial> | undefined +export type SimpleTreeViewApiRef = React.RefObject< + Partial>> | undefined >; export interface SimpleTreeViewProps - extends SimpleTreeViewPluginParameters, + extends UseTreeViewStoreParameters>, React.HTMLAttributes { /** * The content of the component. @@ -54,7 +52,7 @@ export interface SimpleTreeViewProps */ sx?: SxProps; /** - * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`. + * The ref object that allows Tree View manipulation. Can be instantiated with `useSimpleTreeViewApiRef()`. */ apiRef?: SimpleTreeViewApiRef; } diff --git a/packages/x-tree-view/src/SimpleTreeView/useExtractSimpleTreeViewParameters.ts b/packages/x-tree-view/src/SimpleTreeView/useExtractSimpleTreeViewParameters.ts new file mode 100644 index 0000000000000..2bd237d8e0dc3 --- /dev/null +++ b/packages/x-tree-view/src/SimpleTreeView/useExtractSimpleTreeViewParameters.ts @@ -0,0 +1,97 @@ +import * as React from 'react'; +import { SimpleTreeViewProps } from './SimpleTreeView.types'; +import { UseTreeViewStoreParameters } from '../internals/hooks/useTreeViewStore'; +import { SimpleTreeViewStore } from '../internals/SimpleTreeViewStore'; + +export function useExtractSimpleTreeViewParameters( + props: SimpleTreeViewProps, +) { + const { + // Props for Provider + apiRef, + slots, + slotProps, + + // Shared parameters + disabledItemsFocusable, + onItemClick, + itemChildrenIndentation, + id, + expandedItems, + defaultExpandedItems, + onExpandedItemsChange, + onItemExpansionToggle, + expansionTrigger, + disableSelection, + selectedItems, + defaultSelectedItems, + multiSelect, + checkboxSelection, + selectionPropagation, + onSelectedItemsChange, + onItemSelectionToggle, + onItemFocus, + + // SimpleTreeViewStore parameters + + // Forwarded props + ...forwardedProps + } = props; + + const parameters: UseTreeViewStoreParameters> = React.useMemo( + () => ({ + // Shared parameters + disabledItemsFocusable, + onItemClick, + itemChildrenIndentation, + id, + expandedItems, + defaultExpandedItems, + onExpandedItemsChange, + onItemExpansionToggle, + expansionTrigger, + disableSelection, + selectedItems, + defaultSelectedItems, + multiSelect, + checkboxSelection, + selectionPropagation, + onSelectedItemsChange, + onItemSelectionToggle, + onItemFocus, + + // SimpleTreeViewStore parameters + }), + [ + // Shared parameters + disabledItemsFocusable, + onItemClick, + itemChildrenIndentation, + id, + expandedItems, + defaultExpandedItems, + onExpandedItemsChange, + onItemExpansionToggle, + expansionTrigger, + disableSelection, + selectedItems, + defaultSelectedItems, + multiSelect, + checkboxSelection, + selectionPropagation, + onSelectedItemsChange, + onItemSelectionToggle, + onItemFocus, + + // SimpleTreeViewStore parameters + ], + ); + + return { + apiRef, + slots, + slotProps, + parameters, + forwardedProps, + }; +} diff --git a/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.tsx b/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.tsx index 4b66871076134..c702e1c5c399b 100644 --- a/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.tsx +++ b/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.tsx @@ -4,16 +4,15 @@ import PropTypes from 'prop-types'; import { useStore } from '@mui/x-internals/store'; import { TreeItemProviderProps } from './TreeItemProvider.types'; import { useTreeViewContext } from '../internals/TreeViewProvider'; -import { generateTreeItemIdAttribute } from '../internals/corePlugins/useTreeViewId/useTreeViewId.utils'; -import { idSelectors } from '../internals/corePlugins/useTreeViewId'; +import { idSelectors } from '../internals/plugins/id'; +import { TreeViewAnyStore } from '../internals/models'; function TreeItemProvider(props: TreeItemProviderProps) { const { children, itemId, id } = props; - const { wrapItem, instance, store } = useTreeViewContext<[]>(); - const treeId = useStore(store, idSelectors.treeId); - const idAttribute = generateTreeItemIdAttribute({ itemId, treeId, id }); + const { wrapItem, store } = useTreeViewContext(); + const idAttribute = useStore(store, idSelectors.treeItemIdAttribute, itemId, id); - return {wrapItem({ children, itemId, instance, idAttribute })}; + return {wrapItem({ children, itemId, store, idAttribute })}; } TreeItemProvider.propTypes = { diff --git a/packages/x-tree-view/src/hooks/index.ts b/packages/x-tree-view/src/hooks/index.ts index 53089c5f8c7f3..815170f2af2b3 100644 --- a/packages/x-tree-view/src/hooks/index.ts +++ b/packages/x-tree-view/src/hooks/index.ts @@ -1,4 +1,7 @@ -export { useTreeViewApiRef } from './useTreeViewApiRef'; export { useTreeItemUtils } from './useTreeItemUtils'; export { useTreeItemModel } from './useTreeItemModel'; export { useApplyPropagationToSelectedItemsOnMount } from './useApplyPropagationToSelectedItemsOnMount'; + +export { useTreeViewApiRef } from './useTreeViewApiRef'; +export { useRichTreeViewApiRef } from './useRichTreeViewApiRef'; +export { useSimpleTreeViewApiRef } from './useSimpleTreeViewApiRef'; diff --git a/packages/x-tree-view/src/hooks/useApplyPropagationToSelectedItemsOnMount.ts b/packages/x-tree-view/src/hooks/useApplyPropagationToSelectedItemsOnMount.ts index c30156c2c918f..790d6833af450 100644 --- a/packages/x-tree-view/src/hooks/useApplyPropagationToSelectedItemsOnMount.ts +++ b/packages/x-tree-view/src/hooks/useApplyPropagationToSelectedItemsOnMount.ts @@ -1,6 +1,6 @@ import { useRefWithInit } from '@base-ui-components/utils/useRefWithInit'; import { TreeViewItemId, TreeViewSelectionPropagation } from '../models'; -import { getLookupFromArray } from '../internals/plugins/useTreeViewSelection/useTreeViewSelection.utils'; +import { getLookupFromArray } from '../internals/plugins/selection/TreeViewSelectionPlugin'; const defaultGetItemId = (item: any) => item.id; diff --git a/packages/x-tree-view/src/hooks/useRichTreeViewApiRef.ts b/packages/x-tree-view/src/hooks/useRichTreeViewApiRef.ts new file mode 100644 index 0000000000000..a313ae34e2374 --- /dev/null +++ b/packages/x-tree-view/src/hooks/useRichTreeViewApiRef.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { TreeViewDefaultItemModelProperties, TreeViewValidItem } from '../models'; +import { TreeViewPublicAPI } from '../internals/models'; +import { RichTreeViewStore } from '../internals/RichTreeViewStore'; + +/** + * Creates the ref to pass to the `apiRef` prop of the `RichTreeView` component. + */ +export function useRichTreeViewApiRef< + R extends TreeViewValidItem = TreeViewDefaultItemModelProperties, +>() { + return React.useRef(undefined) as React.RefObject< + TreeViewPublicAPI> | undefined + >; +} diff --git a/packages/x-tree-view/src/hooks/useSimpleTreeViewApiRef.ts b/packages/x-tree-view/src/hooks/useSimpleTreeViewApiRef.ts new file mode 100644 index 0000000000000..55cbc4aeba97a --- /dev/null +++ b/packages/x-tree-view/src/hooks/useSimpleTreeViewApiRef.ts @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { TreeViewPublicAPI } from '../internals/models'; +import { SimpleTreeViewStore } from '../internals/SimpleTreeViewStore'; + +/** + * Creates the ref to pass to the `apiRef` prop of the `SimpleTreeView` component. + */ +export function useSimpleTreeViewApiRef() { + return React.useRef(undefined) as React.RefObject< + TreeViewPublicAPI> | undefined + >; +} diff --git a/packages/x-tree-view/src/hooks/useTreeItemModel.ts b/packages/x-tree-view/src/hooks/useTreeItemModel.ts index 3cbe0a2724d93..7f3cffbc75221 100644 --- a/packages/x-tree-view/src/hooks/useTreeItemModel.ts +++ b/packages/x-tree-view/src/hooks/useTreeItemModel.ts @@ -1,12 +1,13 @@ 'use client'; import { useStore } from '@mui/x-internals/store'; import { useTreeViewContext } from '../internals/TreeViewProvider'; -import { TreeViewBaseItem, TreeViewDefaultItemModelProperties, TreeViewItemId } from '../models'; -import { itemsSelectors, UseTreeViewItemsSignature } from '../internals/plugins/useTreeViewItems'; +import { TreeViewDefaultItemModelProperties, TreeViewItemId } from '../models'; +import { itemsSelectors } from '../internals/plugins/items'; +import { TreeViewAnyStore } from '../internals/models'; export const useTreeItemModel = ( itemId: TreeViewItemId, ) => { - const { store } = useTreeViewContext<[UseTreeViewItemsSignature]>(); - return useStore(store, itemsSelectors.itemModel, itemId) as unknown as TreeViewBaseItem | null; + const { store } = useTreeViewContext(); + return useStore(store, itemsSelectors.itemModel, itemId) as R | null; }; diff --git a/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx b/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx index 0687ce0043720..9a77209a6f5b0 100644 --- a/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx +++ b/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx @@ -3,24 +3,15 @@ import * as React from 'react'; import { useStore } from '@mui/x-internals/store'; import { TreeViewCancellableEvent } from '../../models'; import { useTreeViewContext } from '../../internals/TreeViewProvider'; -import type { UseTreeViewLazyLoadingSignature } from '../../internals/plugins/useTreeViewLazyLoading'; -import type { UseTreeViewSelectionSignature } from '../../internals/plugins/useTreeViewSelection'; -import type { UseTreeViewExpansionSignature } from '../../internals/plugins/useTreeViewExpansion'; -import type { UseTreeViewItemsSignature } from '../../internals/plugins/useTreeViewItems'; -import type { UseTreeViewFocusSignature } from '../../internals/plugins/useTreeViewFocus'; -import { - UseTreeViewLabelSignature, - useTreeViewLabel, -} from '../../internals/plugins/useTreeViewLabel'; import type { UseTreeItemStatus } from '../../useTreeItem'; -import { hasPlugin } from '../../internals/utils/plugins'; -import { TreeViewPublicAPI } from '../../internals/models'; -import { expansionSelectors } from '../../internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors'; -import { focusSelectors } from '../../internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors'; -import { itemsSelectors } from '../../internals/plugins/useTreeViewItems/useTreeViewItems.selectors'; -import { selectionSelectors } from '../../internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors'; -import { lazyLoadingSelectors } from '../../internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.selectors'; -import { labelSelectors } from '../../internals/plugins/useTreeViewLabel/useTreeViewLabel.selectors'; +import { TreeViewPublicAPI, TreeViewAnyStore } from '../../internals/models'; +import { expansionSelectors } from '../../internals/plugins/expansion/selectors'; +import { focusSelectors } from '../../internals/plugins/focus/selectors'; +import { itemsSelectors } from '../../internals/plugins/items/selectors'; +import { selectionSelectors } from '../../internals/plugins/selection/selectors'; +import { lazyLoadingSelectors } from '../../internals/plugins/lazyLoading/selectors'; +import { labelSelectors } from '../../internals/plugins/labelEditing/selectors'; +import { TreeViewLabelEditingPlugin } from '../../internals/plugins/labelEditing'; export interface UseTreeItemInteractions { handleExpansion: (event: React.MouseEvent) => void; @@ -31,37 +22,19 @@ export interface UseTreeItemInteractions { handleCancelItemLabelEditing: (event: React.SyntheticEvent) => void; } -/** - * Plugins that need to be present in the Tree View in order for `useTreeItemUtils` to work correctly. - */ -type UseTreeItemUtilsMinimalPlugins = readonly [ - UseTreeViewSelectionSignature, - UseTreeViewExpansionSignature, - UseTreeViewItemsSignature, - UseTreeViewFocusSignature, -]; - -/** - * Plugins that `useTreeItemUtils` can use if they are present, but are not required. - */ - -export type UseTreeItemUtilsOptionalPlugins = readonly [ - UseTreeViewLabelSignature, - UseTreeViewLazyLoadingSignature, -]; - -interface UseTreeItemUtilsReturnValue< - TSignatures extends UseTreeItemUtilsMinimalPlugins, - TOptionalSignatures extends UseTreeItemUtilsOptionalPlugins, -> { +interface UseTreeItemUtilsReturnValue { interactions: UseTreeItemInteractions; status: UseTreeItemStatus; /** * The object the allows Tree View manipulation. */ - publicAPI: TreeViewPublicAPI; + publicAPI: TreeViewPublicAPI; } +type TreeViewStoreWithLabelEditing = TreeViewAnyStore & { + labelEditing?: TreeViewLabelEditingPlugin; +}; + export const itemHasChildren = (reactChildren: React.ReactNode) => { if (Array.isArray(reactChildren)) { return reactChildren.length > 0 && reactChildren.some(itemHasChildren); @@ -70,16 +43,15 @@ export const itemHasChildren = (reactChildren: React.ReactNode) => { }; export const useTreeItemUtils = < - TSignatures extends UseTreeItemUtilsMinimalPlugins = UseTreeItemUtilsMinimalPlugins, - TOptionalSignatures extends UseTreeItemUtilsOptionalPlugins = UseTreeItemUtilsOptionalPlugins, + TStore extends TreeViewStoreWithLabelEditing = TreeViewStoreWithLabelEditing, >({ itemId, children, }: { itemId: string; children?: React.ReactNode; -}): UseTreeItemUtilsReturnValue => { - const { instance, store, publicAPI } = useTreeViewContext(); +}): UseTreeItemUtilsReturnValue => { + const { store, publicAPI } = useTreeViewContext(); const isItemExpandable = useStore(store, expansionSelectors.isItemExpandable, itemId); const isLoading = useStore(store, lazyLoadingSelectors.isItemLoading, itemId); @@ -110,7 +82,7 @@ export const useTreeItemUtils = < } if (!status.focused) { - instance.focusItem(event, itemId); + store.focus.focusItem(event, itemId); } const multiple = @@ -123,7 +95,7 @@ export const useTreeItemUtils = < !(multiple && expansionSelectors.isItemExpanded(store.state, itemId)) ) { // make sure the children selection is propagated again - instance.setItemExpansion({ event, itemId }); + store.expansion.setItemExpansion({ event, itemId }); } }; @@ -133,7 +105,7 @@ export const useTreeItemUtils = < } if (!status.focused && !status.editing) { - instance.focusItem(event, itemId); + store.focus.focusItem(event, itemId); } const multiple = @@ -142,12 +114,12 @@ export const useTreeItemUtils = < if (multiple) { if (event.shiftKey) { - instance.expandSelectionRange(event, itemId); + store.selection.expandSelectionRange(event, itemId); } else { - instance.setItemSelection({ event, itemId, keepExistingSelection: true }); + store.selection.setItemSelection({ event, itemId, keepExistingSelection: true }); } } else { - instance.setItemSelection({ event, itemId, shouldBeSelected: true }); + store.selection.setItemSelection({ event, itemId, shouldBeSelected: true }); } }; @@ -155,9 +127,9 @@ export const useTreeItemUtils = < const hasShift = (event.nativeEvent as PointerEvent).shiftKey; const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(store.state); if (isMultiSelectEnabled && hasShift) { - instance.expandSelectionRange(event, itemId); + store.selection.expandSelectionRange(event, itemId); } else { - instance.setItemSelection({ + store.selection.setItemSelection({ event, itemId, keepExistingSelection: isMultiSelectEnabled, @@ -167,14 +139,15 @@ export const useTreeItemUtils = < }; const toggleItemEditing = () => { - if (!hasPlugin(instance, useTreeViewLabel)) { + // If the store doesn't support label editing, do nothing + if (!store.labelEditing) { return; } if (isEditing) { - instance.setEditedItem(null); + store.labelEditing.setEditedItem(null); } else { - instance.setEditedItem(itemId); + store.labelEditing.setEditedItem(itemId); } }; @@ -182,7 +155,8 @@ export const useTreeItemUtils = < event: React.SyntheticEvent & TreeViewCancellableEvent, newLabel: string, ) => { - if (!hasPlugin(instance, useTreeViewLabel)) { + // If the store doesn't support label editing, do nothing + if (!store.labelEditing) { return; } @@ -190,20 +164,21 @@ export const useTreeItemUtils = < // The `onBlur` event is triggered, which calls `handleSaveItemLabel` again. // To avoid creating an unwanted behavior we need to check if the item is being edited before calling `updateItemLabel` if (labelSelectors.isItemBeingEdited(store.state, itemId)) { - instance.updateItemLabel(itemId, newLabel); + store.labelEditing.updateItemLabel(itemId, newLabel); toggleItemEditing(); - instance.focusItem(event, itemId); + store.focus.focusItem(event, itemId); } }; const handleCancelItemLabelEditing = (event: React.SyntheticEvent) => { - if (!hasPlugin(instance, useTreeViewLabel)) { + // If the store doesn't support label editing, do nothing + if (!store.labelEditing) { return; } if (labelSelectors.isItemBeingEdited(store.state, itemId)) { toggleItemEditing(); - instance.focusItem(event, itemId); + store.focus.focusItem(event, itemId); } }; diff --git a/packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx b/packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx index 7012107b5cc7f..66b3ec1f8a40e 100644 --- a/packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx +++ b/packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx @@ -1,11 +1,8 @@ 'use client'; -import * as React from 'react'; -import { TreeViewAnyPluginSignature, TreeViewPublicAPI } from '../internals/models'; -import { RichTreeViewPluginSignatures } from '../RichTreeView/RichTreeView.plugins'; +import { useRichTreeViewApiRef } from './useRichTreeViewApiRef'; /** * Hook that instantiates a [[TreeViewApiRef]]. + * @deprecated Use `useRichTreeViewApiRef`, `useRichTreeViewProApiRef` or `useSimpleTreeViewApiRef` instead. */ -export const useTreeViewApiRef = < - TSignatures extends readonly TreeViewAnyPluginSignature[] = RichTreeViewPluginSignatures, ->() => React.useRef(undefined) as React.RefObject | undefined>; +export const useTreeViewApiRef = useRichTreeViewApiRef; diff --git a/packages/x-tree-view/src/index.ts b/packages/x-tree-view/src/index.ts index fd368316ad138..2c4ecf6056817 100644 --- a/packages/x-tree-view/src/index.ts +++ b/packages/x-tree-view/src/index.ts @@ -10,8 +10,6 @@ export * from './TreeItemProvider'; export * from './TreeItemDragAndDropOverlay'; export * from './TreeItemLabelInput'; -export { unstable_resetCleanupTracking } from './internals/hooks/useInstanceEventHandler'; - export * from './models'; export * from './icons'; export * from './hooks'; diff --git a/packages/x-tree-view/src/internals/MinimalTreeViewStore/MinimalTreeViewStore.ts b/packages/x-tree-view/src/internals/MinimalTreeViewStore/MinimalTreeViewStore.ts new file mode 100644 index 0000000000000..7c8caf2b2114e --- /dev/null +++ b/packages/x-tree-view/src/internals/MinimalTreeViewStore/MinimalTreeViewStore.ts @@ -0,0 +1,230 @@ +import { Store } from '@mui/x-internals/store'; +import { warnOnce } from '@mui/x-internals/warning'; +import { EventManager } from '@mui/x-internals/EventManager'; +import { + TreeViewModelUpdater, + MinimalTreeViewParameters, + TreeViewParametersToStateMapper, + MinimalTreeViewState, +} from './MinimalTreeViewStore.types'; +import { TreeViewValidItem } from '../../models'; +import { + createMinimalInitialState, + createTreeViewDefaultId, + deriveStateFromParameters, +} from './MinimalTreeViewStore.utils'; +import { TimeoutManager } from './TimeoutManager'; +import { TreeViewKeyboardNavigationPlugin } from '../plugins/keyboardNavigation'; +import { TreeViewFocusPlugin } from '../plugins/focus/TreeViewFocusPlugin'; +import { TreeViewItemsPlugin } from '../plugins/items/TreeViewItemsPlugin'; +import { TreeViewSelectionPlugin } from '../plugins/selection/TreeViewSelectionPlugin'; +import { TreeViewExpansionPlugin } from '../plugins/expansion'; +import { TreeViewItemPluginManager } from './TreeViewItemPluginManager'; +import { + TreeViewEventEvent, + TreeViewEventListener, + TreeViewEventParameters, + TreeViewEvents, +} from '../models'; + +export class MinimalTreeViewStore< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, + State extends MinimalTreeViewState = MinimalTreeViewState, + Parameters extends MinimalTreeViewParameters = MinimalTreeViewParameters< + R, + Multiple + >, +> extends Store { + private initialParameters: Parameters | null = null; + + private mapper: TreeViewParametersToStateMapper; + + private eventManager = new EventManager(); + + public instanceName: string; + + public parameters: Parameters; + + public timeoutManager = new TimeoutManager(); + + public itemPluginManager = new TreeViewItemPluginManager(); + + public items: TreeViewItemsPlugin; + + public focus: TreeViewFocusPlugin; + + public expansion: TreeViewExpansionPlugin; + + public selection: TreeViewSelectionPlugin; + + public keyboardNavigation: TreeViewKeyboardNavigationPlugin; + + public constructor( + parameters: Parameters, + instanceName: string, + mapper: TreeViewParametersToStateMapper, + ) { + const minimalInitialState = createMinimalInitialState(parameters); + const initialState = mapper.getInitialState(minimalInitialState, parameters); + super(initialState); + + this.parameters = parameters; + this.instanceName = instanceName; + this.mapper = mapper; + + // We mount the plugins in the constructor to make sure all the methods of the store are available to the plugins during their construction. + this.items = new TreeViewItemsPlugin(this); + this.focus = new TreeViewFocusPlugin(this); + this.expansion = new TreeViewExpansionPlugin(this); + this.selection = new TreeViewSelectionPlugin(this); + this.keyboardNavigation = new TreeViewKeyboardNavigationPlugin(this); + + if (process.env.NODE_ENV !== 'production') { + this.initialParameters = parameters; + } + } + + /** + * Builds an object containing the method that should be exposed publicly by the Tree View components. + */ + public buildPublicAPI() { + return { + ...this.items.buildPublicAPI(), + ...this.focus.buildPublicAPI(), + ...this.expansion.buildPublicAPI(), + ...this.selection.buildPublicAPI(), + }; + } + + /** + * Updates the state of the Tree View based on the new parameters provided to the root component. + */ + public updateStateFromParameters(parameters: Parameters) { + const updateModel: TreeViewModelUpdater = ( + mutableNewState, + controlledProp, + defaultProp, + ) => { + if (parameters[controlledProp] !== undefined) { + mutableNewState[controlledProp] = parameters[controlledProp] as any; + } + + if (process.env.NODE_ENV !== 'production') { + const defaultValue = parameters[defaultProp]; + const isControlled = parameters[controlledProp] !== undefined; + const initialDefaultValue = this.initialParameters?.[defaultProp]; + const initialIsControlled = this.initialParameters?.[controlledProp] !== undefined; + + if (initialIsControlled !== isControlled) { + warnOnce( + [ + `MUI X Tree View: A component is changing the ${ + initialIsControlled ? '' : 'un' + }controlled ${controlledProp} state of ${this.instanceName} to be ${initialIsControlled ? 'un' : ''}controlled.`, + 'Elements should not switch from uncontrolled to controlled (or vice versa).', + `Decide between using a controlled or uncontrolled ${controlledProp} element for the lifetime of the component.`, + "The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.", + 'More info: https://fb.me/react-controlled-components', + ], + 'error', + ); + } else if (JSON.stringify(initialDefaultValue) !== JSON.stringify(defaultValue)) { + warnOnce( + [ + `MUI X Tree View: A component is changing the default ${controlledProp} state of an uncontrolled ${this.instanceName} after being initialized. `, + `To suppress this warning opt to use a controlled ${this.instanceName}.`, + ], + 'error', + ); + } + } + }; + + const newMinimalState = deriveStateFromParameters(parameters) as Partial; + + updateModel(newMinimalState, 'expandedItems', 'defaultExpandedItems'); + updateModel(newMinimalState, 'selectedItems', 'defaultSelectedItems'); + + if (this.state.providedTreeId !== parameters.id || this.state.treeId === undefined) { + newMinimalState.treeId = createTreeViewDefaultId(); + } + + if ( + !this.mapper.shouldIgnoreItemsStateUpdate(parameters) && + TreeViewItemsPlugin.shouldRebuildItemsState(parameters, this.parameters) + ) { + Object.assign(newMinimalState, TreeViewItemsPlugin.buildItemsStateIfNeeded(parameters)); + } + + const newState = this.mapper.updateStateFromParameters( + newMinimalState, + parameters, + updateModel, + ); + + this.update(newState); + this.parameters = parameters; + } + + /** + * Returns a cleanup function that need to be called when the store is destroyed. + */ + public disposeEffect = () => { + return this.timeoutManager.clearAll; + }; + + /** + * Whether updates based on `props.items` change should be ignored. + */ + public shouldIgnoreItemsStateUpdate = () => { + return this.mapper.shouldIgnoreItemsStateUpdate(this.parameters); + }; + + /** + * Registers an effect to be run when the value returned by the selector changes. + */ + public registerStoreEffect = ( + selector: (state: State) => Value, + effect: (previous: Value, next: Value) => void, + ) => { + let previousValue = selector(this.state); + + this.subscribe((state) => { + const nextValue = selector(state); + if (nextValue !== previousValue) { + effect(previousValue, nextValue); + previousValue = nextValue; + } + }); + }; + + /** + * Publishes an event to all its subscribers. + */ + public publishEvent = ( + name: E, + params: TreeViewEventParameters, + event: TreeViewEventEvent, + ) => { + if (isSyntheticEvent(event) && event.isPropagationStopped()) { + return; + } + this.eventManager.emit(name, params, event); + }; + + /** + * Subscribe to an event emitted by the store. + * For now, the subscription is only removed when the store is destroyed. + */ + public subscribeEvent = ( + eventName: E, + handler: TreeViewEventListener, + ) => { + this.eventManager.on(eventName, handler); + }; +} + +function isSyntheticEvent(event: any): event is React.SyntheticEvent { + return event.isPropagationStopped !== undefined; +} diff --git a/packages/x-tree-view/src/internals/MinimalTreeViewStore/MinimalTreeViewStore.types.ts b/packages/x-tree-view/src/internals/MinimalTreeViewStore/MinimalTreeViewStore.types.ts new file mode 100644 index 0000000000000..2cc6acf6c0e34 --- /dev/null +++ b/packages/x-tree-view/src/internals/MinimalTreeViewStore/MinimalTreeViewStore.types.ts @@ -0,0 +1,306 @@ +import { TreeViewItemId, TreeViewSelectionPropagation, TreeViewValidItem } from '../../models'; +import { TreeViewItemMeta } from '../models'; + +export interface MinimalTreeViewState< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +> { + /** + * Whether the button should be focusable when disabled. + * Always equal to `props.disabledItemsFocusable` (or `false` if not provided). + */ + disabledItemsFocusable: boolean; + /** + * Model of each item as provided by `props.items` or by imperative items updates. + * It is not updated when properties derived from the model are updated: + * - when the label of an item is updated, `itemMetaLookup` is updated, not `itemModelLookup`. + * - when the children of an item are updated, `itemOrderedChildrenIdsLookup` and `itemChildrenIndexesLookup` are updated, not `itemModelLookup`. + * This means that the `children`, `label` or `id` properties of an item model should never be used directly, always use the structured sub-states instead. + */ + itemModelLookup: { [itemId: string]: R }; + /** + * Meta data of each item. + */ + itemMetaLookup: { [itemId: string]: TreeViewItemMeta }; + /** + * Ordered children ids of each item. + */ + itemOrderedChildrenIdsLookup: { [parentItemId: string]: TreeViewItemId[] }; + /** + * Index of each child in the ordered children ids of its parent. + */ + itemChildrenIndexesLookup: { [parentItemId: string]: { [itemId: string]: number } }; + /** + * When equal to 'flat', the tree is rendered as a flat list (children are rendered as siblings of their parents). + * When equal to 'nested', the tree is rendered with nested children (children are rendered inside the groupTransition slot of their children). + * Nested DOM structure is not compatible with collapse / expansion animations. + */ + domStructure: 'flat' | 'nested'; + /** + * Horizontal indentation between an item and its children. + * Examples: 24, "24px", "2rem", "2em". + */ + itemChildrenIndentation: string | number; + /** + * The id of the Tree View as provided by the `id` parameter. + */ + providedTreeId: string | undefined; + /** + * The id of the Tree View used for accessibility attributes. + */ + treeId: string | undefined; + /** + * The ids of the items currently expanded. + */ + expandedItems: readonly TreeViewItemId[]; + /** + * The slot that triggers the item's expansion when clicked. + */ + expansionTrigger: 'content' | 'iconContainer'; + /** + * The ids of the items currently selected. + */ + selectedItems: TreeViewSelectionReadonlyValue; + /** + * Whether selection is disabled. + */ + disableSelection: boolean; + /** + * Whether multi-selection is enabled. + */ + multiSelect: boolean; + /** + * Whether the Tree View renders a checkbox at the left of its label that allows selecting it. + */ + checkboxSelection: boolean; + /** + * The selection propagation behavior. + */ + selectionPropagation: TreeViewSelectionPropagation; + /** + * The id of the currently focused item. + */ + focusedItemId: TreeViewItemId | null; +} + +export interface MinimalTreeViewParameters< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +> { + /** + * Whether the layout is right-to-left. + */ + isRtl: boolean; + /** + * Whether the items should be focusable when disabled. + * @default false + */ + // TODO Base UI: Rename focusableWhenDisabled. + disabledItemsFocusable?: boolean; + items: readonly R[]; + /** + * Used to determine if a given item should be disabled. + * @template R + * @param {R} item The item to check. + * @returns {boolean} `true` if the item should be disabled. + */ + isItemDisabled?: (item: R) => boolean; + /** + * Used to determine the string label for a given item. + * + * @template R + * @param {R} item The item to check. + * @returns {string} The label of the item. + * @default (item) => item.label + */ + getItemLabel?: (item: R) => string; + /** + * Used to determine the children of a given item. + * + * @template R + * @param {R} item The item to check. + * @returns {R[]} The children of the item. + * @default (item) => item.children + */ + getItemChildren?: (item: R) => R[] | undefined; + /** + * Used to determine the id of a given item. + * + * @template R + * @param {R} item The item to check. + * @returns {TreeViewItemId} The id of the item. + * @default (item) => item.id + */ + getItemId?: (item: R) => TreeViewItemId; + /** + * Callback fired when the `content` slot of a given Tree Item is clicked. + * @param {React.MouseEvent} event The DOM event that triggered the change. + * @param {TreeViewItemId} itemId The id of the focused item. + */ + onItemClick?: (event: React.MouseEvent, itemId: TreeViewItemId) => void; + /** + * Horizontal indentation between an item and its children. + * Examples: 24, "24px", "2rem", "2em". + * @default 12px + */ + itemChildrenIndentation?: string | number; + /** + * This prop is used to help implement the accessibility logic. + * If you don't provide this prop. It falls back to a randomly generated id. + */ + id?: string; + /** + * Expanded item ids. + * Used when the item's expansion is controlled. + */ + expandedItems?: readonly TreeViewItemId[]; + /** + * Expanded item ids. + * Used when the item's expansion is not controlled. + * @default [] + */ + defaultExpandedItems?: readonly TreeViewItemId[]; + /** + * Callback fired when Tree Items are expanded/collapsed. + * @param {React.SyntheticEvent} event The DOM event that triggered the change. Can be null when the change is caused by the `publicAPI.setItemExpansion()` method. + * @param {TreeViewItemId[]} itemIds The ids of the expanded items. + */ + onExpandedItemsChange?: (event: React.SyntheticEvent | null, itemIds: TreeViewItemId[]) => void; + /** + * Callback fired when a Tree Item is expanded or collapsed. + * @param {React.SyntheticEvent | null} event The DOM event that triggered the change. Can be null when the change is caused by the `publicAPI.setItemExpansion()` method. + * @param {TreeViewItemId} itemId The itemId of the modified item. + * @param {boolean} isExpanded `true` if the item has just been expanded, `false` if it has just been collapsed. + */ + onItemExpansionToggle?: ( + event: React.SyntheticEvent | null, + itemId: TreeViewItemId, + isExpanded: boolean, + ) => void; + /** + * The slot that triggers the item's expansion when clicked. + * @default 'content' + */ + expansionTrigger?: 'content' | 'iconContainer'; + /** + * Whether selection is disabled. + * @default false + */ + disableSelection?: boolean; + /** + * Selected item ids. (Controlled) + * When `multiSelect` is true this takes an array of strings; when false (default) a string. + */ + selectedItems?: TreeViewSelectionReadonlyValue; + /** + * Selected item ids. (Uncontrolled) + * When `multiSelect` is true this takes an array of strings; when false (default) a string. + * @default [] + */ + defaultSelectedItems?: TreeViewSelectionReadonlyValue; + /** + * Whether multiple items can be selected. + * @default false + */ + multiSelect?: Multiple; + /** + * Whether the Tree View renders a checkbox at the left of its label that allows selecting it. + * @default false + */ + checkboxSelection?: boolean; + /** + * When `selectionPropagation.descendants` is set to `true`. + * + * - Selecting a parent selects all its descendants automatically. + * - Deselecting a parent deselects all its descendants automatically. + * + * When `selectionPropagation.parents` is set to `true`. + * + * - Selecting all the descendants of a parent selects the parent automatically. + * - Deselecting a descendant of a selected parent deselects the parent automatically. + * + * Only works when `multiSelect` is `true`. + * On the , only the expanded items are considered (since the collapsed item are not passed to the Tree View component at all) + * + * @default { parents: false, descendants: false } + */ + selectionPropagation?: TreeViewSelectionPropagation; + /** + * Callback fired when Tree Items are selected/deselected. + * @param {React.SyntheticEvent} event The DOM event that triggered the change. Can be null when the change is caused by the `publicAPI.setItemSelection()` method. + * @param {TreeViewItemId[] | TreeViewItemId} itemIds The ids of the selected items. + * When `multiSelect` is `true`, this is an array of strings; when false (default) a string. + */ + onSelectedItemsChange?: ( + event: React.SyntheticEvent | null, + itemIds: TreeViewSelectionValue, + ) => void; + /** + * Callback fired when a Tree Item is selected or deselected. + * @param {React.SyntheticEvent} event The DOM event that triggered the change. Can be null when the change is caused by the `publicAPI.setItemSelection()` method. + * @param {TreeViewItemId} itemId The itemId of the modified item. + * @param {boolean} isSelected `true` if the item has just been selected, `false` if it has just been deselected. + */ + onItemSelectionToggle?: ( + event: React.SyntheticEvent | null, + itemId: TreeViewItemId, + isSelected: boolean, + ) => void; + /** + * Callback fired when a given Tree Item is focused. + * @param {React.SyntheticEvent | null} event The DOM event that triggered the change. **Warning**: This is a generic event not a focus event. + * @param {TreeViewItemId} itemId The id of the focused item. + */ + onItemFocus?: (event: React.SyntheticEvent | null, itemId: TreeViewItemId) => void; +} + +/** + * Mapper between a Tree View instance's state and parameters. + * Used by classes extending `TreeViewStore` to manage the state based on the parameters. + */ +export interface TreeViewParametersToStateMapper< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, + State extends MinimalTreeViewState, + Parameters extends MinimalTreeViewParameters, +> { + getInitialState: ( + treeViewInitialState: MinimalTreeViewState, + parameters: Parameters, + ) => State; + + updateStateFromParameters: ( + newState: Partial>, + parameters: Parameters, + updateModel: TreeViewModelUpdater, + ) => Partial; + shouldIgnoreItemsStateUpdate: (parameters: Parameters) => boolean; +} + +export type TreeViewModelUpdater< + State extends MinimalTreeViewState, + Parameters extends MinimalTreeViewParameters, +> = ( + newState: Partial, + controlledProp: keyof Parameters & keyof State & string, + defaultProp: keyof Parameters, +) => void; + +export type TreeViewSelectionReadonlyValue = + Multiple extends true + ? Multiple extends false + ? // Multiple === boolean, the type cannot be simplified further + TreeViewItemId | null | readonly TreeViewItemId[] + : // Multiple === true, the selection is multiple + readonly TreeViewItemId[] + : // Multiple === false | undefined, the selection is single + TreeViewItemId | null; + +export type TreeViewSelectionValue = Multiple extends true + ? Multiple extends false + ? // Multiple === boolean, the type cannot be simplified further + TreeViewItemId | null | TreeViewItemId[] + : // Multiple === true, the selection is multiple + TreeViewItemId[] + : // Multiple === false | undefined, the selection is single + TreeViewItemId | null; diff --git a/packages/x-tree-view/src/internals/MinimalTreeViewStore/MinimalTreeViewStore.utils.ts b/packages/x-tree-view/src/internals/MinimalTreeViewStore/MinimalTreeViewStore.utils.ts new file mode 100644 index 0000000000000..98330fc707d2d --- /dev/null +++ b/packages/x-tree-view/src/internals/MinimalTreeViewStore/MinimalTreeViewStore.utils.ts @@ -0,0 +1,76 @@ +import { EMPTY_ARRAY, EMPTY_OBJECT } from '@base-ui-components/utils/empty'; +import { TreeViewValidItem } from '../../models'; +import { getExpansionTrigger } from '../plugins/expansion/utils'; +import { + MinimalTreeViewParameters, + MinimalTreeViewState, + TreeViewSelectionValue, +} from './MinimalTreeViewStore.types'; +import { TreeViewItemsPlugin } from '../plugins/items'; + +/** + * Returns the properties of the state that are derived from the parameters. + * This do not contain state properties that don't update whenever the parameters update. + */ +export function deriveStateFromParameters< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +>(parameters: MinimalTreeViewParameters & { isItemEditable?: any }) { + return { + disabledItemsFocusable: parameters.disabledItemsFocusable ?? false, + domStructure: 'nested' as const, + itemChildrenIndentation: parameters.itemChildrenIndentation ?? '12px', + providedTreeId: parameters.id, + // TODO: Fix + expansionTrigger: getExpansionTrigger({ + isItemEditable: parameters.isItemEditable, + expansionTrigger: parameters.expansionTrigger, + }), + disableSelection: parameters.disableSelection ?? false, + multiSelect: parameters.multiSelect ?? false, + checkboxSelection: parameters.checkboxSelection ?? false, + selectionPropagation: parameters.selectionPropagation ?? EMPTY_OBJECT, + }; +} + +function applyModelInitialValue( + controlledValue: T | undefined, + defaultValue: T | undefined, + fallback: T, +): T { + if (controlledValue !== undefined) { + return controlledValue; + } + if (defaultValue !== undefined) { + return defaultValue; + } + return fallback; +} + +export function createMinimalInitialState< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +>(parameters: MinimalTreeViewParameters): MinimalTreeViewState { + return { + treeId: undefined, + focusedItemId: null, + ...deriveStateFromParameters(parameters), + ...TreeViewItemsPlugin.buildItemsStateIfNeeded(parameters), + expandedItems: applyModelInitialValue( + parameters.expandedItems, + parameters.defaultExpandedItems, + [], + ), + selectedItems: applyModelInitialValue( + parameters.selectedItems, + parameters.defaultSelectedItems, + (parameters.multiSelect ? EMPTY_ARRAY : null) as TreeViewSelectionValue, + ), + }; +} + +let globalTreeViewDefaultId = 0; +export const createTreeViewDefaultId = () => { + globalTreeViewDefaultId += 1; + return `mui-tree-view-${globalTreeViewDefaultId}`; +}; diff --git a/packages/x-tree-view/src/internals/MinimalTreeViewStore/TimeoutManager.ts b/packages/x-tree-view/src/internals/MinimalTreeViewStore/TimeoutManager.ts new file mode 100644 index 0000000000000..b5c1727dbc88a --- /dev/null +++ b/packages/x-tree-view/src/internals/MinimalTreeViewStore/TimeoutManager.ts @@ -0,0 +1,49 @@ +export class TimeoutManager { + private timeoutIds: Map = new Map(); + + private intervalIds: Map = new Map(); + + startTimeout = (key: string, delay: number, fn: Function) => { + this.clearTimeout(key); + const id = setTimeout(() => { + this.timeoutIds.delete(key); + fn(); + }, delay) as unknown as number; /* Node.js types are enabled in development */ + + this.timeoutIds.set(key, id); + }; + + startInterval = (key: string, delay: number, fn: Function) => { + this.clearTimeout(key); + const id = setInterval( + fn, + delay, + ) as unknown as number; /* Node.js types are enabled in development */ + + this.intervalIds.set(key, id); + }; + + clearTimeout = (key: string) => { + const id = this.timeoutIds.get(key); + if (id != null) { + clearTimeout(id); + this.timeoutIds.delete(key); + } + }; + + clearInterval = (key: string) => { + const id = this.intervalIds.get(key); + if (id != null) { + clearInterval(id); + this.intervalIds.delete(key); + } + }; + + clearAll = () => { + this.timeoutIds.forEach(clearTimeout); + this.timeoutIds.clear(); + + this.intervalIds.forEach(clearInterval); + this.intervalIds.clear(); + }; +} diff --git a/packages/x-tree-view/src/internals/MinimalTreeViewStore/TreeViewItemPluginManager.ts b/packages/x-tree-view/src/internals/MinimalTreeViewStore/TreeViewItemPluginManager.ts new file mode 100644 index 0000000000000..ad8ca7b407144 --- /dev/null +++ b/packages/x-tree-view/src/internals/MinimalTreeViewStore/TreeViewItemPluginManager.ts @@ -0,0 +1,22 @@ +import { TreeItemWrapper, TreeViewAnyStore, TreeViewItemPlugin } from '../models'; + +/** + * Manages the registration and application of plugins for the Tree Item. + * This will be replaced with a proper implementation in the future. + */ +export class TreeViewItemPluginManager { + private itemPlugins: TreeViewItemPlugin[] = []; + + private itemWrappers: TreeItemWrapper[] = []; + + public register = (plugin: TreeViewItemPlugin, wrapItem: TreeItemWrapper | null) => { + this.itemPlugins.push(plugin); + if (wrapItem) { + this.itemWrappers.push(wrapItem); + } + }; + + public listPlugins = () => this.itemPlugins; + + public listWrappers = () => this.itemWrappers; +} diff --git a/packages/x-tree-view/src/internals/MinimalTreeViewStore/index.ts b/packages/x-tree-view/src/internals/MinimalTreeViewStore/index.ts new file mode 100644 index 0000000000000..97dacda1c0698 --- /dev/null +++ b/packages/x-tree-view/src/internals/MinimalTreeViewStore/index.ts @@ -0,0 +1,2 @@ +export * from './MinimalTreeViewStore'; +export * from './MinimalTreeViewStore.types'; diff --git a/packages/x-tree-view/src/internals/RichTreeViewStore/RichTreeViewStore.ts b/packages/x-tree-view/src/internals/RichTreeViewStore/RichTreeViewStore.ts new file mode 100644 index 0000000000000..c5fcd9663a4db --- /dev/null +++ b/packages/x-tree-view/src/internals/RichTreeViewStore/RichTreeViewStore.ts @@ -0,0 +1,44 @@ +import { TreeViewValidItem } from '../../models'; +import { TreeViewLabelEditingPlugin } from '../plugins/labelEditing'; +import { MinimalTreeViewStore } from '../MinimalTreeViewStore'; +import { RichTreeViewStoreParameters, RichTreeViewState } from './RichTreeViewStore.types'; +import { parametersToStateMapper } from './RichTreeViewStore.utils'; + +export class ExtendableRichTreeViewStore< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, + State extends RichTreeViewState = RichTreeViewState, + Parameters extends RichTreeViewStoreParameters = RichTreeViewStoreParameters< + R, + Multiple + >, +> extends MinimalTreeViewStore { + public labelEditing = new TreeViewLabelEditingPlugin(this); + + /** + * Mapper of the RichTreeViewStore. + * Can be used by classes extending the RichTreeViewStore to create their own mapper. + */ + public static rawMapper = parametersToStateMapper; + + public buildPublicAPI() { + return { + ...super.buildPublicAPI(), + ...this.labelEditing.buildPublicAPI(), + }; + } +} + +export class RichTreeViewStore< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +> extends ExtendableRichTreeViewStore< + R, + Multiple, + RichTreeViewState, + RichTreeViewStoreParameters +> { + public constructor(parameters: RichTreeViewStoreParameters) { + super(parameters, 'RichTreeView', parametersToStateMapper); + } +} diff --git a/packages/x-tree-view/src/internals/RichTreeViewStore/RichTreeViewStore.types.ts b/packages/x-tree-view/src/internals/RichTreeViewStore/RichTreeViewStore.types.ts new file mode 100644 index 0000000000000..c80431b38bd43 --- /dev/null +++ b/packages/x-tree-view/src/internals/RichTreeViewStore/RichTreeViewStore.types.ts @@ -0,0 +1,42 @@ +import { TreeViewItemId, TreeViewValidItem } from '../../models'; +import { MinimalTreeViewParameters, MinimalTreeViewState } from '../MinimalTreeViewStore'; +import { RichTreeViewLazyLoadedItemsStatus } from '../plugins/lazyLoading'; + +export interface RichTreeViewState< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +> extends MinimalTreeViewState { + /** + * Determine if a given item can be edited. + */ + isItemEditable: ((item: any) => boolean) | boolean; + /** + * The id of the item currently being edited. + */ + editedItemId: string | null; + /** + * The status of the items loaded using lazy loading. + * Is null if lazy loading is not enabled. + */ + lazyLoadedItems: RichTreeViewLazyLoadedItemsStatus | null; +} + +export interface RichTreeViewStoreParameters< + R extends TreeViewValidItem, + Multiple extends boolean | undefined, +> extends MinimalTreeViewParameters { + /** + * Callback fired when the label of an item changes. + * @param {TreeViewItemId} itemId The id of the item that was edited. + * @param {string} newLabel The new label of the items. + */ + onItemLabelChange?: (itemId: TreeViewItemId, newLabel: string) => void; + /** + * Determine if a given item can be edited. + * @template R + * @param {R} item The item to check. + * @returns {boolean} `true` if the item can be edited. + * @default () => false + */ + isItemEditable?: boolean | ((item: R) => boolean); +} diff --git a/packages/x-tree-view/src/internals/RichTreeViewStore/RichTreeViewStore.utils.ts b/packages/x-tree-view/src/internals/RichTreeViewStore/RichTreeViewStore.utils.ts new file mode 100644 index 0000000000000..4584f58220b21 --- /dev/null +++ b/packages/x-tree-view/src/internals/RichTreeViewStore/RichTreeViewStore.utils.ts @@ -0,0 +1,29 @@ +import { TreeViewParametersToStateMapper } from '../MinimalTreeViewStore'; +import { RichTreeViewStoreParameters, RichTreeViewState } from './RichTreeViewStore.types'; + +const deriveStateFromParameters = (parameters: RichTreeViewStoreParameters) => ({ + isItemEditable: parameters.isItemEditable ?? false, +}); + +export const parametersToStateMapper: TreeViewParametersToStateMapper< + any, + any, + RichTreeViewState, + RichTreeViewStoreParameters +> = { + getInitialState: (minimalInitialState, parameters) => ({ + ...minimalInitialState, + ...deriveStateFromParameters(parameters), + editedItemId: null, + lazyLoadedItems: null, + }), + updateStateFromParameters: (newMinimalState, parameters) => { + const newState: Partial> = { + ...newMinimalState, + ...deriveStateFromParameters(parameters), + }; + + return newState; + }, + shouldIgnoreItemsStateUpdate: () => false, +}; diff --git a/packages/x-tree-view/src/internals/RichTreeViewStore/index.ts b/packages/x-tree-view/src/internals/RichTreeViewStore/index.ts new file mode 100644 index 0000000000000..2e01fda16d5b0 --- /dev/null +++ b/packages/x-tree-view/src/internals/RichTreeViewStore/index.ts @@ -0,0 +1,3 @@ +export * from './RichTreeViewStore'; +export * from './RichTreeViewStore.types'; +export * from './RichTreeViewStore.utils'; diff --git a/packages/x-tree-view/src/internals/SimpleTreeViewStore/SimpleTreeViewStore.ts b/packages/x-tree-view/src/internals/SimpleTreeViewStore/SimpleTreeViewStore.ts new file mode 100644 index 0000000000000..f6d019e89cd1f --- /dev/null +++ b/packages/x-tree-view/src/internals/SimpleTreeViewStore/SimpleTreeViewStore.ts @@ -0,0 +1,27 @@ +import { EMPTY_ARRAY } from '@base-ui-components/utils/empty'; +import { MinimalTreeViewStore } from '../MinimalTreeViewStore'; +import { + InnerSimpleTreeViewParameters, + SimpleTreeViewItem, + SimpleTreeViewStoreParameters, + SimpleTreeViewState, +} from './SimpleTreeViewStore.types'; +import { TreeViewJSXItemsPlugin } from '../plugins/jsxItems'; +import { parametersToStateMapper } from './SimpleTreeViewStore.utils'; + +export class SimpleTreeViewStore extends MinimalTreeViewStore< + SimpleTreeViewItem, + Multiple, + SimpleTreeViewState, + InnerSimpleTreeViewParameters +> { + public jsxItems = new TreeViewJSXItemsPlugin(this); + + public constructor(parameters: SimpleTreeViewStoreParameters) { + super({ ...parameters, items: EMPTY_ARRAY }, 'SimpleTreeView', parametersToStateMapper); + } + + public updateStateFromParameters(parameters: SimpleTreeViewStoreParameters) { + super.updateStateFromParameters({ ...parameters, items: EMPTY_ARRAY }); + } +} diff --git a/packages/x-tree-view/src/internals/SimpleTreeViewStore/SimpleTreeViewStore.types.ts b/packages/x-tree-view/src/internals/SimpleTreeViewStore/SimpleTreeViewStore.types.ts new file mode 100644 index 0000000000000..a674d134e9631 --- /dev/null +++ b/packages/x-tree-view/src/internals/SimpleTreeViewStore/SimpleTreeViewStore.types.ts @@ -0,0 +1,19 @@ +import { MinimalTreeViewParameters, MinimalTreeViewState } from '../MinimalTreeViewStore'; + +export interface SimpleTreeViewState + extends MinimalTreeViewState {} + +export interface InnerSimpleTreeViewParameters + extends MinimalTreeViewParameters {} + +export interface SimpleTreeViewStoreParameters + extends Omit< + InnerSimpleTreeViewParameters, + 'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemChildren' | 'getItemId' + > {} + +export interface SimpleTreeViewItem { + id: string; + label: string; + children?: SimpleTreeViewItem[]; +} diff --git a/packages/x-tree-view/src/internals/SimpleTreeViewStore/SimpleTreeViewStore.utils.ts b/packages/x-tree-view/src/internals/SimpleTreeViewStore/SimpleTreeViewStore.utils.ts new file mode 100644 index 0000000000000..9cd80d991f9c9 --- /dev/null +++ b/packages/x-tree-view/src/internals/SimpleTreeViewStore/SimpleTreeViewStore.utils.ts @@ -0,0 +1,13 @@ +import { TreeViewParametersToStateMapper } from '../MinimalTreeViewStore'; +import { InnerSimpleTreeViewParameters, SimpleTreeViewState } from './SimpleTreeViewStore.types'; + +export const parametersToStateMapper: TreeViewParametersToStateMapper< + any, + any, + SimpleTreeViewState, + InnerSimpleTreeViewParameters +> = { + getInitialState: (minimalInitialState) => minimalInitialState, + updateStateFromParameters: (newMinimalState) => newMinimalState, + shouldIgnoreItemsStateUpdate: () => true, +}; diff --git a/packages/x-tree-view/src/internals/SimpleTreeViewStore/index.ts b/packages/x-tree-view/src/internals/SimpleTreeViewStore/index.ts new file mode 100644 index 0000000000000..7e2e84d2176c2 --- /dev/null +++ b/packages/x-tree-view/src/internals/SimpleTreeViewStore/index.ts @@ -0,0 +1,2 @@ +export * from './SimpleTreeViewStore'; +export * from './SimpleTreeViewStore.types'; diff --git a/packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts b/packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts index 2a1fd01f21171..0dffd72c2b49e 100644 --- a/packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts +++ b/packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts @@ -1,9 +1,8 @@ 'use client'; import * as React from 'react'; import { TreeViewItemId } from '../../models'; -import { TreeViewState } from '../models'; -import type { UseTreeViewItemsSignature } from '../plugins/useTreeViewItems'; +import { MinimalTreeViewState } from '../MinimalTreeViewStore'; export const TreeViewItemDepthContext = React.createContext< - ((state: TreeViewState<[UseTreeViewItemsSignature]>, itemId: TreeViewItemId) => number) | number + ((state: MinimalTreeViewState, itemId: TreeViewItemId) => number) | number >(() => -1); diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx index 6046c9d9f457b..5cb3e3f0b52b0 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx @@ -3,9 +3,8 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useTreeViewContext } from './TreeViewContext'; import { escapeOperandAttributeSelector } from '../utils/utils'; -import type { UseTreeViewJSXItemsSignature } from '../plugins/useTreeViewJSXItems'; -import type { UseTreeViewItemsSignature } from '../plugins/useTreeViewItems'; -import { itemsSelectors } from '../plugins/useTreeViewItems/useTreeViewItems.selectors'; +import { itemsSelectors } from '../plugins/items/selectors'; +import { SimpleTreeViewStore } from '../SimpleTreeViewStore'; export const TreeViewChildrenItemContext = React.createContext(null); @@ -19,8 +18,7 @@ interface TreeViewChildrenItemProviderProps { export function TreeViewChildrenItemProvider(props: TreeViewChildrenItemProviderProps) { const { children, itemId = null, idAttribute } = props; - const { instance, store, rootRef } = - useTreeViewContext<[UseTreeViewJSXItemsSignature, UseTreeViewItemsSignature]>(); + const { store, rootRef } = useTreeViewContext>(); const childrenIdAttrToIdRef = React.useRef>(new Map()); React.useEffect(() => { @@ -53,7 +51,7 @@ export function TreeViewChildrenItemProvider(props: TreeViewChildrenItemProvider childrenIds.length !== previousChildrenIds.length || childrenIds.some((childId, index) => childId !== previousChildrenIds[index]); if (hasChanged) { - instance.setJSXItemsOrderedChildrenIds(itemId ?? null, childrenIds); + store.jsxItems.setJSXItemsOrderedChildrenIds(itemId ?? null, childrenIds); } }); diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewContext.ts b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewContext.ts index 37d979b2dca66..bb61bd7db105e 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewContext.ts +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewContext.ts @@ -1,6 +1,6 @@ 'use client'; import * as React from 'react'; -import { TreeViewAnyPluginSignature } from '../models'; +import { TreeViewAnyStore } from '../models'; import { TreeViewContextValue } from './TreeViewProvider.types'; /** @@ -8,14 +8,8 @@ import { TreeViewContextValue } from './TreeViewProvider.types'; */ export const TreeViewContext = React.createContext | null>(null); -export const useTreeViewContext = < - TSignatures extends readonly TreeViewAnyPluginSignature[], - TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [], ->() => { - const context = React.useContext(TreeViewContext) as TreeViewContextValue< - TSignatures, - TOptionalSignatures - >; +export const useTreeViewContext = () => { + const context = React.useContext(TreeViewContext) as TreeViewContextValue | null; if (context == null) { throw new Error( [ diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx index 063eadb037918..6b05e8559359f 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx @@ -1,27 +1,31 @@ import * as React from 'react'; +import { EMPTY_OBJECT } from '@base-ui-components/utils/empty'; import { TreeViewProviderProps } from './TreeViewProvider.types'; import { TreeViewContext } from './TreeViewContext'; -import { TreeViewAnyPluginSignature } from '../models'; import { TreeViewSlotProps, TreeViewSlots, TreeViewStyleContext } from './TreeViewStyleContext'; - -const EMPTY_OBJECT = {}; +import { useTreeViewBuildContext } from './useTreeViewBuildContext'; +import { TreeViewAnyStore } from '../models'; /** * Sets up the contexts for the underlying Tree Item components. * * @ignore - do not document. */ -export function TreeViewProvider( - props: TreeViewProviderProps, +export function TreeViewProvider( + props: TreeViewProviderProps, ) { const { - contextValue, + store, + apiRef, + rootRef, classes = EMPTY_OBJECT, slots = EMPTY_OBJECT as TreeViewSlots, slotProps = EMPTY_OBJECT as TreeViewSlotProps, children, } = props; + const contextValue = useTreeViewBuildContext({ store, apiRef, rootRef }); + const styleContextValue = React.useMemo( () => ({ classes, @@ -50,7 +54,7 @@ export function TreeViewProvider - {contextValue.wrapRoot({ children })} + {children} ); diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts index 2e5c17979efa5..e2c10343edf97 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts @@ -1,36 +1,40 @@ import * as React from 'react'; import { TreeItemWrapper, - TreeRootWrapper, - TreeViewAnyPluginSignature, - TreeViewInstance, TreeViewItemPluginResponse, TreeViewPublicAPI, - TreeViewReadonlyStore, + TreeViewAnyStore, } from '../models'; import type { TreeItemProps } from '../../TreeItem/TreeItem.types'; import { TreeViewClasses, TreeViewSlotProps, TreeViewSlots } from './TreeViewStyleContext'; -import { TreeViewCorePluginSignatures } from '../corePlugins'; +import { UseTreeViewBuildContextParameters } from './useTreeViewBuildContext'; export type TreeViewItemPluginsRunner = ( props: TreeItemProps, ) => Required; -export interface TreeViewContextValue< - TSignatures extends readonly TreeViewAnyPluginSignature[], - TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [], -> { - instance: TreeViewInstance; - publicAPI: TreeViewPublicAPI; - store: TreeViewReadonlyStore; +export type TreeViewStoreInContext = Omit< + TStore, + | 'setState' + | 'update' + | 'set' + | 'updateStateFromParameters' + | 'disposeEffect' + | 'registerStoreEffect' + | 'itemPluginManager' + | 'parameters' +>; + +export interface TreeViewContextValue { + publicAPI: TreeViewPublicAPI; + store: TreeViewStoreInContext; rootRef: React.RefObject; - wrapItem: TreeItemWrapper; - wrapRoot: TreeRootWrapper; + wrapItem: TreeItemWrapper; runItemPlugins: TreeViewItemPluginsRunner; } -export interface TreeViewProviderProps { - contextValue: TreeViewContextValue; +export interface TreeViewProviderProps + extends UseTreeViewBuildContextParameters { children: React.ReactNode; classes: Partial | undefined; slots: TreeViewSlots | undefined; diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewStyleContext.test.tsx b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewStyleContext.test.tsx index 1fba749174324..3101100d73c98 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewStyleContext.test.tsx +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewStyleContext.test.tsx @@ -2,9 +2,7 @@ import { describeTreeView, DescribeTreeViewRendererReturnValue, } from 'test/utils/tree-view/describeTreeView'; - -// TODO #20051: Replace with imported type -type TreeViewAnyStore = { parameters: any }; +import { TreeViewAnyStore } from '../models'; describeTreeView('TreeViewStyleContext component', ({ render }) => { describe('slots (expandIcon, collapseIcon, endIcon, icon)', () => { diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/index.ts b/packages/x-tree-view/src/internals/TreeViewProvider/index.ts index 1750b436ccd19..961955ccc168f 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/index.ts +++ b/packages/x-tree-view/src/internals/TreeViewProvider/index.ts @@ -4,4 +4,5 @@ export type { TreeViewProviderProps, TreeViewContextValue, TreeViewItemPluginsRunner, + TreeViewStoreInContext, } from './TreeViewProvider.types'; diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeViewBuildContext.ts b/packages/x-tree-view/src/internals/TreeViewProvider/useTreeViewBuildContext.ts similarity index 58% rename from packages/x-tree-view/src/internals/useTreeView/useTreeViewBuildContext.ts rename to packages/x-tree-view/src/internals/TreeViewProvider/useTreeViewBuildContext.ts index 17faaaa1031c6..b1a9aabf8b961 100644 --- a/packages/x-tree-view/src/internals/useTreeView/useTreeViewBuildContext.ts +++ b/packages/x-tree-view/src/internals/TreeViewProvider/useTreeViewBuildContext.ts @@ -1,22 +1,27 @@ import * as React from 'react'; -import { TreeViewContextValue, TreeViewItemPluginsRunner } from '../TreeViewProvider'; +import { useRefWithInit } from '@base-ui-components/utils/useRefWithInit'; +import { + TreeViewContextValue, + TreeViewItemPluginsRunner, + TreeViewStoreInContext, +} from './TreeViewProvider.types'; import { - ConvertSignaturesIntoPlugins, - TreeItemWrapper, - TreeRootWrapper, - TreeViewAnyPluginSignature, - TreeViewInstance, - TreeViewPublicAPI, TreeViewItemPluginSlotPropsEnhancers, TreeViewItemPluginSlotPropsEnhancerParams, - TreeViewStore, + TreeViewAnyStore, + TreeViewPublicAPI, + TreeItemWrapper, + TreeViewItemPlugin, } from '../models'; -import { TreeViewCorePluginSignatures } from '../corePlugins'; -export const useTreeViewBuildContext = ( - parameters: UseTreeViewBuildContextParameters, -): TreeViewContextValue => { - const { plugins, instance, publicAPI, store, rootRef } = parameters; +export const useTreeViewBuildContext = ( + parameters: UseTreeViewBuildContextParameters, +): TreeViewContextValue => { + const { store, apiRef, rootRef } = parameters; + + const publicAPI = useRefWithInit(() => store.buildPublicAPI()) + .current as TreeViewPublicAPI; + initializeInputApiRef(publicAPI, apiRef); const runItemPlugins = React.useCallback( (itemPluginProps) => { @@ -27,12 +32,8 @@ export const useTreeViewBuildContext = { - if (!plugin.itemPlugin) { - return; - } - - const itemPluginResponse = plugin.itemPlugin({ + store.itemPluginManager.listPlugins().forEach((itemPlugin: TreeViewItemPlugin) => { + const itemPluginResponse = itemPlugin({ props: itemPluginProps, rootRef: finalRootRef, contentRef: finalContentRef, @@ -86,68 +87,61 @@ export const useTreeViewBuildContext = >( + const wrapItem = React.useCallback>( ({ itemId, children, idAttribute }) => { let finalChildren: React.ReactNode = children; - // The wrappers are reversed to ensure that the first wrapper is the outermost one. - for (let i = plugins.length - 1; i >= 0; i -= 1) { - const plugin = plugins[i]; - if (plugin.wrapItem) { - finalChildren = plugin.wrapItem({ - instance, - itemId, - children: finalChildren, - idAttribute, - }); - } - } + const itemsWrapper = store.itemPluginManager.listWrappers(); - return finalChildren; - }, - [plugins, instance], - ); - - const wrapRoot = React.useCallback( - ({ children }) => { - let finalChildren: React.ReactNode = children; // The wrappers are reversed to ensure that the first wrapper is the outermost one. - for (let i = plugins.length - 1; i >= 0; i -= 1) { - const plugin = plugins[i]; - if (plugin.wrapRoot) { - finalChildren = plugin.wrapRoot({ - children: finalChildren, - }); - } + for (let i = itemsWrapper.length - 1; i >= 0; i -= 1) { + const itemWrapper = itemsWrapper[i]; + finalChildren = itemWrapper({ + store: store as any, + itemId, + children: finalChildren, + idAttribute, + }); } return finalChildren; }, - [plugins], + [store], ); return React.useMemo( () => ({ runItemPlugins, wrapItem, - wrapRoot, - instance, publicAPI, store, rootRef, }), - [runItemPlugins, wrapItem, wrapRoot, instance, publicAPI, store, rootRef], + [runItemPlugins, wrapItem, publicAPI, store, rootRef], ); }; -interface UseTreeViewBuildContextParameters< - TSignatures extends readonly TreeViewAnyPluginSignature[], -> { - plugins: ConvertSignaturesIntoPlugins; - instance: TreeViewInstance; - publicAPI: TreeViewPublicAPI; - store: TreeViewStore; +function initializeInputApiRef( + publicAPI: TreeViewPublicAPI, + apiRef: React.RefObject> | undefined> | undefined, +) { + if (apiRef != null && apiRef.current == null) { + apiRef.current = publicAPI; + } +} + +export interface UseTreeViewBuildContextParameters { + store: TStore; + rootRef: React.RefObject; + apiRef: React.RefObject> | undefined> | undefined; +} + +export interface UseTreeViewBuildContextReturnValue { + publicAPI: TreeViewPublicAPI; + store: TreeViewStoreInContext; rootRef: React.RefObject; + wrapItem: TreeItemWrapper; + runItemPlugins: TreeViewItemPluginsRunner; } diff --git a/packages/x-tree-view/src/internals/components/RichTreeViewItems.tsx b/packages/x-tree-view/src/internals/components/RichTreeViewItems.tsx index 4beb4db270861..b6560b61bb279 100644 --- a/packages/x-tree-view/src/internals/components/RichTreeViewItems.tsx +++ b/packages/x-tree-view/src/internals/components/RichTreeViewItems.tsx @@ -1,20 +1,21 @@ 'use client'; import * as React from 'react'; +import { EMPTY_ARRAY } from '@base-ui-components/utils/empty'; import { useStore } from '@mui/x-internals/store'; import useSlotProps from '@mui/utils/useSlotProps'; import { SlotComponentProps } from '@mui/utils/types'; import { fastObjectShallowCompare } from '@mui/x-internals/fastObjectShallowCompare'; import { TreeItem, TreeItemProps } from '../../TreeItem'; import { TreeViewItemId } from '../../models'; -import { itemsSelectors, UseTreeViewItemsSignature } from '../plugins/useTreeViewItems'; +import { itemsSelectors } from '../plugins/items'; import { useTreeViewContext } from '../TreeViewProvider'; -import { expansionSelectors, UseTreeViewExpansionSignature } from '../plugins/useTreeViewExpansion'; +import { expansionSelectors } from '../plugins/expansion'; +import { RichTreeViewStore } from '../RichTreeViewStore'; const RichTreeViewItemsContext = React.createContext< ((itemId: TreeViewItemId) => React.ReactNode) | null >(null); -const EMPTY_ARRAY: any[] = []; const selectorNoChildren = () => EMPTY_ARRAY; const WrappedTreeItem = React.memo(function WrappedTreeItem({ @@ -24,7 +25,7 @@ const WrappedTreeItem = React.memo(function WrappedTreeItem({ skipChildren, }: WrappedTreeItemProps) { const renderItemForRichTreeView = React.useContext(RichTreeViewItemsContext)!; - const { store } = useTreeViewContext<[UseTreeViewItemsSignature]>(); + const { store } = useTreeViewContext>(); const itemMeta = useStore(store, itemsSelectors.itemMeta, itemId); const children = useStore( @@ -46,8 +47,7 @@ const WrappedTreeItem = React.memo(function WrappedTreeItem({ export function RichTreeViewItems(props: RichTreeViewItemsProps) { const { slots, slotProps } = props; - const { store } = - useTreeViewContext<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>(); + const { store } = useTreeViewContext>(); const itemSlot = slots?.item as React.JSXElementConstructor | undefined; const itemSlotProps = slotProps?.item; diff --git a/packages/x-tree-view/src/internals/corePlugins/corePlugins.ts b/packages/x-tree-view/src/internals/corePlugins/corePlugins.ts deleted file mode 100644 index 76f050c33c8fa..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/corePlugins.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useTreeViewInstanceEvents } from './useTreeViewInstanceEvents'; -import { useTreeViewOptionalPlugins } from './useTreeViewOptionalPlugins'; -import { useTreeViewId, UseTreeViewIdParameters } from './useTreeViewId'; -import { ConvertPluginsIntoSignatures } from '../models'; - -/** - * Internal plugins that create the tools used by the other plugins. - * These plugins are used by the Tree View components. - */ -export const TREE_VIEW_CORE_PLUGINS = [ - useTreeViewInstanceEvents, - useTreeViewOptionalPlugins, - useTreeViewId, -] as const; - -export type TreeViewCorePluginSignatures = ConvertPluginsIntoSignatures< - typeof TREE_VIEW_CORE_PLUGINS ->; - -export interface TreeViewCorePluginParameters extends UseTreeViewIdParameters {} diff --git a/packages/x-tree-view/src/internals/corePlugins/index.ts b/packages/x-tree-view/src/internals/corePlugins/index.ts deleted file mode 100644 index 4bc6d8c4e5abd..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { TREE_VIEW_CORE_PLUGINS } from './corePlugins'; -export type { TreeViewCorePluginSignatures, TreeViewCorePluginParameters } from './corePlugins'; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/index.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/index.ts deleted file mode 100644 index 51be00c451ce1..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { useTreeViewId } from './useTreeViewId'; -export type { - UseTreeViewIdSignature, - UseTreeViewIdParameters, - UseTreeViewIdParametersWithDefaults, -} from './useTreeViewId.types'; -export { idSelectors } from './useTreeViewId.selectors'; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.selectors.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.selectors.ts deleted file mode 100644 index 43eacefed124d..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.selectors.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createSelector } from '@mui/x-internals/store'; -import { UseTreeViewIdSignature } from './useTreeViewId.types'; -import { TreeViewState } from '../../models'; - -export const idSelectors = { - /** - * Get the id attribute of the tree view. - * @param {TreeViewState<[UseTreeViewIdSignature]>} state The state of the tree view. - * @returns {string} The id attribute of the tree view. - */ - treeId: createSelector((state: TreeViewState<[UseTreeViewIdSignature]>) => state.id.treeId), -}; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.ts deleted file mode 100644 index 465fa926282ec..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.ts +++ /dev/null @@ -1,32 +0,0 @@ -'use client'; -import * as React from 'react'; -import { useStore } from '@mui/x-internals/store'; -import { TreeViewPlugin } from '../../models'; -import { UseTreeViewIdSignature } from './useTreeViewId.types'; -import { idSelectors } from './useTreeViewId.selectors'; -import { createTreeViewDefaultId } from './useTreeViewId.utils'; - -export const useTreeViewId: TreeViewPlugin = ({ params, store }) => { - React.useEffect(() => { - const prevIdState = store.state.id; - if (params.id === prevIdState.providedTreeId && prevIdState.treeId !== undefined) { - return; - } - - store.set('id', { ...prevIdState, treeId: params.id ?? createTreeViewDefaultId() }); - }, [store, params.id]); - - const treeId = useStore(store, idSelectors.treeId); - - return { - getRootProps: () => ({ - id: treeId, - }), - }; -}; - -useTreeViewId.params = { - id: true, -}; - -useTreeViewId.getInitialState = ({ id }) => ({ id: { treeId: undefined, providedTreeId: id } }); diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.types.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.types.ts deleted file mode 100644 index 9f804d2e9ea08..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.types.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { TreeViewPluginSignature } from '../../models'; - -export interface UseTreeViewIdParameters { - /** - * This prop is used to help implement the accessibility logic. - * If you don't provide this prop. It falls back to a randomly generated id. - */ - id?: string; -} - -export type UseTreeViewIdParametersWithDefaults = UseTreeViewIdParameters; - -export interface UseTreeViewIdState { - id: { - treeId: string | undefined; - providedTreeId: string | undefined; - }; -} - -export type UseTreeViewIdSignature = TreeViewPluginSignature<{ - params: UseTreeViewIdParameters; - paramsWithDefaults: UseTreeViewIdParametersWithDefaults; - state: UseTreeViewIdState; -}>; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.utils.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.utils.ts deleted file mode 100644 index f1cffe3debfd7..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.utils.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { TreeViewItemId } from '../../../models'; - -let globalTreeViewDefaultId = 0; -export const createTreeViewDefaultId = () => { - globalTreeViewDefaultId += 1; - return `mui-tree-view-${globalTreeViewDefaultId}`; -}; - -/** - * Generate the id attribute (i.e.: the `id` attribute passed to the DOM element) of a Tree Item. - * If the user explicitly defined an id attribute, it will be returned. - * Otherwise, the method creates a unique id for the item based on the Tree View id attribute and the item `itemId` - * @param {object} params The parameters to determine the id attribute of the item. - * @param {TreeViewItemId} params.itemId The id of the item to get the id attribute of. - * @param {string | undefined} params.idAttribute The id attribute of the item if explicitly defined by the user. - * @param {string} params.treeId The id attribute of the Tree View. - * @returns {string} The id attribute of the item. - */ -export const generateTreeItemIdAttribute = ({ - id, - treeId = '', - itemId, -}: { - id: TreeViewItemId | undefined; - treeId: string | undefined; - itemId: string; -}): string => { - if (id != null) { - return id; - } - - return `${treeId}-${itemId}`; -}; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewInstanceEvents/index.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewInstanceEvents/index.ts deleted file mode 100644 index ecb2d0f47bf5f..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewInstanceEvents/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useTreeViewInstanceEvents } from './useTreeViewInstanceEvents'; -export type { UseTreeViewInstanceEventsSignature } from './useTreeViewInstanceEvents.types'; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewInstanceEvents/useTreeViewInstanceEvents.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewInstanceEvents/useTreeViewInstanceEvents.ts deleted file mode 100644 index 580ba602f44a4..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewInstanceEvents/useTreeViewInstanceEvents.ts +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; -import * as React from 'react'; -import { EventManager } from '@mui/x-internals/EventManager'; -import type { TreeViewPlugin } from '../../models'; -import { UseTreeViewInstanceEventsSignature } from './useTreeViewInstanceEvents.types'; -import type { TreeViewEventListener } from '../../models/events'; - -const isSyntheticEvent = (event: any): event is React.SyntheticEvent => { - return event.isPropagationStopped !== undefined; -}; - -export const useTreeViewInstanceEvents: TreeViewPlugin = () => { - const [eventManager] = React.useState(() => new EventManager()); - - const publishEvent = React.useCallback( - (...args: any[]) => { - const [name, params, event = {}] = args; - event.defaultMuiPrevented = false; - - if (isSyntheticEvent(event) && event.isPropagationStopped()) { - return; - } - - eventManager.emit(name, params, event); - }, - [eventManager], - ); - - const subscribeEvent = React.useCallback( - (event: string, handler: TreeViewEventListener) => { - eventManager.on(event, handler); - return () => { - eventManager.removeListener(event, handler); - }; - }, - [eventManager], - ); - - return { - instance: { - $$publishEvent: publishEvent, - $$subscribeEvent: subscribeEvent, - }, - }; -}; - -useTreeViewInstanceEvents.params = {}; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewInstanceEvents/useTreeViewInstanceEvents.types.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewInstanceEvents/useTreeViewInstanceEvents.types.ts deleted file mode 100644 index 958a8c023af6d..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewInstanceEvents/useTreeViewInstanceEvents.types.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { TreeViewPluginSignature } from '../../models'; -import { TreeViewEventListener } from '../../models/events'; - -export interface UseTreeViewInstanceEventsInstance { - /** - * Should never be used directly. - * Please use `useInstanceEventHandler` instead. - * @param {string} eventName Name of the event to subscribe to. - * @param {TreeViewEventListener} handler Event handler to call when the event is published. - * @returns {() => void} Cleanup function. - */ - $$subscribeEvent: (eventName: string, handler: TreeViewEventListener) => () => void; - /** - * Should never be used directly. - * Please use `publishTreeViewEvent` instead. - * @param {string} eventName Name of the event to publish. - * @param {any} params The params to publish with the event. - */ - $$publishEvent: (eventName: string, params: any) => void; -} - -export type UseTreeViewInstanceEventsSignature = TreeViewPluginSignature<{ - instance: UseTreeViewInstanceEventsInstance; -}>; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewOptionalPlugins/index.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewOptionalPlugins/index.ts deleted file mode 100644 index a10084128f63c..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewOptionalPlugins/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useTreeViewOptionalPlugins } from './useTreeViewOptionalPlugins'; -export type { UseTreeViewOptionalPluginsSignature } from './useTreeViewOptionalPlugins.types'; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewOptionalPlugins/useTreeViewOptionalPlugins.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewOptionalPlugins/useTreeViewOptionalPlugins.ts deleted file mode 100644 index 6eadb1c88684f..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewOptionalPlugins/useTreeViewOptionalPlugins.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { TreeViewPlugin } from '../../models'; -import { UseTreeViewOptionalPluginsSignature } from './useTreeViewOptionalPlugins.types'; - -export const useTreeViewOptionalPlugins: TreeViewPlugin = ({ - plugins, -}) => { - const pluginSet = new Set(plugins); - const getAvailablePlugins = () => pluginSet; - - return { - instance: { - getAvailablePlugins, - }, - }; -}; - -useTreeViewOptionalPlugins.params = {}; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewOptionalPlugins/useTreeViewOptionalPlugins.types.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewOptionalPlugins/useTreeViewOptionalPlugins.types.ts deleted file mode 100644 index a88f14e9e0417..0000000000000 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewOptionalPlugins/useTreeViewOptionalPlugins.types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { TreeViewPlugin, TreeViewAnyPluginSignature, TreeViewPluginSignature } from '../../models'; - -interface UseTreeViewOptionalPluginsInstance { - getAvailablePlugins: () => Set>; -} - -export type UseTreeViewOptionalPluginsSignature = TreeViewPluginSignature<{ - instance: UseTreeViewOptionalPluginsInstance; -}>; diff --git a/packages/x-tree-view/src/internals/hooks/useInstanceEventHandler.ts b/packages/x-tree-view/src/internals/hooks/useInstanceEventHandler.ts deleted file mode 100644 index 962db7ba98c30..0000000000000 --- a/packages/x-tree-view/src/internals/hooks/useInstanceEventHandler.ts +++ /dev/null @@ -1,120 +0,0 @@ -'use client'; -import * as React from 'react'; -import { UnregisterToken, CleanupTracking } from '../utils/cleanupTracking/CleanupTracking'; -import { TimerBasedCleanupTracking } from '../utils/cleanupTracking/TimerBasedCleanupTracking'; -import { FinalizationRegistryBasedCleanupTracking } from '../utils/cleanupTracking/FinalizationRegistryBasedCleanupTracking'; -import { TreeViewAnyPluginSignature, TreeViewUsedEvents } from '../models'; -import { TreeViewEventListener } from '../models/events'; -import { UseTreeViewInstanceEventsInstance } from '../corePlugins/useTreeViewInstanceEvents/useTreeViewInstanceEvents.types'; - -interface RegistryContainer { - registry: CleanupTracking | null; -} - -// We use class to make it easier to detect in heap snapshots by name -class ObjectToBeRetainedByReact {} - -// Based on https://github.com/Bnaya/use-dispose-uncommitted/blob/main/src/finalization-registry-based-impl.ts -// Check https://github.com/facebook/react/issues/15317 to get more information -export function createUseInstanceEventHandler(registryContainer: RegistryContainer) { - let cleanupTokensCounter = 0; - - return function useInstanceEventHandler< - Instance extends UseTreeViewInstanceEventsInstance & { - $$signature: TreeViewAnyPluginSignature; - }, - E extends keyof TreeViewUsedEvents, - >( - instance: Instance, - eventName: E, - handler: TreeViewEventListener[E]>, - ) { - type Signature = Instance['$$signature']; - - if (registryContainer.registry === null) { - registryContainer.registry = - typeof FinalizationRegistry !== 'undefined' - ? new FinalizationRegistryBasedCleanupTracking() - : new TimerBasedCleanupTracking(); - } - - const [objectRetainedByReact] = React.useState(new ObjectToBeRetainedByReact()); - const subscription = React.useRef<(() => void) | null>(null); - const handlerRef = React.useRef< - TreeViewEventListener[E]> | undefined - >(undefined); - handlerRef.current = handler; - const cleanupTokenRef = React.useRef(null); - - if (!subscription.current && handlerRef.current) { - const enhancedHandler: TreeViewEventListener[E]> = ( - params, - event, - ) => { - if (!event.defaultMuiPrevented) { - handlerRef.current?.(params, event); - } - }; - - subscription.current = instance.$$subscribeEvent(eventName as string, enhancedHandler); - - cleanupTokensCounter += 1; - cleanupTokenRef.current = { cleanupToken: cleanupTokensCounter }; - - registryContainer.registry.register( - objectRetainedByReact, // The callback below will be called once this reference stops being retained - () => { - subscription.current?.(); - subscription.current = null; - cleanupTokenRef.current = null; - }, - cleanupTokenRef.current, - ); - } else if (!handlerRef.current && subscription.current) { - subscription.current(); - subscription.current = null; - - if (cleanupTokenRef.current) { - registryContainer.registry.unregister(cleanupTokenRef.current); - cleanupTokenRef.current = null; - } - } - - React.useEffect(() => { - if (!subscription.current && handlerRef.current) { - const enhancedHandler: TreeViewEventListener[E]> = ( - params, - event, - ) => { - if (!event.defaultMuiPrevented) { - handlerRef.current?.(params, event); - } - }; - - subscription.current = instance.$$subscribeEvent(eventName as string, enhancedHandler); - } - - if (cleanupTokenRef.current && registryContainer.registry) { - // If the effect was called, it means that this render was committed - // so we can trust the cleanup function to remove the listener. - registryContainer.registry.unregister(cleanupTokenRef.current); - cleanupTokenRef.current = null; - } - - return () => { - subscription.current?.(); - subscription.current = null; - }; - }, [instance, eventName]); - }; -} - -const registryContainer: RegistryContainer = { registry: null }; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const unstable_resetCleanupTracking = () => { - registryContainer.registry?.reset(); - registryContainer.registry = null; -}; - -export const useInstanceEventHandler = createUseInstanceEventHandler(registryContainer); diff --git a/packages/x-tree-view/src/internals/hooks/useTreeViewRootProps.ts b/packages/x-tree-view/src/internals/hooks/useTreeViewRootProps.ts new file mode 100644 index 0000000000000..9fe38de3fde8e --- /dev/null +++ b/packages/x-tree-view/src/internals/hooks/useTreeViewRootProps.ts @@ -0,0 +1,43 @@ +'use client'; +import * as React from 'react'; +import { useStore } from '@mui/x-internals/store'; +import { EventHandlers } from '@mui/utils/types'; +import { TreeViewCancellableEvent } from '../../models'; +import { idSelectors } from '../plugins/id'; +import { selectionSelectors } from '../plugins/selection'; +import { itemsSelectors } from '../plugins/items'; +import { TreeViewAnyStore } from '../models'; + +export function useTreeViewRootProps( + store: TStore, + forwardedProps: React.HTMLAttributes, + ref: React.Ref | undefined, +) { + const treeId = useStore(store, idSelectors.treeId); + const itemChildrenIndentation = useStore(store, itemsSelectors.itemChildrenIndentation); + const isMultiSelectEnabled = useStore(store, selectionSelectors.isMultiSelectEnabled); + + return (otherHandlers: EventHandlers) => ({ + ref, + role: 'tree', + id: treeId, + 'aria-multiselectable': isMultiSelectEnabled, + ...forwardedProps, + ...otherHandlers, + style: { + ...forwardedProps.style, + '--TreeView-itemChildrenIndentation': + typeof itemChildrenIndentation === 'number' + ? `${itemChildrenIndentation}px` + : itemChildrenIndentation, + } as React.CSSProperties, + onFocus: (event: React.FocusEvent & TreeViewCancellableEvent) => { + otherHandlers.onFocus?.(event); + store.focus.handleRootFocus(event); + }, + onBlur: (event: React.FocusEvent & TreeViewCancellableEvent) => { + otherHandlers.onBlur?.(event); + store.focus.handleRootBlur(event); + }, + }); +} diff --git a/packages/x-tree-view/src/internals/hooks/useTreeViewStore.ts b/packages/x-tree-view/src/internals/hooks/useTreeViewStore.ts new file mode 100644 index 0000000000000..3328086c25dcd --- /dev/null +++ b/packages/x-tree-view/src/internals/hooks/useTreeViewStore.ts @@ -0,0 +1,34 @@ +import { useRefWithInit } from '@base-ui-components/utils/useRefWithInit'; +import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; +import { useOnMount } from '@base-ui-components/utils/useOnMount'; +import { useRtl } from '@mui/system/RtlProvider'; +import { TreeViewAnyStore } from '../models'; + +interface ValidTreeViewStoreConstructor { + new (parameters: TStore['parameters']): TStore; +} + +export type UseTreeViewStoreParameters = Omit< + Parameters[0], + 'isRtl' +>; + +/** + * Creates a Tree View store and keep it in sync with the provided parameters. + */ +export function useTreeViewStore( + StoreClass: ValidTreeViewStoreConstructor, + parameters: UseTreeViewStoreParameters, +): TStore { + const isRtl = useRtl(); + const store = useRefWithInit(() => new StoreClass({ ...parameters, isRtl })).current; + + useIsoLayoutEffect( + () => store.updateStateFromParameters({ ...parameters, isRtl }), + [store, isRtl, parameters], + ); + + useOnMount(store.disposeEffect); + + return store; +} diff --git a/packages/x-tree-view/src/internals/index.ts b/packages/x-tree-view/src/internals/index.ts index d204b1ea6f63e..801fc9eb5a693 100644 --- a/packages/x-tree-view/src/internals/index.ts +++ b/packages/x-tree-view/src/internals/index.ts @@ -1,4 +1,3 @@ -export { useTreeView } from './useTreeView'; export { TreeViewProvider, useTreeViewContext } from './TreeViewProvider'; export { RichTreeViewItems } from './components/RichTreeViewItems'; @@ -7,75 +6,24 @@ export type { RichTreeViewItemsSlotProps, } from './components/RichTreeViewItems'; -export { - unstable_resetCleanupTracking, - useInstanceEventHandler, -} from './hooks/useInstanceEventHandler'; +export { useTreeViewRootProps } from './hooks/useTreeViewRootProps'; +export { useTreeViewStore } from './hooks/useTreeViewStore'; +export type { UseTreeViewStoreParameters } from './hooks/useTreeViewStore'; export type { - TreeViewPlugin, - TreeViewPluginSignature, - ConvertPluginsIntoSignatures, - MergeSignaturesProperty, - TreeViewPublicAPI, - TreeViewState, TreeViewItemMeta, - TreeViewInstance, TreeViewItemPlugin, - TreeViewUsedStore, - TreeViewUsedInstance, - TreeViewUsedParamsWithDefaults, + TreeViewEventParameters, + TreeViewEventEvent, + TreeViewPublicAPI, } from './models'; -// Core plugins -export type { TreeViewCorePluginParameters } from './corePlugins'; - // Plugins -export { useTreeViewExpansion, expansionSelectors } from './plugins/useTreeViewExpansion'; -export type { - UseTreeViewExpansionSignature, - UseTreeViewExpansionParameters, -} from './plugins/useTreeViewExpansion'; -export { useTreeViewSelection, selectionSelectors } from './plugins/useTreeViewSelection'; -export type { - UseTreeViewSelectionSignature, - UseTreeViewSelectionParameters, -} from './plugins/useTreeViewSelection'; -export { useTreeViewFocus } from './plugins/useTreeViewFocus'; -export type { - UseTreeViewFocusSignature, - UseTreeViewFocusParameters, -} from './plugins/useTreeViewFocus'; -export { useTreeViewKeyboardNavigation } from './plugins/useTreeViewKeyboardNavigation'; -export type { UseTreeViewKeyboardNavigationSignature } from './plugins/useTreeViewKeyboardNavigation'; -export { - useTreeViewItems, - buildSiblingIndexes, - itemsSelectors, - TREE_VIEW_ROOT_PARENT_ID, -} from './plugins/useTreeViewItems'; -export type { - UseTreeViewItemsSignature, - UseTreeViewItemsParameters, - UseTreeViewItemsState, -} from './plugins/useTreeViewItems'; -export { useTreeViewLabel, labelSelectors } from './plugins/useTreeViewLabel'; -export type { - UseTreeViewLabelSignature, - UseTreeViewLabelParameters, -} from './plugins/useTreeViewLabel'; -export type { - UseTreeViewLazyLoadingSignature, - UseTreeViewLazyLoadingParameters, - UseTreeViewLazyLoadingInstance, - DataSource, -} from './plugins/useTreeViewLazyLoading'; -export { lazyLoadingSelectors } from './plugins/useTreeViewLazyLoading'; -export { useTreeViewJSXItems } from './plugins/useTreeViewJSXItems'; -export type { - UseTreeViewJSXItemsSignature, - UseTreeViewJSXItemsParameters, -} from './plugins/useTreeViewJSXItems'; +export { expansionSelectors } from './plugins/expansion'; +export { selectionSelectors } from './plugins/selection'; +export { buildSiblingIndexes, itemsSelectors, TREE_VIEW_ROOT_PARENT_ID } from './plugins/items'; +export { labelSelectors } from './plugins/labelEditing'; +export { lazyLoadingSelectors } from './plugins/lazyLoading'; export { isTargetInDescendants } from './utils/tree'; @@ -84,3 +32,11 @@ export type { TreeViewSlots, TreeViewSlotProps, } from './TreeViewProvider/TreeViewStyleContext'; + +export { MinimalTreeViewStore } from './MinimalTreeViewStore'; +export type { TreeViewParametersToStateMapper } from './MinimalTreeViewStore'; + +export { ExtendableRichTreeViewStore } from './RichTreeViewStore'; +export type { RichTreeViewState, RichTreeViewStoreParameters } from './RichTreeViewStore'; + +export { TreeViewItemDepthContext } from './TreeViewItemDepthContext'; diff --git a/packages/x-tree-view/src/internals/models/events.ts b/packages/x-tree-view/src/internals/models/events.ts index 35c1897c066b3..1d3bcf4b6dd1e 100644 --- a/packages/x-tree-view/src/internals/models/events.ts +++ b/packages/x-tree-view/src/internals/models/events.ts @@ -1,10 +1,37 @@ import { MuiEvent } from '@mui/x-internals/types'; +import { TreeViewItemId } from '../../models'; -export interface TreeViewEventLookupElement { - params: object; +interface TreeViewEventLookup { + /** + * Fired before an item is expanded or collapsed. + */ + beforeItemToggleExpansion: { + parameters: { + isExpansionPrevented: boolean; + shouldBeExpanded: boolean; + itemId: TreeViewItemId; + }; + event: React.SyntheticEvent | null; + }; } -export type TreeViewEventListener = ( - params: E['params'], - event: MuiEvent<{}>, +export type TreeViewEvents = keyof TreeViewEventLookup; + +export type TreeViewEventListener = ( + params: TreeViewEventParameters, + event: TreeViewEventLookup[E] extends { event: any } + ? MuiEvent + : MuiEvent<{}>, ) => void; + +export type TreeViewEventParameters = TreeViewEventLookup[E] extends { + parameters: infer P; +} + ? P + : undefined; + +export type TreeViewEventEvent = TreeViewEventLookup[E] extends { + event: infer EV; +} + ? EV + : undefined; diff --git a/packages/x-tree-view/src/internals/models/helpers.ts b/packages/x-tree-view/src/internals/models/helpers.ts deleted file mode 100644 index 3584b2dd26d10..0000000000000 --- a/packages/x-tree-view/src/internals/models/helpers.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { TreeViewAnyPluginSignature, TreeViewPlugin } from './plugin'; - -type IsAny = 0 extends 1 & T ? true : false; - -export type OptionalIfEmpty = keyof B extends never - ? Partial> - : IsAny extends true - ? Partial> - : Record; - -export type MergeSignaturesProperty< - TSignatures extends readonly any[], - TProperty extends keyof TreeViewAnyPluginSignature, -> = TSignatures extends readonly [plugin: infer P, ...otherPlugin: infer R] - ? P extends TreeViewAnyPluginSignature - ? P[TProperty] & MergeSignaturesProperty - : {} - : {}; - -export type ConvertPluginsIntoSignatures< - TPlugins extends readonly TreeViewPlugin[], -> = TPlugins extends readonly [plugin: infer TPlugin, ...otherPlugin: infer R] - ? R extends readonly TreeViewPlugin[] - ? TPlugin extends TreeViewPlugin - ? readonly [TSignature, ...ConvertPluginsIntoSignatures] - : never - : never - : []; - -export type ConvertSignaturesIntoPlugins< - TSignatures extends readonly TreeViewAnyPluginSignature[], -> = TSignatures extends readonly [signature: infer TSignature, ...otherSignatures: infer R] - ? R extends readonly TreeViewAnyPluginSignature[] - ? TSignature extends TreeViewAnyPluginSignature - ? readonly [TreeViewPlugin, ...ConvertSignaturesIntoPlugins] - : never - : never - : []; diff --git a/packages/x-tree-view/src/internals/models/index.ts b/packages/x-tree-view/src/internals/models/index.ts index 9095cd823af3f..f94d332fc6f81 100644 --- a/packages/x-tree-view/src/internals/models/index.ts +++ b/packages/x-tree-view/src/internals/models/index.ts @@ -1,4 +1,3 @@ -export * from './helpers'; -export * from './plugin'; export * from './itemPlugin'; export * from './treeView'; +export * from './events'; diff --git a/packages/x-tree-view/src/internals/models/itemPlugin.ts b/packages/x-tree-view/src/internals/models/itemPlugin.ts index b2c6bde80a283..a55832543d068 100644 --- a/packages/x-tree-view/src/internals/models/itemPlugin.ts +++ b/packages/x-tree-view/src/internals/models/itemPlugin.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import { EventHandlers } from '@mui/utils/types'; +import { TreeViewItemId } from '../../models'; import type { UseTreeItemContentSlotOwnProps, UseTreeItemDragAndDropOverlaySlotOwnProps, @@ -10,6 +11,8 @@ import type { } from '../../useTreeItem'; import type { UseTreeItemInteractions } from '../../hooks/useTreeItemUtils/useTreeItemUtils'; import type { TreeItemProps } from '../../TreeItem/TreeItem.types'; +import { TreeViewAnyStore } from './treeView'; +import { TreeViewStoreInContext } from '../TreeViewProvider'; export interface TreeViewItemPluginSlotPropsEnhancerParams { rootRefObject: React.RefObject; @@ -58,3 +61,10 @@ export interface TreeViewItemPluginOptions export type TreeViewItemPlugin = ( options: TreeViewItemPluginOptions, ) => void | TreeViewItemPluginResponse; + +export type TreeItemWrapper = (params: { + itemId: TreeViewItemId; + children: React.ReactNode; + store: TreeViewStoreInContext; + idAttribute: string; +}) => React.ReactNode; diff --git a/packages/x-tree-view/src/internals/models/plugin.ts b/packages/x-tree-view/src/internals/models/plugin.ts deleted file mode 100644 index ef6b3ae734a0d..0000000000000 --- a/packages/x-tree-view/src/internals/models/plugin.ts +++ /dev/null @@ -1,183 +0,0 @@ -import * as React from 'react'; -import { EventHandlers } from '@mui/utils/types'; -import { TreeViewInstance, TreeViewStore } from './treeView'; -import type { MergeSignaturesProperty, OptionalIfEmpty } from './helpers'; -import { TreeViewEventLookupElement } from './events'; -import type { TreeViewCorePluginSignatures } from '../corePlugins'; -import { TreeViewItemPlugin } from './itemPlugin'; -import { TreeViewItemId } from '../../models'; - -export interface TreeViewPluginOptions { - /** - * An imperative API available for internal use. Used to access methods from other plugins. - */ - instance: TreeViewUsedInstance; - /** - * The Tree View parameters after being processed with the default values. - */ - params: TreeViewUsedParamsWithDefaults; - /** - * The store that can be used to access the state of other plugins. - */ - store: TreeViewUsedStore; - /** - * Reference to the root element. - */ - rootRef: React.RefObject; - /** - * All the plugins that are used in the tree-view. - */ - plugins: TreeViewPlugin[]; -} - -type TreeViewResponse = { - getRootProps?: ( - otherHandlers: TOther, - ) => React.HTMLAttributes; -} & OptionalIfEmpty<'publicAPI', TSignature['publicAPI']> & - OptionalIfEmpty<'instance', TSignature['instance']>; - -export type TreeViewPluginSignature< - T extends { - params?: {}; - paramsWithDefaults?: {}; - instance?: {}; - publicAPI?: {}; - events?: { [key in keyof T['events']]: TreeViewEventLookupElement }; - state?: {}; - dependencies?: readonly TreeViewAnyPluginSignature[]; - optionalDependencies?: readonly TreeViewAnyPluginSignature[]; - }, -> = { - /** - * The raw properties that can be passed to the plugin. - */ - params: T extends { params: {} } ? T['params'] : {}; - /** - * The params after being processed with the default values. - */ - paramsWithDefaults: T extends { paramsWithDefaults: {} } ? T['paramsWithDefaults'] : {}; - /** - * An imperative api available for internal use. - */ - instance: T extends { instance: {} } ? T['instance'] : {}; - /** - * The public imperative API that will be exposed to the user. - * Accessed through the `apiRef` property of the plugin. - */ - publicAPI: T extends { publicAPI: {} } ? T['publicAPI'] : {}; - events: T extends { events: {} } ? T['events'] : {}; - /** - * The state is the mutable data that will actually be stored in the plugin state and can be accessed by other plugins. - */ - state: T extends { state: {} } ? T['state'] : {}; - /** - * Any plugins that this plugin depends on. - */ - dependencies: T extends { dependencies: Array } ? T['dependencies'] : []; - /** - * Same as dependencies but the plugin might not have been initialized. Used for dependencies on plugins of features that can be enabled conditionally. - */ - optionalDependencies: T extends { optionalDependencies: Array } - ? T['optionalDependencies'] - : []; -}; - -export type TreeViewAnyPluginSignature = { - state: any; - instance: any; - params: any; - paramsWithDefaults: any; - dependencies: any; - optionalDependencies: any; - events: any; - publicAPI: any; -}; - -type TreeViewRequiredPlugins = [ - ...TreeViewCorePluginSignatures, - ...TSignature['dependencies'], -]; - -type PluginPropertyWithDependencies< - TSignature extends TreeViewAnyPluginSignature, - TProperty extends keyof TreeViewAnyPluginSignature, -> = TSignature[TProperty] & - MergeSignaturesProperty, TProperty> & - Partial>; - -export type TreeViewUsedParams = - PluginPropertyWithDependencies; - -export type TreeViewUsedParamsWithDefaults = - PluginPropertyWithDependencies; - -export type TreeViewUsedInstance = - PluginPropertyWithDependencies & { - /** - * Private property only defined in TypeScript to be able to access the plugin signature from the instance object. - */ - $$signature: TSignature; - }; - -export type TreeViewUsedStore = TreeViewStore< - [TSignature, ...TSignature['dependencies']], - TSignature['optionalDependencies'] ->; - -export type TreeViewUsedEvents = - TSignature['events'] & MergeSignaturesProperty, 'events'>; - -export type TreeItemWrapper = (params: { - itemId: TreeViewItemId; - children: React.ReactNode; - instance: TreeViewInstance; - idAttribute: string; -}) => React.ReactNode; - -export type TreeRootWrapper = (params: { children: React.ReactNode }) => React.ReactNode; - -export type TreeViewPlugin = { - /** - * The main function of the plugin that will be executed by the Tree View. - * - * This should be a valid React `use` function, as it will be executed in the render phase and can contain hooks. - */ - (options: TreeViewPluginOptions): TreeViewResponse; - /** - * A function that receives the parameters and returns them after being processed with the default values. - * - * @param {TreeViewUsedParams} options The options object. - * @param {TreeViewUsedParams['params']} options.params The parameters before being processed with the default values. - * @returns {TSignature['paramsWithDefaults']} The parameters after being processed with the default values. - */ - applyDefaultValuesToParams?: (options: { - params: TreeViewUsedParams; - }) => TSignature['paramsWithDefaults']; - /** - * The initial state is computed after the default values are applied. - * It sets up the state for the first render. - * Other state modifications have to be done in effects and so could not be applied on the initial render. - * - * @param {TreeViewUsedParamsWithDefaults} params The parameters after being processed with the default values. - * @returns {TSignature['state']} The initial state of the plugin. - */ - getInitialState?: (params: TreeViewUsedParamsWithDefaults) => TSignature['state']; - /** - * An object where each property used by the plugin is set to `true`. - */ - params: Record; - itemPlugin?: TreeViewItemPlugin; - /** - * Render function used to add React wrappers around the TreeItem. - * @param {{ nodeId: TreeViewItemId; children: React.ReactNode; }} params The params of the item. - * @returns {React.ReactNode} The wrapped item. - */ - wrapItem?: TreeItemWrapper<[TSignature, ...TSignature['dependencies']]>; - /** - * Render function used to add React wrappers around the TreeView. - * @param {{ children: React.ReactNode; }} params The params of the root. - * @returns {React.ReactNode} The wrapped root. - */ - wrapRoot?: TreeRootWrapper; -}; diff --git a/packages/x-tree-view/src/internals/models/treeView.ts b/packages/x-tree-view/src/internals/models/treeView.ts index 079b35d57210b..353e81fa42d68 100644 --- a/packages/x-tree-view/src/internals/models/treeView.ts +++ b/packages/x-tree-view/src/internals/models/treeView.ts @@ -1,7 +1,4 @@ -import { ReadonlyStore, Store } from '@mui/x-internals/store'; -import type { TreeViewAnyPluginSignature } from './plugin'; -import type { MergeSignaturesProperty } from './helpers'; -import type { TreeViewCorePluginSignatures } from '../corePlugins'; +import { MinimalTreeViewStore } from '../MinimalTreeViewStore'; export interface TreeViewItemMeta { id: string; @@ -19,30 +16,10 @@ export interface TreeViewItemMeta { label?: string; } -export type TreeViewInstance< - TSignatures extends readonly TreeViewAnyPluginSignature[], - TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [], -> = MergeSignaturesProperty<[...TreeViewCorePluginSignatures, ...TSignatures], 'instance'> & - Partial>; - -export type TreeViewPublicAPI< - TSignatures extends readonly TreeViewAnyPluginSignature[], - TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [], -> = MergeSignaturesProperty<[...TreeViewCorePluginSignatures, ...TSignatures], 'publicAPI'> & - Partial>; - -export type TreeViewState< - TSignatures extends readonly TreeViewAnyPluginSignature[], - TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [], -> = MergeSignaturesProperty<[...TreeViewCorePluginSignatures, ...TSignatures], 'state'> & - Partial>; - -export type TreeViewStore< - TSignatures extends readonly TreeViewAnyPluginSignature[], - TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [], -> = Store>; +export interface TreeViewAnyStore extends MinimalTreeViewStore { + itemPluginManager: any; +} -export type TreeViewReadonlyStore< - TSignatures extends readonly TreeViewAnyPluginSignature[], - TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [], -> = ReadonlyStore>; +export type TreeViewPublicAPI = ReturnType< + TStore['buildPublicAPI'] +>; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx b/packages/x-tree-view/src/internals/plugins/expansion/TreeViewExpansionPlugin.test.tsx similarity index 97% rename from packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx rename to packages/x-tree-view/src/internals/plugins/expansion/TreeViewExpansionPlugin.test.tsx index 70d32b406f68a..8c2eb4e70d302 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/expansion/TreeViewExpansionPlugin.test.tsx @@ -6,9 +6,7 @@ import { TreeItem, TreeItemProps } from '@mui/x-tree-view/TreeItem'; import { UseTreeItemContentSlotOwnProps } from '@mui/x-tree-view/useTreeItem'; import { useTreeItemUtils } from '@mui/x-tree-view/hooks'; import { clearWarningsCache } from '@mui/x-internals/warning'; - -// TODO #20051: Replace with imported type -type TreeViewAnyStore = { parameters: any }; +import { TreeViewAnyStore } from '../../models'; /** * All tests related to keyboard navigation (e.g.: expanding using "Enter" and "ArrowRight") @@ -16,8 +14,6 @@ type TreeViewAnyStore = { parameters: any }; */ describeTreeView( 'TreeViewExpansionPlugin', - // TODO #20051: Remove next line - // eslint-disable-next-line @typescript-eslint/no-unused-vars ({ render, treeViewComponentName }) => { describe('model props (expandedItems, defaultExpandedItems, onExpandedItemsChange)', () => { beforeEach(() => { @@ -132,7 +128,7 @@ describeTreeView( expect(() => { view.setProps({ expandedItems: undefined }); }).toErrorDev( - 'MUI X: A component is changing the controlled expandedItems state of Tree View to be uncontrolled.', + `MUI X Tree View: A component is changing the controlled expandedItems state of ${treeViewComponentName} to be uncontrolled.`, ); }); @@ -147,7 +143,7 @@ describeTreeView( expect(view.isItemExpanded('1')).to.equal(true); expect(view.isItemExpanded('2')).to.equal(false); }).toErrorDev( - 'MUI X: A component is changing the default expandedItems state of an uncontrolled Tree View after being initialized', + `MUI X Tree View: A component is changing the default expandedItems state of an uncontrolled ${treeViewComponentName} after being initialized.`, ); }); }); diff --git a/packages/x-tree-view/src/internals/plugins/expansion/TreeViewExpansionPlugin.ts b/packages/x-tree-view/src/internals/plugins/expansion/TreeViewExpansionPlugin.ts new file mode 100644 index 0000000000000..50b0861f37102 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/expansion/TreeViewExpansionPlugin.ts @@ -0,0 +1,146 @@ +import { TreeViewItemId } from '../../../models'; +import { expansionSelectors } from './selectors'; +import { itemsSelectors } from '../items/selectors'; +import { MinimalTreeViewStore } from '../../MinimalTreeViewStore'; +import { TreeViewEventParameters } from '../../models'; + +export class TreeViewExpansionPlugin { + private store: MinimalTreeViewStore; + + // We can't type `store`, otherwise we get the following TS error: + // 'expansion' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. + constructor(store: any) { + this.store = store; + } + + private setExpandedItems = (event: React.SyntheticEvent | null, value: TreeViewItemId[]) => { + if (this.store.parameters.expandedItems === undefined) { + this.store.set('expandedItems', value); + } + this.store.parameters.onExpandedItemsChange?.(event, value); + }; + + /** + * Check if an item is expanded. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item is expanded, `false` otherwise. + */ + private isItemExpanded = (itemId: TreeViewItemId) => + expansionSelectors.isItemExpanded(this.store.state, itemId); + + public buildPublicAPI = () => { + return { + isItemExpanded: this.isItemExpanded, + setItemExpansion: this.setItemExpansion, + }; + }; + + /** + * Change the expansion status of a given item. + * @param {object} parameters The parameters of the method. + * @param {TreeViewItemId} parameters.itemId The id of the item to expand of collapse. + * @param {React.SyntheticEvent} parameters.event The DOM event that triggered the change. + * @param {boolean} parameters.shouldBeExpanded If `true` the item will be expanded. If `false` the item will be collapsed. If not defined, the item's expansion status will be the toggled. + */ + public setItemExpansion = ({ + itemId, + event = null, + shouldBeExpanded, + }: { + itemId: TreeViewItemId; + event?: React.SyntheticEvent | null; + shouldBeExpanded?: boolean; + }) => { + const isExpandedBefore = expansionSelectors.isItemExpanded(this.store.state, itemId); + const cleanShouldBeExpanded = shouldBeExpanded ?? !isExpandedBefore; + if (isExpandedBefore === cleanShouldBeExpanded) { + return; + } + + const eventParameters: TreeViewEventParameters<'beforeItemToggleExpansion'> = { + isExpansionPrevented: false, + shouldBeExpanded: cleanShouldBeExpanded, + itemId, + }; + this.store.publishEvent('beforeItemToggleExpansion', eventParameters, event); + if (eventParameters.isExpansionPrevented) { + return; + } + + this.applyItemExpansion({ itemId, event, shouldBeExpanded: cleanShouldBeExpanded }); + }; + + /** + * Apply the new expansion status of a given item. + * Is used by the `setItemExpansion` method and by the `useTreeViewLazyLoading` plugin. + * Unlike `setItemExpansion`, this method does not trigger the lazy loading. + * @param {object} parameters The parameters of the method. + * @param {TreeViewItemId} parameters.itemId The id of the item to expand of collapse. + * @param {React.SyntheticEvent | null} parameters.event The DOM event that triggered the change. + * @param {boolean} parameters.shouldBeExpanded If `true` the item will be expanded. If `false` the item will be collapsed. + */ + public applyItemExpansion = ({ + itemId, + event, + shouldBeExpanded, + }: { + itemId: TreeViewItemId; + event: React.SyntheticEvent | null; + shouldBeExpanded: boolean; + }) => { + const oldExpanded = expansionSelectors.expandedItemsRaw(this.store.state); + let newExpanded: TreeViewItemId[]; + if (shouldBeExpanded) { + newExpanded = [itemId].concat(oldExpanded); + } else { + newExpanded = oldExpanded.filter((id) => id !== itemId); + } + + this.store.parameters.onItemExpansionToggle?.(event, itemId, shouldBeExpanded); + this.setExpandedItems(event, newExpanded); + }; + + /** + * Expand all the siblings (i.e.: the items that have the same parent) of a given item. + * @param {React.SyntheticEvent} event The DOM event that triggered the change. + * @param {TreeViewItemId} itemId The id of the item whose siblings will be expanded. + */ + public expandAllSiblings = (event: React.KeyboardEvent, itemId: TreeViewItemId) => { + const itemMeta = itemsSelectors.itemMeta(this.store.state, itemId); + if (itemMeta == null) { + return; + } + + const siblings = itemsSelectors.itemOrderedChildrenIds(this.store.state, itemMeta.parentId); + + const diff = siblings.filter( + (child) => + expansionSelectors.isItemExpandable(this.store.state, child) && + !expansionSelectors.isItemExpanded(this.store.state, child), + ); + + const newExpanded = expansionSelectors.expandedItemsRaw(this.store.state).concat(diff); + + if (diff.length > 0) { + if (this.store.parameters.onItemExpansionToggle) { + diff.forEach((newlyExpandedItemId) => { + this.store.parameters.onItemExpansionToggle!(event, newlyExpandedItemId, true); + }); + } + + this.setExpandedItems(event, newExpanded); + } + }; + + /** + * Mark a list of items as expandable. + * @param {TreeViewItemId[]} items The ids of the items to mark as expandable. + */ + public addExpandableItems = (items: TreeViewItemId[]) => { + const newItemMetaLookup = { ...this.store.state.itemMetaLookup }; + for (const itemId of items) { + newItemMetaLookup[itemId] = { ...newItemMetaLookup[itemId], expandable: true }; + } + this.store.set('itemMetaLookup', newItemMetaLookup); + }; +} diff --git a/packages/x-tree-view/src/internals/plugins/expansion/index.ts b/packages/x-tree-view/src/internals/plugins/expansion/index.ts new file mode 100644 index 0000000000000..421fbfbb6a4a2 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/expansion/index.ts @@ -0,0 +1,2 @@ +export * from './TreeViewExpansionPlugin'; +export * from './selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors.ts b/packages/x-tree-view/src/internals/plugins/expansion/selectors.ts similarity index 75% rename from packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors.ts rename to packages/x-tree-view/src/internals/plugins/expansion/selectors.ts index 9da1daa605d30..204edd789bbed 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors.ts +++ b/packages/x-tree-view/src/internals/plugins/expansion/selectors.ts @@ -1,12 +1,11 @@ import { createSelector, createSelectorMemoized } from '@mui/x-internals/store'; import { TreeViewItemId } from '../../../models'; -import { TreeViewState } from '../../models'; -import { itemsSelectors } from '../useTreeViewItems/useTreeViewItems.selectors'; -import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types'; -import { TREE_VIEW_ROOT_PARENT_ID } from '../useTreeViewItems'; +import { MinimalTreeViewState } from '../../MinimalTreeViewStore'; +import { itemsSelectors } from '../items/selectors'; +import { TREE_VIEW_ROOT_PARENT_ID } from '../items'; const expandedItemMapSelector = createSelectorMemoized( - (state: TreeViewState<[UseTreeViewExpansionSignature]>) => state.expansion.expandedItems, + (state: MinimalTreeViewState) => state.expandedItems, (expandedItems) => { const expandedItemsMap = new Map(); expandedItems.forEach((id) => { @@ -21,9 +20,7 @@ export const expansionSelectors = { /** * Gets the expanded items as provided to the component. */ - expandedItemsRaw: createSelector( - (state: TreeViewState<[UseTreeViewExpansionSignature]>) => state.expansion.expandedItems, - ), + expandedItemsRaw: createSelector((state: MinimalTreeViewState) => state.expandedItems), /** * Gets the expanded items as a Map. */ @@ -55,9 +52,7 @@ export const expansionSelectors = { /** * Gets the slot that triggers the item's expansion when clicked. */ - triggerSlot: createSelector( - (state: TreeViewState<[UseTreeViewExpansionSignature]>) => state.expansion.expansionTrigger, - ), + triggerSlot: createSelector((state: MinimalTreeViewState) => state.expansionTrigger), /** * Checks whether an item is expanded. */ diff --git a/packages/x-tree-view/src/internals/plugins/expansion/utils.ts b/packages/x-tree-view/src/internals/plugins/expansion/utils.ts new file mode 100644 index 0000000000000..41f7d1f888528 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/expansion/utils.ts @@ -0,0 +1,16 @@ +import { RichTreeViewStoreParameters } from '../../RichTreeViewStore'; + +export const getExpansionTrigger = ({ + isItemEditable, + expansionTrigger, +}: Pick, 'isItemEditable' | 'expansionTrigger'>) => { + if (expansionTrigger) { + return expansionTrigger; + } + + if (isItemEditable) { + return 'iconContainer'; + } + + return 'content'; +}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.test.tsx b/packages/x-tree-view/src/internals/plugins/focus/TreeViewFocusPlugin.test.tsx similarity index 98% rename from packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.test.tsx rename to packages/x-tree-view/src/internals/plugins/focus/TreeViewFocusPlugin.test.tsx index bfbf007113c31..7546b73d674b5 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/focus/TreeViewFocusPlugin.test.tsx @@ -2,9 +2,7 @@ import * as React from 'react'; import { spy } from 'sinon'; import { act, fireEvent } from '@mui/internal-test-utils'; import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; - -// TODO #20051: Replace with imported type -type TreeViewAnyStore = { parameters: any }; +import { TreeViewAnyStore } from '../../models'; /** * All tests related to keyboard navigation (e.g.: type-ahead when using `props.disabledItemsFocusable`) diff --git a/packages/x-tree-view/src/internals/plugins/focus/TreeViewFocusPlugin.ts b/packages/x-tree-view/src/internals/plugins/focus/TreeViewFocusPlugin.ts new file mode 100644 index 0000000000000..01477c4a222b8 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/focus/TreeViewFocusPlugin.ts @@ -0,0 +1,130 @@ +import { TreeViewCancellableEvent, TreeViewItemId } from '../../../models'; +import { expansionSelectors } from '../expansion'; +import { focusSelectors } from './selectors'; +import { itemsSelectors } from '../items'; +import { MinimalTreeViewStore } from '../../MinimalTreeViewStore'; + +export class TreeViewFocusPlugin { + private store: MinimalTreeViewStore; + + // We can't type `store`, otherwise we get the following TS error: + // 'focus' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. + constructor(store: any) { + this.store = store; + + // Whenever the items change, we need to ensure the focused item is still present. + this.store.registerStoreEffect(itemsSelectors.itemMetaLookup, () => { + const focusedItemId = focusSelectors.focusedItemId(store.state); + if (focusedItemId == null) { + return; + } + + const hasItemBeenRemoved = !itemsSelectors.itemMeta(store.state, focusedItemId); + if (!hasItemBeenRemoved) { + return; + } + + const defaultFocusableItemId = focusSelectors.defaultFocusableItemId(store.state); + if (defaultFocusableItemId == null) { + this.setFocusedItemId(null); + return; + } + + this.applyItemFocus(null, defaultFocusableItemId); + }); + } + + private setFocusedItemId = (itemId: TreeViewItemId | null) => { + const focusedItemId = focusSelectors.focusedItemId(this.store.state); + if (focusedItemId === itemId) { + return; + } + + this.store.set('focusedItemId', itemId); + }; + + private applyItemFocus = (event: React.SyntheticEvent | null, itemId: TreeViewItemId) => { + this.store.items.getItemDOMElement(itemId)?.focus(); + this.setFocusedItemId(itemId); + this.store.parameters.onItemFocus?.(event, itemId); + }; + + public buildPublicAPI = () => { + return { + focusItem: this.focusItem, + }; + }; + + /** + * Focus the item with the given id. + * + * If the item is the child of a collapsed item, then this method will do nothing. + * Make sure to expand the ancestors of the item before calling this method if needed. + * @param {React.SyntheticEvent | null} event The DOM event that triggered the change. + * @param {TreeViewItemId} itemId The id of the item to focus. + */ + public focusItem = (event: React.SyntheticEvent | null, itemId: TreeViewItemId) => { + // If we receive an itemId, and it is visible, the focus will be set to it + const itemMeta = itemsSelectors.itemMeta(this.store.state, itemId); + const isItemVisible = + itemMeta && + (itemMeta.parentId == null || + expansionSelectors.isItemExpanded(this.store.state, itemMeta.parentId)); + + if (isItemVisible) { + this.applyItemFocus(event, itemId); + } + }; + + /** + * Remove the focus from the currently focused item (both from the internal state and the DOM). + */ + public removeFocusedItem = () => { + const focusedItemId = focusSelectors.focusedItemId(this.store.state); + if (focusedItemId == null) { + return; + } + + const itemMeta = itemsSelectors.itemMeta(this.store.state, focusedItemId); + if (itemMeta) { + const itemElement = this.store.items.getItemDOMElement(focusedItemId); + if (itemElement) { + itemElement.blur(); + } + } + + this.setFocusedItemId(null); + }; + + /** + * Event handler to fire when the `root` slot of the Tree View is focused. + * @param {React.MouseEvent} event The DOM event that triggered the change. + */ + public handleRootFocus = ( + event: React.FocusEvent & TreeViewCancellableEvent, + ) => { + if (event.defaultMuiPrevented) { + return; + } + + // if the event bubbled (which is React specific) we don't want to steal focus + const defaultFocusableItemId = focusSelectors.defaultFocusableItemId(this.store.state); + if (event.target === event.currentTarget && defaultFocusableItemId != null) { + this.applyItemFocus(event, defaultFocusableItemId); + } + }; + + /** + * Event handler to fire when the `root` slot of the Tree View is blurred. + * @param {React.MouseEvent} event The DOM event that triggered the change. + */ + public handleRootBlur = ( + event: React.FocusEvent & TreeViewCancellableEvent, + ) => { + if (event.defaultMuiPrevented) { + return; + } + + this.setFocusedItemId(null); + }; +} diff --git a/packages/x-tree-view/src/internals/plugins/focus/index.ts b/packages/x-tree-view/src/internals/plugins/focus/index.ts new file mode 100644 index 0000000000000..f64efa858e2f0 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/focus/index.ts @@ -0,0 +1,2 @@ +export * from './TreeViewFocusPlugin'; +export * from './selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors.ts b/packages/x-tree-view/src/internals/plugins/focus/selectors.ts similarity index 68% rename from packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors.ts rename to packages/x-tree-view/src/internals/plugins/focus/selectors.ts index 8caf02569ee32..cc495f6504bfc 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors.ts +++ b/packages/x-tree-view/src/internals/plugins/focus/selectors.ts @@ -1,20 +1,17 @@ import { createSelector, createSelectorMemoized } from '@mui/x-internals/store'; -import { UseTreeViewFocusSignature } from './useTreeViewFocus.types'; -import { selectionSelectors } from '../useTreeViewSelection/useTreeViewSelection.selectors'; -import { itemsSelectors } from '../useTreeViewItems/useTreeViewItems.selectors'; -import { isItemDisabled } from '../useTreeViewItems/useTreeViewItems.utils'; -import { expansionSelectors } from '../useTreeViewExpansion/useTreeViewExpansion.selectors'; -import { TreeViewState } from '../../models'; +import { selectionSelectors } from '../selection/selectors'; +import { itemsSelectors } from '../items/selectors'; +import { isItemDisabled } from '../items/utils'; +import { expansionSelectors } from '../expansion/selectors'; +import { MinimalTreeViewState } from '../../MinimalTreeViewStore'; import { TreeViewItemId } from '../../../models'; -import { UseTreeViewItemsSignature } from '../useTreeViewItems'; const defaultFocusableItemIdSelector = createSelectorMemoized( selectionSelectors.selectedItems, expansionSelectors.expandedItemsMap, itemsSelectors.itemMetaLookup, itemsSelectors.disabledItemFocusable, - (state: TreeViewState<[UseTreeViewItemsSignature]>) => - itemsSelectors.itemOrderedChildrenIds(state, null), + (state: MinimalTreeViewState) => itemsSelectors.itemOrderedChildrenIds(state, null), (selectedItems, expandedItemsMap, itemMetaLookup, disabledItemsFocusable, orderedRootItemIds) => { const firstSelectedItem = selectedItems.find((itemId) => { if (!disabledItemsFocusable && isItemDisabled(itemMetaLookup, itemId)) { @@ -58,14 +55,12 @@ export const focusSelectors = { /** * Gets the id of the item that is currently focused. */ - focusedItemId: createSelector( - (state: TreeViewState<[UseTreeViewFocusSignature]>) => state.focus.focusedItemId, - ), + focusedItemId: createSelector((state: MinimalTreeViewState) => state.focusedItemId), /** * Checks whether an item is focused. */ isItemFocused: createSelector( - (state: TreeViewState<[UseTreeViewFocusSignature]>, itemId: TreeViewItemId) => - state.focus.focusedItemId === itemId, + (state: MinimalTreeViewState, itemId: TreeViewItemId) => + state.focusedItemId === itemId, ), }; diff --git a/packages/x-tree-view/src/internals/plugins/id/index.ts b/packages/x-tree-view/src/internals/plugins/id/index.ts new file mode 100644 index 0000000000000..8c9698fe1e268 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/id/index.ts @@ -0,0 +1 @@ +export * from './selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/id/selectors.ts b/packages/x-tree-view/src/internals/plugins/id/selectors.ts new file mode 100644 index 0000000000000..239854f56528f --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/id/selectors.ts @@ -0,0 +1,25 @@ +import { createSelector } from '@mui/x-internals/store'; +import { MinimalTreeViewState } from '../../MinimalTreeViewStore'; +import { TreeViewItemId } from '../../../models'; + +export const idSelectors = { + /** + * Get the id attribute of the tree view. + */ + treeId: createSelector((state: MinimalTreeViewState) => state.treeId), + /** + * Generate the id attribute (i.e.: the `id` attribute passed to the DOM element) of a Tree Item. + * If the user explicitly defined an id attribute, it will be returned. + * Otherwise, the method creates a unique id for the item based on the Tree View id attribute and the item `itemId` + */ + treeItemIdAttribute: createSelector( + (state: MinimalTreeViewState) => state.treeId, + (treeId, itemId: TreeViewItemId, providedIdAttribute: string | undefined) => { + if (providedIdAttribute != null) { + return providedIdAttribute; + } + + return `${treeId ?? ''}-${itemId}`; + }, + ), +}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx b/packages/x-tree-view/src/internals/plugins/items/TreeViewItemsPlugin.test.tsx similarity index 99% rename from packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx rename to packages/x-tree-view/src/internals/plugins/items/TreeViewItemsPlugin.test.tsx index 689f23f1e89cf..6bf972092f521 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/items/TreeViewItemsPlugin.test.tsx @@ -3,9 +3,7 @@ import { act, fireEvent, reactMajor, waitFor } from '@mui/internal-test-utils'; import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; import { TreeItemLabel } from '@mui/x-tree-view/TreeItem'; import { isJSDOM } from 'test/utils/skipIf'; - -// TODO #20051: Replace with imported type -type TreeViewAnyStore = { parameters: any }; +import { TreeViewAnyStore } from '../../models'; describeTreeView( 'TreeViewItemsPlugin', diff --git a/packages/x-tree-view/src/internals/plugins/items/TreeViewItemsPlugin.ts b/packages/x-tree-view/src/internals/plugins/items/TreeViewItemsPlugin.ts new file mode 100644 index 0000000000000..ba2167b51a3f7 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/items/TreeViewItemsPlugin.ts @@ -0,0 +1,266 @@ +import { TreeViewItemId, TreeViewValidItem } from '../../../models'; +import { idSelectors } from '../id'; +import { itemsSelectors } from './selectors'; +import { buildItemsLookups, TREE_VIEW_ROOT_PARENT_ID } from './utils'; +import type { MinimalTreeViewStore } from '../../MinimalTreeViewStore/MinimalTreeViewStore'; +import { + MinimalTreeViewParameters, + MinimalTreeViewState, +} from '../../MinimalTreeViewStore/MinimalTreeViewStore.types'; + +export class TreeViewItemsPlugin> { + private store: MinimalTreeViewStore; + + // We can't type `store`, otherwise we get the following TS error: + // 'items' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. + constructor(store: any) { + this.store = store; + } + + /** + * Determines if the items state should be rebuilt based on the new and previous parameters. + */ + public static shouldRebuildItemsState = >( + newParameters: MinimalTreeViewParameters, + previousParameters: MinimalTreeViewParameters, + ): boolean => { + return ['items', 'isItemDisabled', 'getItemId', 'getItemLabel', 'getItemChildren'].some( + (key) => { + const typedKey = key as keyof MinimalTreeViewParameters; + return newParameters[typedKey] !== previousParameters[typedKey]; + }, + ); + }; + + /** + * Builds the state properties derived from the `items` prop. + */ + public static buildItemsStateIfNeeded = >( + parameters: Pick< + MinimalTreeViewParameters, + // When adding new parameters here, please also update the `shouldRebuildItemsState` method accordingly. + 'items' | 'isItemDisabled' | 'getItemId' | 'getItemLabel' | 'getItemChildren' + >, + ) => { + const itemMetaLookup: MinimalTreeViewState['itemMetaLookup'] = {}; + const itemModelLookup: MinimalTreeViewState['itemModelLookup'] = {}; + const itemOrderedChildrenIdsLookup: MinimalTreeViewState< + R2, + any + >['itemOrderedChildrenIdsLookup'] = {}; + const itemChildrenIndexesLookup: MinimalTreeViewState['itemChildrenIndexesLookup'] = + {}; + + function processSiblings(items: readonly R2[], parentId: string | null, depth: number) { + const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID; + const { metaLookup, modelLookup, orderedChildrenIds, childrenIndexes, itemsChildren } = + buildItemsLookups({ + storeParameters: parameters, + items, + parentId, + depth, + isItemExpandable: (item, children) => !!children && children.length > 0, + otherItemsMetaLookup: itemMetaLookup, + }); + + Object.assign(itemMetaLookup, metaLookup); + Object.assign(itemModelLookup, modelLookup); + itemOrderedChildrenIdsLookup[parentIdWithDefault] = orderedChildrenIds; + itemChildrenIndexesLookup[parentIdWithDefault] = childrenIndexes; + + for (const item of itemsChildren) { + processSiblings(item.children || [], item.id, depth + 1); + } + } + + processSiblings(parameters.items, null, 0); + + return { + itemMetaLookup, + itemModelLookup, + itemOrderedChildrenIdsLookup, + itemChildrenIndexesLookup, + }; + }; + + /** + * Get the item with the given id. + * When used in the Simple Tree View, it returns an object with the `id` and `label` properties. + * @param {TreeViewItemId} itemId The id of the item to retrieve. + * @returns {R} The item with the given id. + */ + private getItem = (itemId: TreeViewItemId): R => + itemsSelectors.itemModel(this.store.state, itemId); + + /** + * Get all the items in the same format as provided by `props.items`. + * @returns {R[]} The items in the tree. + */ + private getItemTree = (): R[] => { + const getItemFromItemId = (itemId: TreeViewItemId): R => { + const item = itemsSelectors.itemModel(this.store.state, itemId); + const itemToMutate = { ...item }; + const newChildren = itemsSelectors.itemOrderedChildrenIds(this.store.state, itemId); + if (newChildren.length > 0) { + itemToMutate.children = newChildren.map(getItemFromItemId); + } else { + delete itemToMutate.children; + } + + return itemToMutate; + }; + + return itemsSelectors.itemOrderedChildrenIds(this.store.state, null).map(getItemFromItemId); + }; + + /** + * Get the ids of a given item's children. + * Those ids are returned in the order they should be rendered. + * To get the root items, pass `null` as the `itemId`. + * @param {TreeViewItemId | null} itemId The id of the item to get the children of. + * @returns {TreeViewItemId[]} The ids of the item's children. + */ + private getItemOrderedChildrenIds = (itemId: TreeViewItemId | null): TreeViewItemId[] => + itemsSelectors.itemOrderedChildrenIds(this.store.state, itemId); + + /** * Get the id of the parent item. + * @param {TreeViewItemId} itemId The id of the item to whose parentId we want to retrieve. + * @returns {TreeViewItemId | null} The id of the parent item. + */ + private getParentId = (itemId: TreeViewItemId): TreeViewItemId | null => { + const itemMeta = itemsSelectors.itemMeta(this.store.state, itemId); + return itemMeta?.parentId || null; + }; + + /** + * Toggle the disabled state of the item with the given id. + * @param {object} parameters The params of the method. + * @param {TreeViewItemId } parameters.itemId The id of the item to get the children of. + * @param {boolean } parameters.shouldBeDisabled true if the item should be disabled. + */ + private setIsItemDisabled = ({ + itemId, + shouldBeDisabled, + }: { + itemId: TreeViewItemId; + shouldBeDisabled?: boolean; + }) => { + if (!this.store.state.itemMetaLookup[itemId]) { + return; + } + + const itemMetaLookup = { ...this.store.state.itemMetaLookup }; + itemMetaLookup[itemId] = { + ...itemMetaLookup[itemId], + disabled: shouldBeDisabled ?? !itemMetaLookup[itemId].disabled, + }; + + this.store.set('itemMetaLookup', itemMetaLookup); + }; + + public buildPublicAPI = () => { + return { + getItem: this.getItem, + getItemDOMElement: this.getItemDOMElement, + getItemOrderedChildrenIds: this.getItemOrderedChildrenIds, + getItemTree: this.getItemTree, + getParentId: this.getParentId, + setIsItemDisabled: this.setIsItemDisabled, + }; + }; + + /** + * Get the DOM element of the item with the given id. + * @param {TreeViewItemId} itemId The id of the item to get the DOM element of. + * @returns {HTMLElement | null} The DOM element of the item with the given id. + */ + public getItemDOMElement = (itemId: TreeViewItemId): HTMLElement | null => { + const itemMeta = itemsSelectors.itemMeta(this.store.state, itemId); + if (itemMeta == null) { + return null; + } + + const idAttribute = idSelectors.treeItemIdAttribute( + this.store.state, + itemId, + itemMeta.idAttribute, + ); + return document.getElementById(idAttribute); + }; + + /** + * Add an array of items to the tree. + * @param {SetItemChildrenParameters} args The items to add to the tree and information about their ancestors. + */ + public setItemChildren = ({ + items, + parentId, + getChildrenCount, + }: { + items: readonly R[]; + parentId: TreeViewItemId | null; + getChildrenCount: (item: R) => number; + }) => { + const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID; + const parentDepth = + parentId == null ? -1 : itemsSelectors.itemDepth(this.store.state, parentId); + + const { metaLookup, modelLookup, orderedChildrenIds, childrenIndexes } = buildItemsLookups({ + storeParameters: this.store.parameters, + items, + parentId, + depth: parentDepth + 1, + isItemExpandable: getChildrenCount ? (item) => getChildrenCount(item) > 0 : () => false, + otherItemsMetaLookup: itemsSelectors.itemMetaLookup(this.store.state), + }); + + this.store.update({ + itemModelLookup: { ...this.store.state.itemModelLookup, ...modelLookup }, + itemMetaLookup: { ...this.store.state.itemMetaLookup, ...metaLookup }, + itemOrderedChildrenIdsLookup: { + ...this.store.state.itemOrderedChildrenIdsLookup, + [parentIdWithDefault]: orderedChildrenIds, + }, + itemChildrenIndexesLookup: { + ...this.store.state.itemChildrenIndexesLookup, + [parentIdWithDefault]: childrenIndexes, + }, + }); + }; + + /** + * Remove the children of an item. + * @param {TreeViewItemId | null} parentId The id of the item to remove the children of. + */ + public removeChildren = (parentId: TreeViewItemId | null) => { + const itemMetaLookup = this.store.state.itemMetaLookup; + const newMetaMap = Object.keys(itemMetaLookup).reduce((acc, key) => { + const item = itemMetaLookup[key]; + if (item.parentId === parentId) { + return acc; + } + return { ...acc, [item.id]: item }; + }, {}); + + const newItemOrderedChildrenIdsLookup = { ...this.store.state.itemOrderedChildrenIdsLookup }; + const newItemChildrenIndexesLookup = { ...this.store.state.itemChildrenIndexesLookup }; + const cleanId = parentId ?? TREE_VIEW_ROOT_PARENT_ID; + delete newItemChildrenIndexesLookup[cleanId]; + delete newItemOrderedChildrenIdsLookup[cleanId]; + + this.store.update({ + itemMetaLookup: newMetaMap, + itemOrderedChildrenIdsLookup: newItemOrderedChildrenIdsLookup, + itemChildrenIndexesLookup: newItemChildrenIndexesLookup, + }); + }; + + /** + * Callback fired when the `content` slot of a given Tree Item is clicked. + * @param {React.MouseEvent} event The DOM event that triggered the change. + * @param {TreeViewItemId} itemId The id of the item being clicked. + */ + public handleItemClick = (event: React.MouseEvent, itemId: TreeViewItemId) => { + this.store.parameters.onItemClick?.(event, itemId); + }; +} diff --git a/packages/x-tree-view/src/internals/plugins/items/index.ts b/packages/x-tree-view/src/internals/plugins/items/index.ts new file mode 100644 index 0000000000000..a224f6c005c5c --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/items/index.ts @@ -0,0 +1,3 @@ +export * from './TreeViewItemsPlugin'; +export * from './selectors'; +export { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from './utils'; diff --git a/packages/x-tree-view/src/internals/plugins/items/selectors.ts b/packages/x-tree-view/src/internals/plugins/items/selectors.ts new file mode 100644 index 0000000000000..0bba82f7ad154 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/items/selectors.ts @@ -0,0 +1,97 @@ +import { createSelector } from '@mui/x-internals/store'; +import { TreeViewItemId } from '../../../models'; +import { TreeViewItemMeta } from '../../models'; +import { isItemDisabled, TREE_VIEW_ROOT_PARENT_ID } from './utils'; +import { MinimalTreeViewState } from '../../MinimalTreeViewStore'; + +const EMPTY_CHILDREN: TreeViewItemId[] = []; + +export const itemsSelectors = { + /** + * Gets the DOM structure of the Tree View. + */ + domStructure: createSelector((state: MinimalTreeViewState) => state.domStructure), + /** + * Checks whether the disabled items are focusable. + */ + disabledItemFocusable: createSelector( + (state: MinimalTreeViewState) => state.disabledItemsFocusable, + ), + /** + * Gets the meta-information of all items. + */ + itemMetaLookup: createSelector((state: MinimalTreeViewState) => state.itemMetaLookup), + /** + * Gets the ordered children ids of all items. + */ + itemOrderedChildrenIdsLookup: createSelector( + (state: MinimalTreeViewState) => state.itemOrderedChildrenIdsLookup, + ), + /** + * Gets the meta-information of an item. + */ + itemMeta: createSelector( + (state: MinimalTreeViewState, itemId: TreeViewItemId | null) => + (state.itemMetaLookup[itemId ?? TREE_VIEW_ROOT_PARENT_ID] ?? null) as TreeViewItemMeta | null, + ), + /** + * Gets the ordered children ids of an item. + */ + itemOrderedChildrenIds: createSelector( + (state: MinimalTreeViewState, itemId: TreeViewItemId | null) => + state.itemOrderedChildrenIdsLookup[itemId ?? TREE_VIEW_ROOT_PARENT_ID] ?? EMPTY_CHILDREN, + ), + /** + * Gets the model of an item. + */ + itemModel: createSelector( + (state: MinimalTreeViewState, itemId: TreeViewItemId) => + state.itemModelLookup[itemId], + ), + /** + * Checks whether an item is disabled. + */ + isItemDisabled: createSelector((state: MinimalTreeViewState, itemId: TreeViewItemId) => + isItemDisabled(state.itemMetaLookup, itemId), + ), + /** + * Gets the index of an item in its parent's children. + */ + itemIndex: createSelector((state: MinimalTreeViewState, itemId: TreeViewItemId) => { + const itemMeta = state.itemMetaLookup[itemId]; + if (itemMeta == null) { + return -1; + } + + const parentIndexes = + state.itemChildrenIndexesLookup[itemMeta.parentId ?? TREE_VIEW_ROOT_PARENT_ID]; + return parentIndexes[itemMeta.id]; + }), + /** + * Gets the id of an item's parent. + */ + itemParentId: createSelector( + (state: MinimalTreeViewState, itemId: TreeViewItemId) => + state.itemMetaLookup[itemId]?.parentId ?? null, + ), + /** + * Gets the depth of an item (items at the root level have a depth of 0). + */ + itemDepth: createSelector( + (state: MinimalTreeViewState, itemId: TreeViewItemId) => + state.itemMetaLookup[itemId]?.depth ?? 0, + ), + /** + * Checks whether an item can be focused. + */ + canItemBeFocused: createSelector( + (state: MinimalTreeViewState, itemId: TreeViewItemId) => + state.disabledItemsFocusable || !isItemDisabled(state.itemMetaLookup, itemId), + ), + /** + * Gets the identation between an item and its children. + */ + itemChildrenIndentation: createSelector( + (state: MinimalTreeViewState) => state.itemChildrenIndentation, + ), +}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.utils.ts b/packages/x-tree-view/src/internals/plugins/items/utils.ts similarity index 52% rename from packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.utils.ts rename to packages/x-tree-view/src/internals/plugins/items/utils.ts index ee72bf3b81b3b..7df762cf23bdb 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.utils.ts +++ b/packages/x-tree-view/src/internals/plugins/items/utils.ts @@ -1,9 +1,6 @@ -import { TreeViewBaseItem, TreeViewItemId } from '../../../models'; +import { TreeViewValidItem, TreeViewItemId } from '../../../models'; import { TreeViewItemMeta } from '../../models'; -import { - UseTreeViewItemsParametersWithDefaults, - UseTreeViewItemsState, -} from './useTreeViewItems.types'; +import type { MinimalTreeViewParameters, MinimalTreeViewState } from '../../MinimalTreeViewStore'; export const TREE_VIEW_ROOT_PARENT_ID = '__TREE_VIEW_ROOT_PARENT_ID__'; @@ -55,66 +52,20 @@ export const isItemDisabled = ( return false; }; -type State = UseTreeViewItemsState['items']; -export function buildItemsState(parameters: BuildItemsStateParameters): State { - const { config, items: itemsParam, disabledItemsFocusable } = parameters; - - const itemMetaLookup: State['itemMetaLookup'] = {}; - const itemModelLookup: State['itemModelLookup'] = {}; - const itemOrderedChildrenIdsLookup: State['itemOrderedChildrenIdsLookup'] = {}; - const itemChildrenIndexesLookup: State['itemChildrenIndexesLookup'] = {}; - - function processSiblings( - items: readonly TreeViewBaseItem[], - parentId: string | null, - depth: number, - ) { - const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID; - const { metaLookup, modelLookup, orderedChildrenIds, childrenIndexes, itemsChildren } = - buildItemsLookups({ - config, - items, - parentId, - depth, - isItemExpandable: (item, children) => !!children && children.length > 0, - otherItemsMetaLookup: itemMetaLookup, - }); - - Object.assign(itemMetaLookup, metaLookup); - Object.assign(itemModelLookup, modelLookup); - itemOrderedChildrenIdsLookup[parentIdWithDefault] = orderedChildrenIds; - itemChildrenIndexesLookup[parentIdWithDefault] = childrenIndexes; - - for (const item of itemsChildren) { - processSiblings(item.children || [], item.id, depth + 1); - } - } - - processSiblings(itemsParam, null, 0); - - return { - disabledItemsFocusable, - itemMetaLookup, - itemModelLookup, - itemOrderedChildrenIdsLookup, - itemChildrenIndexesLookup, - domStructure: 'nested', - }; -} - -interface BuildItemsStateParameters extends Pick { - disabledItemsFocusable: boolean; -} - -export function buildItemsLookups(parameters: BuildItemsLookupsParameters) { - const { config, items, parentId, depth, isItemExpandable, otherItemsMetaLookup } = parameters; - const metaLookup: State['itemMetaLookup'] = {}; - const modelLookup: State['itemModelLookup'] = {}; +export function buildItemsLookups>( + parameters: BuildItemsLookupsParameters, +) { + const { storeParameters, items, parentId, depth, isItemExpandable, otherItemsMetaLookup } = + parameters; + const metaLookup: MinimalTreeViewState['itemMetaLookup'] = {}; + const modelLookup: MinimalTreeViewState['itemModelLookup'] = {}; const orderedChildrenIds: string[] = []; - const itemsChildren: { id: string | null; children: TreeViewBaseItem[] }[] = []; + const itemsChildren: { id: string | null; children: R[] }[] = []; - const processItem = (item: TreeViewBaseItem) => { - const id: string = config.getItemId ? config.getItemId(item) : (item as any).id; + const processItem = (item: R) => { + const id: string = storeParameters.getItemId + ? storeParameters.getItemId(item) + : (item as any).id; checkId({ id, parentId, @@ -122,9 +73,9 @@ export function buildItemsLookups(parameters: BuildItemsLookupsParameters) { itemMetaLookup: otherItemsMetaLookup, siblingsMetaLookup: metaLookup, }); - const label = config.getItemLabel - ? config.getItemLabel(item) - : (item as { label: string }).label; + const label = storeParameters.getItemLabel + ? storeParameters.getItemLabel(item) + : (item as any).label; if (label == null) { throw new Error( [ @@ -137,9 +88,9 @@ export function buildItemsLookups(parameters: BuildItemsLookupsParameters) { } const children = - (config.getItemChildren - ? config.getItemChildren(item) - : (item as { children?: TreeViewBaseItem[] }).children) || []; + (storeParameters.getItemChildren + ? storeParameters.getItemChildren(item) + : (item as { children?: R[] }).children) || []; itemsChildren.push({ id, children }); @@ -151,7 +102,7 @@ export function buildItemsLookups(parameters: BuildItemsLookupsParameters) { parentId, idAttribute: undefined, expandable: isItemExpandable(item, children), - disabled: config.isItemDisabled ? config.isItemDisabled(item) : false, + disabled: storeParameters.isItemDisabled ? storeParameters.isItemDisabled(item) : false, depth, }; @@ -171,16 +122,19 @@ export function buildItemsLookups(parameters: BuildItemsLookupsParameters) { }; } -interface BuildItemsLookupsParameters { - items: readonly TreeViewBaseItem[]; - config: BuildItemsLookupConfig; +interface BuildItemsLookupsParameters> { + items: readonly R[]; + storeParameters: Pick< + MinimalTreeViewParameters, + 'getItemId' | 'getItemLabel' | 'getItemChildren' | 'isItemDisabled' + >; parentId: string | null; depth: number; - isItemExpandable: (item: TreeViewBaseItem, children: TreeViewBaseItem[] | undefined) => boolean; + isItemExpandable: (item: R, children: R[] | undefined) => boolean; otherItemsMetaLookup: { [itemId: string]: TreeViewItemMeta }; } -function checkId({ +function checkId>({ id, parentId, item, @@ -189,7 +143,7 @@ function checkId({ }: { id: TreeViewItemId | null; parentId: TreeViewItemId | null; - item: TreeViewBaseItem; + item: R; itemMetaLookup: { [itemId: string]: TreeViewItemMeta }; siblingsMetaLookup: { [itemId: string]: TreeViewItemMeta }; }) { @@ -218,9 +172,3 @@ function checkId({ ); } } - -export interface BuildItemsLookupConfig - extends Pick< - UseTreeViewItemsParametersWithDefaults, - 'isItemDisabled' | 'getItemLabel' | 'getItemChildren' | 'getItemId' - > {} diff --git a/packages/x-tree-view/src/internals/plugins/jsxItems/TreeViewJSXItemsPlugin.ts b/packages/x-tree-view/src/internals/plugins/jsxItems/TreeViewJSXItemsPlugin.ts new file mode 100644 index 0000000000000..17ff9c660d18e --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/jsxItems/TreeViewJSXItemsPlugin.ts @@ -0,0 +1,98 @@ +import { TreeViewItemId } from '../../../models'; +import { TreeViewItemMeta } from '../../models'; +import type { SimpleTreeViewStore } from '../../SimpleTreeViewStore'; +import { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from '../items'; +import { jsxItemsitemWrapper, useJSXItemsItemPlugin } from './itemPlugin'; + +export class TreeViewJSXItemsPlugin { + private store: SimpleTreeViewStore; + + public constructor(store: SimpleTreeViewStore) { + this.store = store; + store.itemPluginManager.register(useJSXItemsItemPlugin, jsxItemsitemWrapper); + } + + /** + * Insert a new item in the state from a Tree Item component. + * @param {TreeViewItemMeta} item The meta-information of the item to insert. + * @returns {() => void} A function to remove the item from the state. + */ + public insertJSXItem = (item: TreeViewItemMeta) => { + if (this.store.state.itemMetaLookup[item.id] != null) { + throw new Error( + [ + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'Alternatively, you can use the `getItemId` prop to specify a custom id for each item.', + `Two items were provided with the same id in the \`items\` prop: "${item.id}"`, + ].join('\n'), + ); + } + + this.store.update({ + itemMetaLookup: { ...this.store.state.itemMetaLookup, [item.id]: item }, + // For Simple Tree View, we don't have a proper `item` object, so we create a very basic one. + itemModelLookup: { + ...this.store.state.itemModelLookup, + [item.id]: { id: item.id, label: item.label ?? '' }, + }, + }); + + return () => { + const newItemMetaLookup = { ...this.store.state.itemMetaLookup }; + const newItemModelLookup = { ...this.store.state.itemModelLookup }; + delete newItemMetaLookup[item.id]; + delete newItemModelLookup[item.id]; + + this.store.update({ + itemMetaLookup: newItemMetaLookup, + itemModelLookup: newItemModelLookup, + }); + }; + }; + + /** + * Updates the `labelMap` to register the first character of the given item's label. + * This map is used to navigate the tree using type-ahead search. + * @param {TreeViewItemId} itemId The id of the item to map the label of. + * @param {string} label The item's label. + * @returns {() => void} A function to remove the item from the `labelMap`. + */ + public mapLabelFromJSX = (itemId: TreeViewItemId, label: string) => { + this.store.keyboardNavigation.updateLabelMap((labelMap) => { + labelMap[itemId] = label; + return labelMap; + }); + + return () => { + this.store.keyboardNavigation.updateLabelMap((labelMap) => { + const newMap = { ...labelMap }; + delete newMap[itemId]; + return newMap; + }); + }; + }; + + /** + * Store the ids of a given item's children in the state. + * Those ids must be passed in the order they should be rendered. + * @param {TreeViewItemId | null} parentId The id of the item to store the children of. + * @param {TreeViewItemId[]} orderedChildrenIds The ids of the item's children. + */ + public setJSXItemsOrderedChildrenIds = ( + parentId: TreeViewItemId | null, + orderedChildrenIds: TreeViewItemId[], + ) => { + const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID; + + this.store.update({ + itemOrderedChildrenIdsLookup: { + ...this.store.state.itemOrderedChildrenIdsLookup, + [parentIdWithDefault]: orderedChildrenIds, + }, + itemChildrenIndexesLookup: { + ...this.store.state.itemChildrenIndexesLookup, + [parentIdWithDefault]: buildSiblingIndexes(orderedChildrenIds), + }, + }); + }; +} diff --git a/packages/x-tree-view/src/internals/plugins/jsxItems/index.ts b/packages/x-tree-view/src/internals/plugins/jsxItems/index.ts new file mode 100644 index 0000000000000..b65c377116066 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/jsxItems/index.ts @@ -0,0 +1 @@ +export * from './TreeViewJSXItemsPlugin'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/itemPlugin.ts b/packages/x-tree-view/src/internals/plugins/jsxItems/itemPlugin.tsx similarity index 55% rename from packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/itemPlugin.ts rename to packages/x-tree-view/src/internals/plugins/jsxItems/itemPlugin.tsx index 21de4cd117e74..633b91465161b 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/itemPlugin.ts +++ b/packages/x-tree-view/src/internals/plugins/jsxItems/itemPlugin.tsx @@ -3,20 +3,19 @@ import * as React from 'react'; import { useStore } from '@mui/x-internals/store'; import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs'; import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; -import { TreeViewItemPlugin } from '../../models'; +import { TreeItemWrapper, TreeViewItemPlugin } from '../../models'; import { useTreeViewContext } from '../../TreeViewProvider'; -import { TreeViewChildrenItemContext } from '../../TreeViewProvider/TreeViewChildrenItemProvider'; +import { + TreeViewChildrenItemContext, + TreeViewChildrenItemProvider, +} from '../../TreeViewProvider/TreeViewChildrenItemProvider'; +import { TreeViewItemDepthContext } from '../../TreeViewItemDepthContext'; import { itemHasChildren } from '../../../hooks/useTreeItemUtils/useTreeItemUtils'; -import { idSelectors } from '../../corePlugins/useTreeViewId'; -import { UseTreeViewJSXItemsSignature } from './useTreeViewJSXItems.types'; -import { generateTreeItemIdAttribute } from '../../corePlugins/useTreeViewId/useTreeViewId.utils'; +import { idSelectors } from '../id'; +import { SimpleTreeViewStore } from '../../SimpleTreeViewStore'; -export const useTreeViewJSXItemsItemPlugin: TreeViewItemPlugin = ({ - props, - rootRef, - contentRef, -}) => { - const { instance, store } = useTreeViewContext<[UseTreeViewJSXItemsSignature]>(); +export const useJSXItemsItemPlugin: TreeViewItemPlugin = ({ props, rootRef, contentRef }) => { + const { store } = useTreeViewContext>(); const { children, disabled = false, label, itemId, id } = props; const parentContext = React.useContext(TreeViewChildrenItemContext); @@ -34,41 +33,57 @@ export const useTreeViewJSXItemsItemPlugin: TreeViewItemPlugin = ({ const expandable = itemHasChildren(children); const pluginContentRef = React.useRef(null); const handleContentRef = useMergedRefs(pluginContentRef, contentRef); - const treeId = useStore(store, idSelectors.treeId); + const idAttribute = useStore(store, idSelectors.treeItemIdAttribute, itemId, id); // Prevent any flashing useIsoLayoutEffect(() => { - const idAttribute = generateTreeItemIdAttribute({ itemId, treeId, id }); registerChild(idAttribute, itemId); return () => { unregisterChild(idAttribute); unregisterChild(idAttribute); }; - }, [store, instance, registerChild, unregisterChild, itemId, id, treeId]); + }, [store, registerChild, unregisterChild, idAttribute, itemId]); useIsoLayoutEffect(() => { - return instance.insertJSXItem({ + return store.jsxItems.insertJSXItem({ id: itemId, idAttribute: id, parentId, expandable, disabled, }); - }, [instance, parentId, itemId, expandable, disabled, id]); + }, [store, parentId, itemId, expandable, disabled, id]); React.useEffect(() => { if (label) { - return instance.mapLabelFromJSX( + return store.jsxItems.mapLabelFromJSX( itemId, (pluginContentRef.current?.textContent ?? '').toLowerCase(), ); } return undefined; - }, [instance, itemId, label]); + }, [store, itemId, label]); return { contentRef: handleContentRef, rootRef, }; }; + +export const jsxItemsitemWrapper: TreeItemWrapper> = ({ + children, + itemId, + idAttribute, +}) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const depthContext = React.useContext(TreeViewItemDepthContext); + + return ( + + + {children} + + + ); +}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.test.tsx b/packages/x-tree-view/src/internals/plugins/keyboardNavigation/TreeViewKeyboardNavigationPlugin.test.tsx similarity index 99% rename from packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.test.tsx rename to packages/x-tree-view/src/internals/plugins/keyboardNavigation/TreeViewKeyboardNavigationPlugin.test.tsx index b1ce039ec6312..6d38b0ba26cd5 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/keyboardNavigation/TreeViewKeyboardNavigationPlugin.test.tsx @@ -1,9 +1,7 @@ import { spy } from 'sinon'; import { act, fireEvent } from '@mui/internal-test-utils'; import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; - -// TODO #20051: Replace with imported type -type TreeViewAnyStore = { parameters: any }; +import { TreeViewAnyStore } from '../../models'; describeTreeView( 'TreeViewKeyboardNavigationPlugin', diff --git a/packages/x-tree-view/src/internals/plugins/keyboardNavigation/TreeViewKeyboardNavigationPlugin.ts b/packages/x-tree-view/src/internals/plugins/keyboardNavigation/TreeViewKeyboardNavigationPlugin.ts new file mode 100644 index 0000000000000..9d3f1b509c092 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/keyboardNavigation/TreeViewKeyboardNavigationPlugin.ts @@ -0,0 +1,366 @@ +import { TreeViewCancellableEvent, TreeViewItemId } from '../../../models'; +import { TreeViewAnyStore, TreeViewItemMeta } from '../../models'; +import { expansionSelectors } from '../expansion'; +import { itemsSelectors } from '../items'; +import { labelSelectors, TreeViewLabelEditingPlugin } from '../labelEditing'; +import { selectionSelectors } from '../selection/selectors'; +import { + getFirstNavigableItem, + getLastNavigableItem, + getNextNavigableItem, + getPreviousNavigableItem, + isTargetInDescendants, +} from '../../utils/tree'; + +const TYPEAHEAD_TIMEOUT = 500; + +type TreeViewStoreWithLabelEditing = TreeViewAnyStore & { + labelEditing?: TreeViewLabelEditingPlugin; +}; + +type TreeViewLabelMap = { [itemId: string]: string }; + +export class TreeViewKeyboardNavigationPlugin { + private store: TreeViewStoreWithLabelEditing; + + private labelMap: TreeViewLabelMap; + + private typeaheadQuery = ''; + + // We can't type `store`, otherwise we get the following TS error: + // 'keyboardNavigation' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. + constructor(store: any) { + this.store = store; + + this.labelMap = createLabelMapFromItemMetaLookup( + itemsSelectors.itemMetaLookup(this.store.state), + ); + + // Whenever the itemMetaLookup changes, we need to regen the label map. + this.store.registerStoreEffect(itemsSelectors.itemMetaLookup, (_, itemMetaLookup) => { + if (this.store.shouldIgnoreItemsStateUpdate()) { + return; + } + + this.labelMap = createLabelMapFromItemMetaLookup(itemMetaLookup); + }); + } + + private canToggleItemSelection = (itemId: TreeViewItemId) => + selectionSelectors.enabled(this.store.state) && + !itemsSelectors.isItemDisabled(this.store.state, itemId); + + private canToggleItemExpansion = (itemId: TreeViewItemId) => { + return ( + !itemsSelectors.isItemDisabled(this.store.state, itemId) && + expansionSelectors.isItemExpandable(this.store.state, itemId) + ); + }; + + private getFirstItemMatchingTypeaheadQuery = ( + itemId: TreeViewItemId, + newKey: string, + ): TreeViewItemId | null => { + const getNextItem = (itemIdToCheck: TreeViewItemId) => { + const nextItemId = getNextNavigableItem(this.store.state, itemIdToCheck); + // We reached the end of the tree, check from the beginning + if (nextItemId === null) { + return getFirstNavigableItem(this.store.state); + } + + return nextItemId; + }; + + const getNextMatchingItemId = (query: string): TreeViewItemId | null => { + let matchingItemId: TreeViewItemId | null = null; + const checkedItems: Record = {}; + // If query length > 1, first check if current item matches + let currentItemId: TreeViewItemId = query.length > 1 ? itemId : getNextItem(itemId); + // The "!checkedItems[currentItemId]" condition avoids an infinite loop when there is no matching item. + while (matchingItemId == null && !checkedItems[currentItemId]) { + const itemLabel = this.labelMap[currentItemId]; + + if (itemLabel?.startsWith(query)) { + matchingItemId = currentItemId; + } else { + checkedItems[currentItemId] = true; + currentItemId = getNextItem(currentItemId); + } + } + return matchingItemId; + }; + + const cleanNewKey = newKey.toLowerCase(); + + // Try matching with accumulated query + new key + const concatenatedQuery = `${this.typeaheadQuery}${cleanNewKey}`; + + // check if the entire typed query matches an item + const concatenatedQueryMatchingItemId = getNextMatchingItemId(concatenatedQuery); + if (concatenatedQueryMatchingItemId != null) { + this.typeaheadQuery = concatenatedQuery; + return concatenatedQueryMatchingItemId; + } + + const newKeyMatchingItemId = getNextMatchingItemId(cleanNewKey); + if (newKeyMatchingItemId != null) { + this.typeaheadQuery = cleanNewKey; + return newKeyMatchingItemId; + } + + this.typeaheadQuery = ''; + return null; + }; + + /** + * Updates the `labelMap` to add/remove the first character of some item's labels. + * This map is used to navigate the tree using type-ahead search. + * This method is only used by the `useTreeViewJSXItems` plugin, otherwise the updates are handled internally. + * @param {(map: TreeViewLabelMap) => TreeViewLabelMap} updater The function to update the map. + */ + public updateLabelMap = (callback: (labelMap: TreeViewLabelMap) => TreeViewLabelMap) => { + this.labelMap = callback(this.labelMap); + }; + + // ARIA specification: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/#keyboardinteraction + /** + * Callback fired when a key is pressed on an item. + * Handles all the keyboard navigation logic. + * @param {React.KeyboardEvent & TreeViewCancellableEvent} event The keyboard event that triggered the callback. + * @param {TreeViewItemId} itemId The id of the item that the event was triggered on. + */ + public handleItemKeyDown = async ( + event: React.KeyboardEvent & TreeViewCancellableEvent, + itemId: TreeViewItemId, + ) => { + if (event.defaultMuiPrevented) { + return; + } + + if ( + event.altKey || + isTargetInDescendants(event.target as HTMLElement, event.currentTarget as HTMLElement) + ) { + return; + } + + const ctrlPressed = event.ctrlKey || event.metaKey; + const key = event.key; + const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(this.store.state); + + // eslint-disable-next-line default-case + switch (true) { + // Select the item when pressing "Space" + case key === ' ' && this.canToggleItemSelection(itemId): { + event.preventDefault(); + if (isMultiSelectEnabled && event.shiftKey) { + this.store.selection.expandSelectionRange(event, itemId); + } else { + this.store.selection.setItemSelection({ + event, + itemId, + keepExistingSelection: isMultiSelectEnabled, + shouldBeSelected: undefined, + }); + } + break; + } + + // If the focused item has children, we expand it. + // If the focused item has no children, we select it. + case key === 'Enter': { + if ( + this.store.labelEditing?.setEditedItem && + labelSelectors.isItemEditable(this.store.state, itemId) && + !labelSelectors.isItemBeingEdited(this.store.state, itemId) + ) { + this.store.labelEditing.setEditedItem(itemId); + } else if (this.canToggleItemExpansion(itemId)) { + this.store.expansion.setItemExpansion({ event, itemId }); + event.preventDefault(); + } else if (this.canToggleItemSelection(itemId)) { + if (isMultiSelectEnabled) { + event.preventDefault(); + this.store.selection.setItemSelection({ event, itemId, keepExistingSelection: true }); + } else if (!selectionSelectors.isItemSelected(this.store.state, itemId)) { + this.store.selection.setItemSelection({ event, itemId }); + event.preventDefault(); + } + } + + break; + } + + // Focus the next focusable item + case key === 'ArrowDown': { + const nextItem = getNextNavigableItem(this.store.state, itemId); + if (nextItem) { + event.preventDefault(); + this.store.focus.focusItem(event, nextItem); + + // Multi select behavior when pressing Shift + ArrowDown + // Toggles the selection state of the next item + if (isMultiSelectEnabled && event.shiftKey && this.canToggleItemSelection(nextItem)) { + this.store.selection.selectItemFromArrowNavigation(event, itemId, nextItem); + } + } + + break; + } + + // Focuses the previous focusable item + case key === 'ArrowUp': { + const previousItem = getPreviousNavigableItem(this.store.state, itemId); + if (previousItem) { + event.preventDefault(); + this.store.focus.focusItem(event, previousItem); + + // Multi select behavior when pressing Shift + ArrowUp + // Toggles the selection state of the previous item + if (isMultiSelectEnabled && event.shiftKey && this.canToggleItemSelection(previousItem)) { + this.store.selection.selectItemFromArrowNavigation(event, itemId, previousItem); + } + } + + break; + } + + // If the focused item is expanded, we move the focus to its first child + // If the focused item is collapsed and has children, we expand it + case (key === 'ArrowRight' && !this.store.parameters.isRtl) || + (key === 'ArrowLeft' && this.store.parameters.isRtl): { + if (ctrlPressed) { + return; + } + if (expansionSelectors.isItemExpanded(this.store.state, itemId)) { + const nextItemId = getNextNavigableItem(this.store.state, itemId); + if (nextItemId) { + this.store.focus.focusItem(event, nextItemId); + event.preventDefault(); + } + } else if (this.canToggleItemExpansion(itemId)) { + this.store.expansion.setItemExpansion({ event, itemId }); + event.preventDefault(); + } + + break; + } + + // If the focused item is expanded, we collapse it + // If the focused item is collapsed and has a parent, we move the focus to this parent + case (key === 'ArrowLeft' && !this.store.parameters.isRtl) || + (key === 'ArrowRight' && this.store.parameters.isRtl): { + if (ctrlPressed) { + return; + } + if ( + this.canToggleItemExpansion(itemId) && + expansionSelectors.isItemExpanded(this.store.state, itemId) + ) { + this.store.expansion.setItemExpansion({ event, itemId }); + event.preventDefault(); + } else { + const parent = itemsSelectors.itemParentId(this.store.state, itemId); + if (parent) { + this.store.focus.focusItem(event, parent); + event.preventDefault(); + } + } + + break; + } + + // Focuses the first item in the tree + case key === 'Home': { + // Multi select behavior when pressing Ctrl + Shift + Home + // Selects the focused item and all items up to the first item. + if ( + this.canToggleItemSelection(itemId) && + isMultiSelectEnabled && + ctrlPressed && + event.shiftKey + ) { + this.store.selection.selectRangeFromStartToItem(event, itemId); + } else { + this.store.focus.focusItem(event, getFirstNavigableItem(this.store.state)); + } + + event.preventDefault(); + break; + } + + // Focuses the last item in the tree + case key === 'End': { + // Multi select behavior when pressing Ctrl + Shirt + End + // Selects the focused item and all the items down to the last item. + if ( + this.canToggleItemSelection(itemId) && + isMultiSelectEnabled && + ctrlPressed && + event.shiftKey + ) { + this.store.selection.selectRangeFromItemToEnd(event, itemId); + } else { + this.store.focus.focusItem(event, getLastNavigableItem(this.store.state)); + } + + event.preventDefault(); + break; + } + + // Expand all siblings that are at the same level as the focused item + case key === '*': { + this.store.expansion.expandAllSiblings(event, itemId); + event.preventDefault(); + break; + } + + // Multi select behavior when pressing Ctrl + a + // Selects all the items + case String.fromCharCode(event.keyCode) === 'A' && + ctrlPressed && + isMultiSelectEnabled && + selectionSelectors.enabled(this.store.state): { + this.store.selection.selectAllNavigableItems(event); + event.preventDefault(); + break; + } + + // Type-ahead + case !ctrlPressed && !event.shiftKey && isPrintableKey(key): { + this.store.timeoutManager.clearTimeout('typeahead'); + + const matchingItem = this.getFirstItemMatchingTypeaheadQuery(itemId, key); + if (matchingItem != null) { + this.store.focus.focusItem(event, matchingItem); + event.preventDefault(); + } else { + this.typeaheadQuery = ''; + } + + this.store.timeoutManager.startTimeout('typeahead', TYPEAHEAD_TIMEOUT, () => { + this.typeaheadQuery = ''; + }); + break; + } + } + }; +} + +function isPrintableKey(string: string) { + return !!string && string.length === 1 && !!string.match(/\S/); +} + +function createLabelMapFromItemMetaLookup(itemMetaLookup: { + [itemId: string]: TreeViewItemMeta; +}): TreeViewLabelMap { + const labelMap: { [itemId: string]: string } = {}; + + const processItem = (item: TreeViewItemMeta) => { + labelMap[item.id] = item.label!.toLowerCase(); + }; + + Object.values(itemMetaLookup).forEach(processItem); + + return labelMap; +} diff --git a/packages/x-tree-view/src/internals/plugins/keyboardNavigation/index.ts b/packages/x-tree-view/src/internals/plugins/keyboardNavigation/index.ts new file mode 100644 index 0000000000000..f64f9961a98ad --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/keyboardNavigation/index.ts @@ -0,0 +1 @@ +export * from './TreeViewKeyboardNavigationPlugin'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.test.tsx b/packages/x-tree-view/src/internals/plugins/labelEditing/TreeViewLabelEditingPlugin.test.tsx similarity index 97% rename from packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.test.tsx rename to packages/x-tree-view/src/internals/plugins/labelEditing/TreeViewLabelEditingPlugin.test.tsx index 90865d3a87b55..62cb7eeeab2d4 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/labelEditing/TreeViewLabelEditingPlugin.test.tsx @@ -1,12 +1,9 @@ import { act, fireEvent } from '@mui/internal-test-utils'; import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; - -// TODO #20051: Replace with imported type -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type ExtendableRichTreeViewStore = { parameters: any }; +import { ExtendableRichTreeViewStore } from '@mui/x-tree-view/internals'; describeTreeView>( - 'TreeViewLabelEditingPlugin', + 'useTreeViewLabel plugin', ({ render, treeViewComponentName }) => { const isSimpleTreeView = treeViewComponentName.startsWith('SimpleTreeView'); diff --git a/packages/x-tree-view/src/internals/plugins/labelEditing/TreeViewLabelEditingPlugin.ts b/packages/x-tree-view/src/internals/plugins/labelEditing/TreeViewLabelEditingPlugin.ts new file mode 100644 index 0000000000000..5a69eb2bca172 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/labelEditing/TreeViewLabelEditingPlugin.ts @@ -0,0 +1,64 @@ +import { ExtendableRichTreeViewStore } from '../../RichTreeViewStore/RichTreeViewStore'; +import { TreeViewItemId } from '../../../models'; +import { labelSelectors } from './selectors'; +import { useLabelEditingItemPlugin } from './itemPlugin'; + +export class TreeViewLabelEditingPlugin { + private store: ExtendableRichTreeViewStore; + + constructor(store: ExtendableRichTreeViewStore) { + this.store = store; + store.itemPluginManager.register(useLabelEditingItemPlugin, null); + } + + public buildPublicAPI = () => { + return { + setEditedItem: this.setEditedItem, + updateItemLabel: this.updateItemLabel, + }; + }; + + /** + * Set which item is currently being edited. + * You can pass `null` to exit editing mode. + * @param {TreeViewItemId | null} itemId The id of the item to edit, or `null` to exit editing mode. + */ + public setEditedItem = (itemId: TreeViewItemId | null) => { + if (itemId !== null && !labelSelectors.isItemEditable(this.store.state, itemId)) { + return; + } + + this.store.set('editedItemId', itemId); + }; + + /** + * Used to update the label of an item. + * @param {TreeViewItemId} itemId The id of the item to update the label of. + * @param {string} label The new label of the item. + */ + public updateItemLabel = (itemId: TreeViewItemId, label: string) => { + if (!label) { + throw new Error( + [ + 'MUI X: The Tree View component requires all items to have a `label` property.', + 'The label of an item cannot be empty.', + itemId, + ].join('\n'), + ); + } + + const item = this.store.state.itemMetaLookup[itemId]; + if (item.label === label) { + return; + } + + this.store.set('itemMetaLookup', { + ...this.store.state.itemMetaLookup, + [itemId]: { ...item, label }, + }); + + if (this.store.parameters.onItemLabelChange) { + this.store.parameters.onItemLabelChange(itemId, label); + } + }; +} diff --git a/packages/x-tree-view/src/internals/plugins/labelEditing/index.ts b/packages/x-tree-view/src/internals/plugins/labelEditing/index.ts new file mode 100644 index 0000000000000..36c39f445dbee --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/labelEditing/index.ts @@ -0,0 +1,2 @@ +export * from './TreeViewLabelEditingPlugin'; +export * from './selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/itemPlugin.ts b/packages/x-tree-view/src/internals/plugins/labelEditing/itemPlugin.ts similarity index 83% rename from packages/x-tree-view/src/internals/plugins/useTreeViewLabel/itemPlugin.ts rename to packages/x-tree-view/src/internals/plugins/labelEditing/itemPlugin.ts index 1b6e57c946479..79952bdd59929 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/itemPlugin.ts +++ b/packages/x-tree-view/src/internals/plugins/labelEditing/itemPlugin.ts @@ -4,13 +4,12 @@ import { useStore } from '@mui/x-internals/store'; import { useTreeViewContext } from '../../TreeViewProvider'; import { TreeViewCancellableEvent } from '../../../models'; import { TreeViewItemPlugin } from '../../models'; -import { UseTreeViewItemsSignature } from '../useTreeViewItems'; -import { UseTreeViewLabelSignature } from './useTreeViewLabel.types'; -import { labelSelectors } from './useTreeViewLabel.selectors'; +import { labelSelectors } from './selectors'; +import { ExtendableRichTreeViewStore } from '../../RichTreeViewStore'; import { TreeItemLabelInputProps } from '../../../TreeItemLabelInput'; -export const useTreeViewLabelItemPlugin: TreeViewItemPlugin = ({ props }) => { - const { store } = useTreeViewContext<[UseTreeViewItemsSignature, UseTreeViewLabelSignature]>(); +export const useLabelEditingItemPlugin: TreeViewItemPlugin = ({ props }) => { + const { store } = useTreeViewContext>(); const { label, itemId } = props; const [labelInputValue, setLabelInputValue] = React.useState(label as string); @@ -83,9 +82,9 @@ export const useTreeViewLabelItemPlugin: TreeViewItemPlugin = ({ props }) => { }; }; -export interface UseTreeItemLabelInputSlotPropsFromLabelEditing extends TreeItemLabelInputProps {} +interface UseTreeItemLabelInputSlotPropsFromLabelEditing extends TreeItemLabelInputProps {} -export interface UseTreeItemLabelSlotPropsFromLabelEditing { +interface UseTreeItemLabelSlotPropsFromLabelEditing { editable?: boolean; } diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.selectors.ts b/packages/x-tree-view/src/internals/plugins/labelEditing/selectors.ts similarity index 57% rename from packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.selectors.ts rename to packages/x-tree-view/src/internals/plugins/labelEditing/selectors.ts index da94640d9cf4c..93e49151ad570 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.selectors.ts +++ b/packages/x-tree-view/src/internals/plugins/labelEditing/selectors.ts @@ -1,7 +1,6 @@ import { createSelector } from '@mui/x-internals/store'; -import { UseTreeViewLabelSignature } from './useTreeViewLabel.types'; -import { itemsSelectors } from '../useTreeViewItems/useTreeViewItems.selectors'; -import { TreeViewState } from '../../models'; +import { itemsSelectors } from '../items/selectors'; +import { RichTreeViewState } from '../../RichTreeViewStore'; import { TreeViewItemId } from '../../../models'; export const labelSelectors = { @@ -9,7 +8,7 @@ export const labelSelectors = { * Checks whether an item is editable. */ isItemEditable: createSelector( - (state: TreeViewState<[], [UseTreeViewLabelSignature]>) => state.label?.isItemEditable, + (state: RichTreeViewState) => state.isItemEditable, itemsSelectors.itemModel, (isItemEditable, itemModel, _itemId: TreeViewItemId) => { if (!itemModel || isItemEditable == null) { @@ -27,13 +26,13 @@ export const labelSelectors = { * Checks whether an item is being edited. */ isItemBeingEdited: createSelector( - (state: TreeViewState<[], [UseTreeViewLabelSignature]>, itemId: TreeViewItemId | null) => - itemId == null ? false : state.label?.editedItemId === itemId, + (state: RichTreeViewState, itemId: TreeViewItemId | null) => + itemId == null ? false : state.editedItemId === itemId, ), /** * Checks whether any item is being edited. */ isAnyItemBeingEdited: createSelector( - (state: TreeViewState<[], [UseTreeViewLabelSignature]>) => !!state.label?.editedItemId, + (state: RichTreeViewState) => !!state.editedItemId, ), }; diff --git a/packages/x-tree-view/src/internals/plugins/lazyLoading/index.ts b/packages/x-tree-view/src/internals/plugins/lazyLoading/index.ts new file mode 100644 index 0000000000000..3f07f0bec0489 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/lazyLoading/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/lazyLoading/selectors.ts b/packages/x-tree-view/src/internals/plugins/lazyLoading/selectors.ts new file mode 100644 index 0000000000000..5bfbc0d1747d7 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/lazyLoading/selectors.ts @@ -0,0 +1,41 @@ +import { createSelector } from '@mui/x-internals/store'; +import { TreeViewItemId } from '../../../models'; +import { TREE_VIEW_ROOT_PARENT_ID } from '../items'; +import { RichTreeViewState } from '../../RichTreeViewStore'; + +export const lazyLoadingSelectors = { + /** + * Checks if the lazy loaded state is empty. + */ + isEmpty: createSelector((state: RichTreeViewState) => { + if (state.lazyLoadedItems == null) { + return true; + } + + return ( + Object.keys(state.lazyLoadedItems.loading).length === 0 && + Object.keys(state.lazyLoadedItems.errors).length === 0 + ); + }), + /** + * Checks whether an item is loading. + */ + isItemLoading: createSelector( + (state: RichTreeViewState, itemId: TreeViewItemId | null) => + state.lazyLoadedItems?.loading[itemId ?? TREE_VIEW_ROOT_PARENT_ID] ?? false, + ), + /** + * Checks whether an item has errors. + */ + itemHasError: createSelector( + (state: RichTreeViewState, itemId: TreeViewItemId | null) => + !!state.lazyLoadedItems?.errors[itemId ?? TREE_VIEW_ROOT_PARENT_ID], + ), + /** + * Get an item error. + */ + itemError: createSelector( + (state: RichTreeViewState, itemId: TreeViewItemId | null) => + state.lazyLoadedItems?.errors[itemId ?? TREE_VIEW_ROOT_PARENT_ID], + ), +}; diff --git a/packages/x-tree-view/src/internals/plugins/lazyLoading/types.ts b/packages/x-tree-view/src/internals/plugins/lazyLoading/types.ts new file mode 100644 index 0000000000000..a2d166071fab8 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/lazyLoading/types.ts @@ -0,0 +1,6 @@ +import { TreeViewItemId } from '../../../models'; + +export interface RichTreeViewLazyLoadedItemsStatus { + loading: Record; + errors: Record; +} diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx b/packages/x-tree-view/src/internals/plugins/selection/TreeViewSelectionPlugin.test.tsx similarity index 98% rename from packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx rename to packages/x-tree-view/src/internals/plugins/selection/TreeViewSelectionPlugin.test.tsx index 3a68add330f38..312e9d091fb8a 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/selection/TreeViewSelectionPlugin.test.tsx @@ -2,9 +2,7 @@ import { spy } from 'sinon'; import { fireEvent, act } from '@mui/internal-test-utils'; import { describeTreeView } from 'test/utils/tree-view/describeTreeView'; import { clearWarningsCache } from '@mui/x-internals/warning'; - -// TODO #20051: Replace with imported type -type TreeViewAnyStore = { parameters: any }; +import { TreeViewAnyStore } from '../../models'; /** * All tests related to keyboard navigation (e.g.: selection using "Space") @@ -12,8 +10,6 @@ type TreeViewAnyStore = { parameters: any }; */ describeTreeView( 'TreeViewSelectionPlugin', - // TODO #20051: Remove next line - // eslint-disable-next-line @typescript-eslint/no-unused-vars ({ render, treeViewComponentName }) => { describe('model props (selectedItems, defaultSelectedItems, onSelectedItemsChange)', () => { beforeEach(() => { @@ -152,7 +148,7 @@ describeTreeView( expect(() => { view.setProps({ selectedItems: undefined }); }).toErrorDev( - 'MUI X: A component is changing the controlled selectedItems state of Tree View to be uncontrolled.', + `MUI X Tree View: A component is changing the controlled selectedItems state of ${treeViewComponentName} to be uncontrolled.`, ); }); @@ -166,7 +162,7 @@ describeTreeView( view.setProps({ defaultSelectedItems: ['2'] }); expect(view.getSelectedTreeItems()).to.deep.equal(['1']); }).toErrorDev( - 'MUI X: A component is changing the default selectedItems state of an uncontrolled Tree View after being initialized. To suppress this warning opt to use a controlled Tree View.', + `MUI X Tree View: A component is changing the default selectedItems state of an uncontrolled ${treeViewComponentName} after being initialized.`, ); }); }); diff --git a/packages/x-tree-view/src/internals/plugins/selection/TreeViewSelectionPlugin.ts b/packages/x-tree-view/src/internals/plugins/selection/TreeViewSelectionPlugin.ts new file mode 100644 index 0000000000000..f6d5b3231f532 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/selection/TreeViewSelectionPlugin.ts @@ -0,0 +1,402 @@ +import { EMPTY_OBJECT } from '@base-ui-components/utils/empty'; +import { TreeViewItemId, TreeViewSelectionPropagation } from '../../../models'; +import { TreeViewAnyStore } from '../../models'; +import { itemsSelectors } from '../items'; +import { selectionSelectors } from './selectors'; +import { useSelectionItemPlugin } from './itemPlugin'; +import { + findOrderInTremauxTree, + getAllNavigableItems, + getFirstNavigableItem, + getLastNavigableItem, + getNonDisabledItemsInRange, +} from '../../utils/tree'; +import type { MinimalTreeViewStore } from '../../MinimalTreeViewStore/MinimalTreeViewStore'; +import { TreeViewSelectionValue } from '../../MinimalTreeViewStore/MinimalTreeViewStore.types'; + +export class TreeViewSelectionPlugin { + private store: MinimalTreeViewStore; + + private lastSelectedItem: TreeViewItemId | null = null; + + private lastSelectedRange: Record = {}; + + // We can't type `store`, otherwise we get the following TS error: + // 'selection' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. + constructor(store: any) { + this.store = store; + store.itemPluginManager.register(useSelectionItemPlugin, null); + } + + private setSelectedItems = ( + event: React.SyntheticEvent | null, + newModel: string[] | string | null, + additionalItemsToPropagate?: TreeViewItemId[], + ) => { + const { + selectionPropagation = EMPTY_OBJECT as TreeViewSelectionPropagation, + selectedItems, + onItemSelectionToggle, + onSelectedItemsChange, + } = this.store.parameters; + + const oldModel = selectionSelectors.selectedItemsRaw(this.store.state); + let cleanModel: TreeViewSelectionValue; + const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(this.store.state); + + if ( + isMultiSelectEnabled && + (selectionPropagation.descendants || selectionPropagation.parents) + ) { + cleanModel = propagateSelection({ + store: this.store, + selectionPropagation, + newModel: newModel as string[], + oldModel: oldModel as string[], + additionalItemsToPropagate, + }) as TreeViewSelectionValue; + } else { + cleanModel = newModel as TreeViewSelectionValue; + } + + if (onItemSelectionToggle) { + if (isMultiSelectEnabled) { + const changes = getAddedAndRemovedItems({ + store: this.store, + newModel: cleanModel as string[], + oldModel: oldModel as string[], + }); + + if (onItemSelectionToggle) { + changes.added.forEach((itemId) => { + onItemSelectionToggle!(event, itemId, true); + }); + + changes.removed.forEach((itemId) => { + onItemSelectionToggle!(event, itemId, false); + }); + } + } else if (cleanModel !== oldModel) { + if (oldModel != null) { + onItemSelectionToggle(event, oldModel as string, false); + } + if (cleanModel != null) { + onItemSelectionToggle(event, cleanModel as string, true); + } + } + } + + if (selectedItems === undefined) { + this.store.set('selectedItems', cleanModel); + } + + onSelectedItemsChange?.(event, cleanModel); + }; + + private selectRange = (event: React.SyntheticEvent, [start, end]: [string, string]) => { + const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(this.store.state); + if (!isMultiSelectEnabled) { + return; + } + + let newSelectedItems = selectionSelectors.selectedItems(this.store.state).slice(); + + // If the last selection was a range selection, + // remove the items that were part of the last range from the model + if (Object.keys(this.lastSelectedRange).length > 0) { + newSelectedItems = newSelectedItems.filter((id) => !this.lastSelectedRange[id]); + } + + // Add to the model the items that are part of the new range and not already part of the model. + const selectedItemsLookup = getLookupFromArray(newSelectedItems); + const range = getNonDisabledItemsInRange(this.store.state, start, end); + const itemsToAddToModel = range.filter((id) => !selectedItemsLookup[id]); + newSelectedItems = newSelectedItems.concat(itemsToAddToModel); + + this.setSelectedItems(event, newSelectedItems); + this.lastSelectedRange = getLookupFromArray(range); + }; + + public buildPublicAPI = () => { + return { + setItemSelection: this.setItemSelection, + }; + }; + + /** + * Select or deselect an item. + * @param {object} parameters The parameters of the method. + * @param {TreeViewItemId} parameters.itemId The id of the item to select or deselect. + * @param {React.SyntheticEvent} parameters.event The DOM event that triggered the change. + * @param {boolean} parameters.keepExistingSelection If `true`, the other already selected items will remain selected, otherwise, they will be deselected. This parameter is only relevant when `multiSelect` is `true` + * @param {boolean | undefined} parameters.shouldBeSelected If `true` the item will be selected. If `false` the item will be deselected. If not defined, the item's selection status will be toggled. + */ + public setItemSelection = ({ + itemId, + event = null, + keepExistingSelection = false, + shouldBeSelected, + }: { + itemId: string; + event?: React.SyntheticEvent | null; + shouldBeSelected?: boolean; + keepExistingSelection?: boolean; + }) => { + if (!selectionSelectors.enabled(this.store.state)) { + return; + } + + let newSelected: TreeViewSelectionValue; + const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(this.store.state); + if (keepExistingSelection) { + const oldSelected = selectionSelectors.selectedItems(this.store.state); + const isSelectedBefore = selectionSelectors.isItemSelected(this.store.state, itemId); + if (isSelectedBefore && (shouldBeSelected === false || shouldBeSelected == null)) { + newSelected = oldSelected.filter((id) => id !== itemId); + } else if (!isSelectedBefore && (shouldBeSelected === true || shouldBeSelected == null)) { + newSelected = [itemId].concat(oldSelected); + } else { + newSelected = oldSelected; + } + } else { + // eslint-disable-next-line no-lonely-if + if ( + shouldBeSelected === false || + (shouldBeSelected == null && selectionSelectors.isItemSelected(this.store.state, itemId)) + ) { + newSelected = isMultiSelectEnabled ? [] : null; + } else { + newSelected = isMultiSelectEnabled ? [itemId] : itemId; + } + } + + this.setSelectedItems( + event, + newSelected, + // If shouldBeSelected === selectionSelectors.isItemSelected(store, itemId), we still want to propagate the select. + // This is useful when the element is in an indeterminate state. + [itemId], + ); + this.lastSelectedItem = itemId; + this.lastSelectedRange = {}; + }; + + /** + * Select all the navigable items in the tree. + * @param {React.SyntheticEvent} event The DOM event that triggered the change. + */ + public selectAllNavigableItems = (event: React.SyntheticEvent) => { + const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(this.store.state); + if (!isMultiSelectEnabled) { + return; + } + + const navigableItems = getAllNavigableItems(this.store.state); + this.setSelectedItems(event, navigableItems); + + this.lastSelectedRange = getLookupFromArray(navigableItems); + }; + + /** + * Expand the current selection range up to the given item. + * @param {React.SyntheticEvent} event The DOM event that triggered the change. + * @param {TreeViewItemId} itemId The id of the item to expand the selection to. + */ + public expandSelectionRange = (event: React.SyntheticEvent, itemId: string) => { + if (this.lastSelectedItem != null) { + const [start, end] = findOrderInTremauxTree(this.store.state, itemId, this.lastSelectedItem); + this.selectRange(event, [start, end]); + } + }; + + /** + * Expand the current selection range from the first navigable item to the given item. + * @param {React.SyntheticEvent} event The DOM event that triggered the change. + * @param {TreeViewItemId} itemId The id of the item up to which the selection range should be expanded. + */ + public selectRangeFromStartToItem = (event: React.SyntheticEvent, itemId: string) => { + this.selectRange(event, [getFirstNavigableItem(this.store.state), itemId]); + }; + + /** + * Expand the current selection range from the given item to the last navigable item. + * @param {React.SyntheticEvent} event The DOM event that triggered the change. + * @param {TreeViewItemId} itemId The id of the item from which the selection range should be expanded. + */ + public selectRangeFromItemToEnd = (event: React.SyntheticEvent, itemId: string) => { + this.selectRange(event, [itemId, getLastNavigableItem(this.store.state)]); + }; + + /** + * Update the selection when navigating with ArrowUp / ArrowDown keys. + * @param {React.SyntheticEvent} event The DOM event that triggered the change. + * @param {TreeViewItemId} currentItemId The id of the active item before the keyboard navigation. + * @param {TreeViewItemId} nextItemId The id of the active item after the keyboard navigation. + */ + public selectItemFromArrowNavigation = ( + event: React.SyntheticEvent, + currentItem: string, + nextItem: string, + ) => { + const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(this.store.state); + if (!isMultiSelectEnabled) { + return; + } + + let newSelectedItems = selectionSelectors.selectedItems(this.store.state).slice(); + + if (Object.keys(this.lastSelectedRange).length === 0) { + newSelectedItems.push(nextItem); + this.lastSelectedRange = { [currentItem]: true, [nextItem]: true }; + } else { + if (!this.lastSelectedRange[currentItem]) { + this.lastSelectedRange = {}; + } + + if (this.lastSelectedRange[nextItem]) { + newSelectedItems = newSelectedItems.filter((id) => id !== currentItem); + delete this.lastSelectedRange[currentItem]; + } else { + newSelectedItems.push(nextItem); + this.lastSelectedRange[nextItem] = true; + } + } + + this.setSelectedItems(event, newSelectedItems); + }; +} + +function propagateSelection({ + store, + selectionPropagation, + newModel, + oldModel, + additionalItemsToPropagate, +}: { + store: TreeViewAnyStore; + selectionPropagation: TreeViewSelectionPropagation; + newModel: TreeViewItemId[]; + oldModel: TreeViewItemId[]; + additionalItemsToPropagate?: TreeViewItemId[]; +}): TreeViewItemId[] { + if (!selectionPropagation.descendants && !selectionPropagation.parents) { + return newModel; + } + + let shouldRegenerateModel = false; + const newModelLookup = getLookupFromArray(newModel); + + const changes = getAddedAndRemovedItems({ + store, + newModel, + oldModel, + }); + + additionalItemsToPropagate?.forEach((itemId) => { + if (newModelLookup[itemId]) { + if (!changes.added.includes(itemId)) { + changes.added.push(itemId); + } + } else if (!changes.removed.includes(itemId)) { + changes.removed.push(itemId); + } + }); + + changes.added.forEach((addedItemId) => { + if (selectionPropagation.descendants) { + const selectDescendants = (itemId: TreeViewItemId) => { + if (itemId !== addedItemId) { + shouldRegenerateModel = true; + newModelLookup[itemId] = true; + } + + itemsSelectors.itemOrderedChildrenIds(store.state, itemId).forEach(selectDescendants); + }; + + selectDescendants(addedItemId); + } + + if (selectionPropagation.parents) { + const checkAllDescendantsSelected = (itemId: TreeViewItemId): boolean => { + if (!newModelLookup[itemId]) { + return false; + } + + const children = itemsSelectors.itemOrderedChildrenIds(store.state, itemId); + return children.every(checkAllDescendantsSelected); + }; + + const selectParents = (itemId: TreeViewItemId) => { + const parentId = itemsSelectors.itemParentId(store.state, itemId); + if (parentId == null) { + return; + } + + const siblings = itemsSelectors.itemOrderedChildrenIds(store.state, parentId); + if (siblings.every(checkAllDescendantsSelected)) { + shouldRegenerateModel = true; + newModelLookup[parentId] = true; + selectParents(parentId); + } + }; + selectParents(addedItemId); + } + }); + + changes.removed.forEach((removedItemId) => { + if (selectionPropagation.parents) { + let parentId = itemsSelectors.itemParentId(store.state, removedItemId); + while (parentId != null) { + if (newModelLookup[parentId]) { + shouldRegenerateModel = true; + delete newModelLookup[parentId]; + } + + parentId = itemsSelectors.itemParentId(store.state, parentId); + } + } + + if (selectionPropagation.descendants) { + const deSelectDescendants = (itemId: TreeViewItemId) => { + if (itemId !== removedItemId) { + shouldRegenerateModel = true; + delete newModelLookup[itemId]; + } + + itemsSelectors.itemOrderedChildrenIds(store.state, itemId).forEach(deSelectDescendants); + }; + + deSelectDescendants(removedItemId); + } + }); + + return shouldRegenerateModel ? Object.keys(newModelLookup) : newModel; +} + +function getAddedAndRemovedItems({ + store, + oldModel, + newModel, +}: { + store: TreeViewAnyStore; + oldModel: TreeViewItemId[]; + newModel: TreeViewItemId[]; +}) { + const newModelMap = new Map(); + newModel.forEach((id) => { + newModelMap.set(id, true); + }); + + return { + added: newModel.filter((itemId) => !selectionSelectors.isItemSelected(store.state, itemId)), + removed: oldModel.filter((itemId) => !newModelMap.has(itemId)), + }; +} + +export function getLookupFromArray(array: string[]) { + const lookup: { [itemId: string]: true } = {}; + array.forEach((itemId) => { + lookup[itemId] = true; + }); + return lookup; +} diff --git a/packages/x-tree-view/src/internals/plugins/selection/index.ts b/packages/x-tree-view/src/internals/plugins/selection/index.ts new file mode 100644 index 0000000000000..4b752f2b074e4 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/selection/index.ts @@ -0,0 +1,2 @@ +export * from './TreeViewSelectionPlugin'; +export * from './selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/itemPlugin.ts b/packages/x-tree-view/src/internals/plugins/selection/itemPlugin.ts similarity index 86% rename from packages/x-tree-view/src/internals/plugins/useTreeViewSelection/itemPlugin.ts rename to packages/x-tree-view/src/internals/plugins/selection/itemPlugin.ts index bea4c88e8342d..ed05f30a38751 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/itemPlugin.ts +++ b/packages/x-tree-view/src/internals/plugins/selection/itemPlugin.ts @@ -6,17 +6,13 @@ import { TreeViewCancellableEventHandler, } from '../../../models'; import { useTreeViewContext } from '../../TreeViewProvider'; -import { TreeViewItemPlugin, TreeViewState } from '../../models'; -import { UseTreeViewSelectionSignature } from './useTreeViewSelection.types'; -import { UseTreeViewItemsSignature } from '../useTreeViewItems'; -import { itemsSelectors } from '../useTreeViewItems/useTreeViewItems.selectors'; -import { selectionSelectors } from './useTreeViewSelection.selectors'; +import { TreeViewAnyStore, TreeViewItemPlugin } from '../../models'; +import { itemsSelectors } from '../items/selectors'; +import { selectionSelectors } from './selectors'; +import { MinimalTreeViewState } from '../../MinimalTreeViewStore'; const selectorCheckboxSelectionStatus = createSelector( - ( - state: TreeViewState<[UseTreeViewItemsSignature, UseTreeViewSelectionSignature]>, - itemId: TreeViewItemId, - ) => { + (state: MinimalTreeViewState, itemId: TreeViewItemId) => { if (selectionSelectors.isItemSelected(state, itemId)) { return 'checked'; } @@ -57,11 +53,10 @@ const selectorCheckboxSelectionStatus = createSelector( }, ); -export const useTreeViewSelectionItemPlugin: TreeViewItemPlugin = ({ props }) => { +export const useSelectionItemPlugin: TreeViewItemPlugin = ({ props }) => { const { itemId } = props; - const { store } = - useTreeViewContext<[UseTreeViewItemsSignature, UseTreeViewSelectionSignature]>(); + const { store } = useTreeViewContext(); const isCheckboxSelectionEnabled = useStore(store, selectionSelectors.isCheckboxSelectionEnabled); const isItemSelectionEnabled = useStore(store, selectionSelectors.canItemBeSelected, itemId); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors.ts b/packages/x-tree-view/src/internals/plugins/selection/selectors.ts similarity index 65% rename from packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors.ts rename to packages/x-tree-view/src/internals/plugins/selection/selectors.ts index d742dfc00f982..91ca7faf3686b 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors.ts +++ b/packages/x-tree-view/src/internals/plugins/selection/selectors.ts @@ -1,11 +1,10 @@ import { createSelector, createSelectorMemoized } from '@mui/x-internals/store'; import { TreeViewItemId } from '../../../models'; -import { TreeViewState } from '../../models'; -import { itemsSelectors } from '../useTreeViewItems/useTreeViewItems.selectors'; -import { UseTreeViewSelectionSignature } from './useTreeViewSelection.types'; +import { MinimalTreeViewState } from '../../MinimalTreeViewStore'; +import { itemsSelectors } from '../items/selectors'; const selectedItemsSelector = createSelectorMemoized( - (state: TreeViewState<[UseTreeViewSelectionSignature]>) => state.selection.selectedItems, + (state: MinimalTreeViewState) => state.selectedItems, (selectedItemsRaw) => { if (Array.isArray(selectedItemsRaw)) { return selectedItemsRaw; @@ -31,9 +30,7 @@ export const selectionSelectors = { /** * Gets the selected items as provided to the component. */ - selectedItemsRaw: createSelector( - (state: TreeViewState<[UseTreeViewSelectionSignature]>) => state.selection.selectedItems, - ), + selectedItemsRaw: createSelector((state: MinimalTreeViewState) => state.selectedItems), /** * Gets the selected items as an array. */ @@ -45,27 +42,24 @@ export const selectionSelectors = { /** * Checks whether selection is enabled. */ - enabled: createSelector( - (state: TreeViewState<[UseTreeViewSelectionSignature]>) => state.selection.isEnabled, - ), + enabled: createSelector((state: MinimalTreeViewState) => !state.disableSelection), /** * Checks whether multi selection is enabled. */ isMultiSelectEnabled: createSelector( - (state: TreeViewState<[UseTreeViewSelectionSignature]>) => state.selection.isMultiSelectEnabled, + (state: MinimalTreeViewState) => state.multiSelect, ), /** * Checks whether checkbox selection is enabled. */ isCheckboxSelectionEnabled: createSelector( - (state: TreeViewState<[UseTreeViewSelectionSignature]>) => - state.selection.isCheckboxSelectionEnabled, + (state: MinimalTreeViewState) => state.checkboxSelection, ), /** * Gets the selection propagation rules. */ propagationRules: createSelector( - (state: TreeViewState<[UseTreeViewSelectionSignature]>) => state.selection.selectionPropagation, + (state: MinimalTreeViewState) => state.selectionPropagation, ), /** * Checks whether an item is selected. @@ -79,7 +73,7 @@ export const selectionSelectors = { */ canItemBeSelected: createSelector( itemsSelectors.isItemDisabled, - (state: TreeViewState<[UseTreeViewSelectionSignature]>) => state.selection.isEnabled, + (state: MinimalTreeViewState) => !state.disableSelection, (isItemDisabled, isSelectionEnabled, _itemId: TreeViewItemId) => isSelectionEnabled && !isItemDisabled, ), diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/index.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/index.ts deleted file mode 100644 index 3718b390774f5..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { useTreeViewExpansion } from './useTreeViewExpansion'; -export type { - UseTreeViewExpansionSignature, - UseTreeViewExpansionParameters, - UseTreeViewExpansionParametersWithDefaults, -} from './useTreeViewExpansion.types'; -export { expansionSelectors } from './useTreeViewExpansion.selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts deleted file mode 100644 index cea7c3cc569c3..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts +++ /dev/null @@ -1,177 +0,0 @@ -import * as React from 'react'; -import { useAssertModelConsistency } from '@mui/x-internals/useAssertModelConsistency'; -import { useEventCallback } from '@base-ui-components/utils/useEventCallback'; -import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; -import { TreeViewPlugin } from '../../models'; -import { - UseTreeViewExpansionInstance, - UseTreeViewExpansionPublicAPI, - UseTreeViewExpansionSignature, -} from './useTreeViewExpansion.types'; -import { TreeViewItemId } from '../../../models'; -import { expansionSelectors } from './useTreeViewExpansion.selectors'; -import { getExpansionTrigger } from './useTreeViewExpansion.utils'; -import { itemsSelectors } from '../useTreeViewItems/useTreeViewItems.selectors'; -import { publishTreeViewEvent } from '../../utils/publishTreeViewEvent'; - -export const useTreeViewExpansion: TreeViewPlugin = ({ - instance, - store, - params, -}) => { - useAssertModelConsistency({ - componentName: 'Tree View', - propName: 'expandedItems', - controlled: params.expandedItems, - defaultValue: params.defaultExpandedItems, - }); - - useIsoLayoutEffect(() => { - const newExpansionTrigger = getExpansionTrigger({ - isItemEditable: params.isItemEditable, - expansionTrigger: params.expansionTrigger, - }); - if (store.state.expansion.expansionTrigger === newExpansionTrigger) { - return; - } - - store.set('expansion', { - ...store.state.expansion, - expansionTrigger: newExpansionTrigger, - }); - }, [store, params.isItemEditable, params.expansionTrigger]); - - const setExpandedItems = (event: React.SyntheticEvent | null, value: TreeViewItemId[]) => { - if (params.expandedItems === undefined) { - store.set('expansion', { - ...store.state.expansion, - expandedItems: value, - }); - } - params.onExpandedItemsChange?.(event, value); - }; - - const resetItemExpansion = useEventCallback(() => { - setExpandedItems(null, []); - }); - - const applyItemExpansion: UseTreeViewExpansionInstance['applyItemExpansion'] = useEventCallback( - ({ itemId, event, shouldBeExpanded }) => { - const oldExpanded = expansionSelectors.expandedItemsRaw(store.state); - let newExpanded: string[]; - if (shouldBeExpanded) { - newExpanded = [itemId].concat(oldExpanded); - } else { - newExpanded = oldExpanded.filter((id) => id !== itemId); - } - - if (params.onItemExpansionToggle) { - params.onItemExpansionToggle(event, itemId, shouldBeExpanded); - } - - setExpandedItems(event, newExpanded); - }, - ); - - const setItemExpansion: UseTreeViewExpansionInstance['setItemExpansion'] = useEventCallback( - ({ itemId, event = null, shouldBeExpanded }) => { - const isExpandedBefore = expansionSelectors.isItemExpanded(store.state, itemId); - const cleanShouldBeExpanded = shouldBeExpanded ?? !isExpandedBefore; - if (isExpandedBefore === cleanShouldBeExpanded) { - return; - } - - const eventParameters = { - isExpansionPrevented: false, - shouldBeExpanded: cleanShouldBeExpanded, - event, - itemId, - }; - publishTreeViewEvent(instance, 'beforeItemToggleExpansion', eventParameters); - if (eventParameters.isExpansionPrevented) { - return; - } - - instance.applyItemExpansion({ itemId, event, shouldBeExpanded: cleanShouldBeExpanded }); - }, - ); - - const isItemExpanded: UseTreeViewExpansionPublicAPI['isItemExpanded'] = useEventCallback( - (itemId) => { - return expansionSelectors.isItemExpanded(store.state, itemId); - }, - ); - - const expandAllSiblings = (event: React.KeyboardEvent, itemId: TreeViewItemId) => { - const itemMeta = itemsSelectors.itemMeta(store.state, itemId); - if (itemMeta == null) { - return; - } - - const siblings = itemsSelectors.itemOrderedChildrenIds(store.state, itemMeta.parentId); - - const diff = siblings.filter( - (child) => - expansionSelectors.isItemExpandable(store.state, child) && - !expansionSelectors.isItemExpanded(store.state, child), - ); - - const newExpanded = expansionSelectors.expandedItemsRaw(store.state).concat(diff); - - if (diff.length > 0) { - if (params.onItemExpansionToggle) { - diff.forEach((newlyExpandedItemId) => { - params.onItemExpansionToggle!(event, newlyExpandedItemId, true); - }); - } - - setExpandedItems(event, newExpanded); - } - }; - - /** - * Update the controlled model when the `expandedItems` prop changes. - */ - useIsoLayoutEffect(() => { - const expandedItems = params.expandedItems; - if (expandedItems !== undefined) { - store.set('expansion', { ...store.state.expansion, expandedItems }); - } - }, [store, params.expandedItems]); - - return { - publicAPI: { - setItemExpansion, - isItemExpanded, - }, - instance: { - setItemExpansion, - applyItemExpansion, - expandAllSiblings, - resetItemExpansion, - }, - }; -}; - -const DEFAULT_EXPANDED_ITEMS: string[] = []; - -useTreeViewExpansion.applyDefaultValuesToParams = ({ params }) => ({ - ...params, - defaultExpandedItems: params.defaultExpandedItems ?? DEFAULT_EXPANDED_ITEMS, -}); - -useTreeViewExpansion.getInitialState = (params) => ({ - expansion: { - expandedItems: - params.expandedItems === undefined ? params.defaultExpandedItems : params.expandedItems, - expansionTrigger: getExpansionTrigger(params), - }, -}); - -useTreeViewExpansion.params = { - expandedItems: true, - defaultExpandedItems: true, - onExpandedItemsChange: true, - onItemExpansionToggle: true, - expansionTrigger: true, -}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts deleted file mode 100644 index bf7b65f042267..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts +++ /dev/null @@ -1,127 +0,0 @@ -import * as React from 'react'; -import { DefaultizedProps } from '@mui/x-internals/types'; -import { TreeViewPluginSignature } from '../../models'; -import { UseTreeViewItemsSignature } from '../useTreeViewItems'; -import { TreeViewItemId } from '../../../models'; -import { UseTreeViewLabelSignature } from '../useTreeViewLabel'; - -export interface UseTreeViewExpansionPublicAPI { - /** - * Change the expansion status of a given item. - * @param {object} parameters The parameters of the method. - * @param {TreeViewItemId} parameters.itemId The id of the item to expand of collapse. - * @param {React.SyntheticEvent} parameters.event The DOM event that triggered the change. - * @param {boolean} parameters.shouldBeExpanded If `true` the item will be expanded. If `false` the item will be collapsed. If not defined, the item's expansion status will be the toggled. - */ - setItemExpansion: (parameters: { - itemId: TreeViewItemId; - event?: React.SyntheticEvent; - shouldBeExpanded?: boolean; - }) => void; - /** - * Check if an item is expanded. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean} `true` if the item is expanded, `false` otherwise. - */ - isItemExpanded: (itemId: TreeViewItemId) => boolean; -} - -export interface UseTreeViewExpansionInstance - // We don't expose isItemExpanded here to make sure we always use the selector internally. - extends Omit { - /** - * Expand all the siblings (i.e.: the items that have the same parent) of a given item. - * @param {React.SyntheticEvent} event The DOM event that triggered the change. - * @param {TreeViewItemId} itemId The id of the item whose siblings will be expanded. - */ - expandAllSiblings: (event: React.KeyboardEvent, itemId: TreeViewItemId) => void; - /** - * Apply the new expansion status of a given item. - * Is used by the `setItemExpansion` method and by the `useTreeViewLazyLoading` plugin. - * Unlike `setItemExpansion`, this method does not trigger the lazy loading. - * @param {object} parameters The parameters of the method. - * @param {TreeViewItemId} parameters.itemId The id of the item to expand of collapse. - * @param {React.SyntheticEvent | null} parameters.event The DOM event that triggered the change. - * @param {boolean} parameters.shouldBeExpanded If `true` the item will be expanded. If `false` the item will be collapsed. - */ - applyItemExpansion: (parameters: { - itemId: TreeViewItemId; - event: React.SyntheticEvent | null; - shouldBeExpanded: boolean; - }) => void; - /** - * Reset the expansion state of all items. - */ - resetItemExpansion: () => void; -} - -export interface UseTreeViewExpansionParameters { - /** - * Expanded item ids. - * Used when the item's expansion is controlled. - */ - expandedItems?: readonly TreeViewItemId[]; - /** - * Expanded item ids. - * Used when the item's expansion is not controlled. - * @default [] - */ - defaultExpandedItems?: readonly TreeViewItemId[]; - /** - * Callback fired when Tree Items are expanded/collapsed. - * @param {React.SyntheticEvent} event The DOM event that triggered the change. Can be null when the change is caused by the `publicAPI.setItemExpansion()` method. - * @param {TreeViewItemId[]} itemIds The ids of the expanded items. - */ - onExpandedItemsChange?: (event: React.SyntheticEvent | null, itemIds: TreeViewItemId[]) => void; - /** - * Callback fired when a Tree Item is expanded or collapsed. - * @param {React.SyntheticEvent | null} event The DOM event that triggered the change. Can be null when the change is caused by the `publicAPI.setItemExpansion()` method. - * @param {TreeViewItemId} itemId The itemId of the modified item. - * @param {boolean} isExpanded `true` if the item has just been expanded, `false` if it has just been collapsed. - */ - onItemExpansionToggle?: ( - event: React.SyntheticEvent | null, - itemId: TreeViewItemId, - isExpanded: boolean, - ) => void; - /** - * The slot that triggers the item's expansion when clicked. - * @default 'content' - */ - expansionTrigger?: 'content' | 'iconContainer'; -} - -export type UseTreeViewExpansionParametersWithDefaults = DefaultizedProps< - UseTreeViewExpansionParameters, - 'defaultExpandedItems' ->; - -export interface UseTreeViewExpansionState { - expansion: { - expandedItems: readonly TreeViewItemId[]; - expansionTrigger: 'content' | 'iconContainer'; - }; -} - -interface UseTreeViewExpansionEventLookup { - beforeItemToggleExpansion: { - params: { - isExpansionPrevented: boolean; - shouldBeExpanded: boolean; - event: React.SyntheticEvent | null; - itemId: TreeViewItemId; - }; - }; -} - -export type UseTreeViewExpansionSignature = TreeViewPluginSignature<{ - params: UseTreeViewExpansionParameters; - paramsWithDefaults: UseTreeViewExpansionParametersWithDefaults; - instance: UseTreeViewExpansionInstance; - publicAPI: UseTreeViewExpansionPublicAPI; - modelNames: 'expandedItems'; - state: UseTreeViewExpansionState; - dependencies: [UseTreeViewItemsSignature]; - optionalDependencies: [UseTreeViewLabelSignature]; - events: UseTreeViewExpansionEventLookup; -}>; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.utils.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.utils.ts deleted file mode 100644 index 179ebec1ff42f..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TreeViewUsedParamsWithDefaults } from '../../models'; -import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types'; - -export const getExpansionTrigger = ({ - isItemEditable, - expansionTrigger, -}: Pick< - TreeViewUsedParamsWithDefaults, - 'isItemEditable' | 'expansionTrigger' ->) => { - if (expansionTrigger) { - return expansionTrigger; - } - - if (isItemEditable) { - return 'iconContainer'; - } - - return 'content'; -}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/index.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/index.ts deleted file mode 100644 index d21aa0cc752ba..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { useTreeViewFocus } from './useTreeViewFocus'; -export type { - UseTreeViewFocusSignature, - UseTreeViewFocusParameters, - UseTreeViewFocusParametersWithDefaults, -} from './useTreeViewFocus.types'; -export { focusSelectors } from './useTreeViewFocus.selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts deleted file mode 100644 index f3926ad14523d..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts +++ /dev/null @@ -1,140 +0,0 @@ -import * as React from 'react'; -import { useEventCallback } from '@base-ui-components/utils/useEventCallback'; -import { EventHandlers } from '@mui/utils/types'; -import { useStoreEffect } from '@mui/x-internals/store'; -import { TreeViewPlugin } from '../../models'; -import { UseTreeViewFocusSignature } from './useTreeViewFocus.types'; -import { TreeViewCancellableEvent } from '../../../models'; -import { focusSelectors } from './useTreeViewFocus.selectors'; -import { expansionSelectors } from '../useTreeViewExpansion/useTreeViewExpansion.selectors'; -import { itemsSelectors } from '../useTreeViewItems/useTreeViewItems.selectors'; - -export const useTreeViewFocus: TreeViewPlugin = ({ - instance, - params, - store, -}) => { - const setFocusedItemId = useEventCallback((itemId: string | null) => { - const focusedItemId = focusSelectors.focusedItemId(store.state); - if (focusedItemId === itemId) { - return; - } - - store.set('focus', { ...store.state.focus, focusedItemId: itemId }); - }); - - const isItemVisible = (itemId: string) => { - const itemMeta = itemsSelectors.itemMeta(store.state, itemId); - return ( - itemMeta && - (itemMeta.parentId == null || - expansionSelectors.isItemExpanded(store.state, itemMeta.parentId)) - ); - }; - - const innerFocusItem = (event: React.SyntheticEvent | null, itemId: string) => { - const itemElement = instance.getItemDOMElement(itemId); - if (itemElement) { - itemElement.focus(); - } - - setFocusedItemId(itemId); - - if (params.onItemFocus) { - params.onItemFocus(event, itemId); - } - }; - - const focusItem = useEventCallback((event: React.SyntheticEvent | null, itemId: string) => { - // If we receive an itemId, and it is visible, the focus will be set to it - if (isItemVisible(itemId)) { - innerFocusItem(event, itemId); - } - }); - - const removeFocusedItem = useEventCallback(() => { - const focusedItemId = focusSelectors.focusedItemId(store.state); - if (focusedItemId == null) { - return; - } - - const itemMeta = itemsSelectors.itemMeta(store.state, focusedItemId); - if (itemMeta) { - const itemElement = instance.getItemDOMElement(focusedItemId); - if (itemElement) { - itemElement.blur(); - } - } - - setFocusedItemId(null); - }); - - // Whenever the items change, we need to ensure the focused item is still present. - useStoreEffect(store, itemsSelectors.itemMetaLookup, () => { - const focusedItemId = focusSelectors.focusedItemId(store.state); - if (focusedItemId == null) { - return; - } - - const hasItemBeenRemoved = !itemsSelectors.itemMeta(store.state, focusedItemId); - if (!hasItemBeenRemoved) { - return; - } - - const defaultFocusableItemId = focusSelectors.defaultFocusableItemId(store.state); - if (defaultFocusableItemId == null) { - setFocusedItemId(null); - return; - } - - innerFocusItem(null, defaultFocusableItemId); - }); - - const createRootHandleFocus = - (otherHandlers: EventHandlers) => - (event: React.FocusEvent & TreeViewCancellableEvent) => { - otherHandlers.onFocus?.(event); - if (event.defaultMuiPrevented) { - return; - } - - // if the event bubbled (which is React specific) we don't want to steal focus - const defaultFocusableItemId = focusSelectors.defaultFocusableItemId(store.state); - if (event.target === event.currentTarget && defaultFocusableItemId != null) { - innerFocusItem(event, defaultFocusableItemId); - } - }; - - const createRootHandleBlur = - (otherHandlers: EventHandlers) => - (event: React.FocusEvent & TreeViewCancellableEvent) => { - otherHandlers.onBlur?.(event); - if (event.defaultMuiPrevented) { - return; - } - - setFocusedItemId(null); - }; - - return { - getRootProps: (otherHandlers) => ({ - onFocus: createRootHandleFocus(otherHandlers), - onBlur: createRootHandleBlur(otherHandlers), - }), - publicAPI: { - focusItem, - }, - instance: { - focusItem, - removeFocusedItem, - }, - }; -}; - -useTreeViewFocus.getInitialState = () => ({ - focus: { focusedItemId: null }, -}); - -useTreeViewFocus.params = { - onItemFocus: true, -}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts deleted file mode 100644 index f448c12ca87b7..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from 'react'; -import { TreeViewPluginSignature } from '../../models'; -import type { UseTreeViewItemsSignature } from '../useTreeViewItems'; -import type { UseTreeViewSelectionSignature } from '../useTreeViewSelection'; -import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion'; -import { TreeViewItemId } from '../../../models'; - -export interface UseTreeViewFocusPublicAPI { - /** - * Focus the item with the given id. - * - * If the item is the child of a collapsed item, then this method will do nothing. - * Make sure to expand the ancestors of the item before calling this method if needed. - * @param {React.SyntheticEvent | null} event The DOM event that triggered the change. - * @param {TreeViewItemId} itemId The id of the item to focus. - */ - focusItem: (event: React.SyntheticEvent | null, itemId: TreeViewItemId) => void; -} - -export interface UseTreeViewFocusInstance extends UseTreeViewFocusPublicAPI { - /** - * Remove the focus from the currently focused item (both from the internal state and the DOM). - */ - removeFocusedItem: () => void; -} - -export interface UseTreeViewFocusParameters { - /** - * Callback fired when a given Tree Item is focused. - * @param {React.SyntheticEvent | null} event The DOM event that triggered the change. **Warning**: This is a generic event not a focus event. - * @param {TreeViewItemId} itemId The id of the focused item. - */ - onItemFocus?: (event: React.SyntheticEvent | null, itemId: TreeViewItemId) => void; -} - -export type UseTreeViewFocusParametersWithDefaults = UseTreeViewFocusParameters; - -export interface UseTreeViewFocusState { - focus: { - focusedItemId: TreeViewItemId | null; - }; -} - -export type UseTreeViewFocusSignature = TreeViewPluginSignature<{ - params: UseTreeViewFocusParameters; - paramsWithDefaults: UseTreeViewFocusParametersWithDefaults; - instance: UseTreeViewFocusInstance; - publicAPI: UseTreeViewFocusPublicAPI; - state: UseTreeViewFocusState; - dependencies: [ - UseTreeViewItemsSignature, - UseTreeViewSelectionSignature, - UseTreeViewExpansionSignature, - ]; -}>; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/index.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/index.ts deleted file mode 100644 index 0f6dafcddb90a..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { useTreeViewItems } from './useTreeViewItems'; -export type { - UseTreeViewItemsSignature, - UseTreeViewItemsParameters, - UseTreeViewItemsParametersWithDefaults, - UseTreeViewItemsState, -} from './useTreeViewItems.types'; -export { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils'; -export { itemsSelectors } from './useTreeViewItems.selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.selectors.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.selectors.ts deleted file mode 100644 index 52a5cd0150b51..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.selectors.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { createSelector } from '@mui/x-internals/store'; -import { TreeViewItemId } from '../../../models'; -import { TreeViewItemMeta, TreeViewState } from '../../models'; -import { isItemDisabled, TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils'; -import { UseTreeViewItemsSignature } from './useTreeViewItems.types'; - -const EMPTY_CHILDREN: TreeViewItemId[] = []; - -export const itemsSelectors = { - /** - * Gets the DOM structure of the Tree View. - */ - domStructure: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>) => state.items.domStructure, - ), - /** - * Checks whether the disabled items are focusable. - */ - disabledItemFocusable: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>) => state.items.disabledItemsFocusable, - ), - /** - * Gets the meta-information of all items. - */ - itemMetaLookup: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>) => state.items.itemMetaLookup, - ), - /** - * Gets the ordered children ids of all items. - */ - itemOrderedChildrenIdsLookup: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>) => state.items.itemOrderedChildrenIdsLookup, - ), - /** - * Gets the meta-information of an item. - */ - itemMeta: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>, itemId: TreeViewItemId | null) => - (state.items.itemMetaLookup[itemId ?? TREE_VIEW_ROOT_PARENT_ID] ?? - null) as TreeViewItemMeta | null, - ), - /** - * Gets the ordered children ids of an item. - */ - itemOrderedChildrenIds: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>, itemId: TreeViewItemId | null) => - state.items.itemOrderedChildrenIdsLookup[itemId ?? TREE_VIEW_ROOT_PARENT_ID] ?? - EMPTY_CHILDREN, - ), - /** - * Gets the model of an item. - */ - itemModel: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>, itemId: TreeViewItemId) => - state.items.itemModelLookup[itemId], - ), - /** - * Checks whether an item is disabled. - */ - isItemDisabled: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>, itemId: TreeViewItemId) => - isItemDisabled(state.items.itemMetaLookup, itemId), - ), - /** - * Gets the index of an item in its parent's children. - */ - itemIndex: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>, itemId: TreeViewItemId) => { - const itemMeta = state.items.itemMetaLookup[itemId]; - if (itemMeta == null) { - return -1; - } - - const parentIndexes = - state.items.itemChildrenIndexesLookup[itemMeta.parentId ?? TREE_VIEW_ROOT_PARENT_ID]; - return parentIndexes[itemMeta.id]; - }, - ), - /** - * Gets the id of an item's parent. - */ - itemParentId: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>, itemId: TreeViewItemId) => - state.items.itemMetaLookup[itemId]?.parentId ?? null, - ), - /** - * Gets the depth of an item (items at the root level have a depth of 0). - */ - itemDepth: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>, itemId: TreeViewItemId) => - state.items.itemMetaLookup[itemId]?.depth ?? 0, - ), - /** - * Checks whether an item can be focused. - */ - canItemBeFocused: createSelector( - (state: TreeViewState<[UseTreeViewItemsSignature]>, itemId: TreeViewItemId) => - state.items.disabledItemsFocusable || !isItemDisabled(state.items.itemMetaLookup, itemId), - ), -}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx deleted file mode 100644 index 0ca8cc5be204b..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx +++ /dev/null @@ -1,257 +0,0 @@ -'use client'; -import * as React from 'react'; -import { useEventCallback } from '@base-ui-components/utils/useEventCallback'; -import { TreeViewPlugin } from '../../models'; -import { UseTreeViewItemsSignature, SetItemChildrenParameters } from './useTreeViewItems.types'; -import { TreeViewBaseItem, TreeViewItemId } from '../../../models'; -import { - BuildItemsLookupConfig, - buildItemsLookups, - buildItemsState, - TREE_VIEW_ROOT_PARENT_ID, -} from './useTreeViewItems.utils'; -import { TreeViewItemDepthContext } from '../../TreeViewItemDepthContext'; -import { itemsSelectors } from './useTreeViewItems.selectors'; -import { idSelectors } from '../../corePlugins/useTreeViewId'; -import { generateTreeItemIdAttribute } from '../../corePlugins/useTreeViewId/useTreeViewId.utils'; - -export const useTreeViewItems: TreeViewPlugin = ({ - instance, - params, - store, -}) => { - const itemsConfig: BuildItemsLookupConfig = React.useMemo( - () => ({ - isItemDisabled: params.isItemDisabled, - getItemLabel: params.getItemLabel, - getItemChildren: params.getItemChildren, - getItemId: params.getItemId, - }), - [params.isItemDisabled, params.getItemLabel, params.getItemChildren, params.getItemId], - ); - - const getItem = React.useCallback( - (itemId: string) => itemsSelectors.itemModel(store.state, itemId), - [store], - ); - const getParentId = React.useCallback( - (itemId: string) => { - const itemMeta = itemsSelectors.itemMeta(store.state, itemId); - return itemMeta?.parentId || null; - }, - [store], - ); - - const setIsItemDisabled = useEventCallback( - ({ itemId, shouldBeDisabled }: { itemId: string; shouldBeDisabled?: boolean }) => { - if (!store.state.items.itemMetaLookup[itemId]) { - return; - } - - const itemMetaLookup = { ...store.state.items.itemMetaLookup }; - itemMetaLookup[itemId] = { - ...itemMetaLookup[itemId], - disabled: shouldBeDisabled ?? !itemMetaLookup[itemId].disabled, - }; - - store.set('items', { ...store.state.items, itemMetaLookup }); - }, - ); - - const getItemTree = React.useCallback(() => { - const getItemFromItemId = (itemId: TreeViewItemId): TreeViewBaseItem => { - const item = itemsSelectors.itemModel(store.state, itemId); - const itemToMutate = { ...item }; - const newChildren = itemsSelectors.itemOrderedChildrenIds(store.state, itemId); - if (newChildren.length > 0) { - itemToMutate.children = newChildren.map(getItemFromItemId); - } else { - delete itemToMutate.children; - } - - return itemToMutate; - }; - - return itemsSelectors.itemOrderedChildrenIds(store.state, null).map(getItemFromItemId); - }, [store]); - - const getItemOrderedChildrenIds = React.useCallback( - (itemId: string | null) => itemsSelectors.itemOrderedChildrenIds(store.state, itemId), - [store], - ); - - const getItemDOMElement = (itemId: string) => { - const itemMeta = itemsSelectors.itemMeta(store.state, itemId); - if (itemMeta == null) { - return null; - } - - const idAttribute = generateTreeItemIdAttribute({ - treeId: idSelectors.treeId(store.state), - itemId, - id: itemMeta.idAttribute, - }); - return document.getElementById(idAttribute); - }; - - const areItemUpdatesPreventedRef = React.useRef(false); - const preventItemUpdates = React.useCallback(() => { - areItemUpdatesPreventedRef.current = true; - }, []); - - const areItemUpdatesPrevented = React.useCallback(() => areItemUpdatesPreventedRef.current, []); - - const setItemChildren = ({ - items, - parentId, - getChildrenCount, - }: SetItemChildrenParameters) => { - const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID; - const parentDepth = parentId == null ? -1 : itemsSelectors.itemDepth(store.state, parentId); - - const { metaLookup, modelLookup, orderedChildrenIds, childrenIndexes } = buildItemsLookups({ - config: itemsConfig, - items, - parentId, - depth: parentDepth + 1, - isItemExpandable: getChildrenCount ? (item) => getChildrenCount(item) > 0 : () => false, - otherItemsMetaLookup: itemsSelectors.itemMetaLookup(store.state), - }); - - const lookups = { - itemModelLookup: { ...store.state.items.itemModelLookup, ...modelLookup }, - itemMetaLookup: { ...store.state.items.itemMetaLookup, ...metaLookup }, - itemOrderedChildrenIdsLookup: { - ...store.state.items.itemOrderedChildrenIdsLookup, - [parentIdWithDefault]: orderedChildrenIds, - }, - itemChildrenIndexesLookup: { - ...store.state.items.itemChildrenIndexesLookup, - [parentIdWithDefault]: childrenIndexes, - }, - }; - - store.set('items', { ...store.state.items, ...lookups }); - }; - - const removeChildren = useEventCallback((parentId: string | null) => { - const newMetaMap = Object.keys(store.state.items.itemMetaLookup).reduce((acc, key) => { - const item = store.state.items.itemMetaLookup[key]; - if (item.parentId === parentId) { - return acc; - } - return { ...acc, [item.id]: item }; - }, {}); - - const newItemOrderedChildrenIdsLookup = { ...store.state.items.itemOrderedChildrenIdsLookup }; - const newItemChildrenIndexesLookup = { ...store.state.items.itemChildrenIndexesLookup }; - const cleanId = parentId ?? TREE_VIEW_ROOT_PARENT_ID; - delete newItemChildrenIndexesLookup[cleanId]; - delete newItemOrderedChildrenIdsLookup[cleanId]; - - store.set('items', { - ...store.state.items, - itemMetaLookup: newMetaMap, - itemOrderedChildrenIdsLookup: newItemOrderedChildrenIdsLookup, - itemChildrenIndexesLookup: newItemChildrenIndexesLookup, - }); - }); - - const addExpandableItems = useEventCallback((items: TreeViewItemId[]) => { - const newItemMetaLookup = { ...store.state.items.itemMetaLookup }; - for (const itemId of items) { - newItemMetaLookup[itemId] = { ...newItemMetaLookup[itemId], expandable: true }; - } - store.set('items', { - ...store.state.items, - itemMetaLookup: newItemMetaLookup, - }); - }); - - React.useEffect(() => { - if (instance.areItemUpdatesPrevented()) { - return; - } - - const newState = buildItemsState({ - disabledItemsFocusable: params.disabledItemsFocusable, - items: params.items, - config: itemsConfig, - }); - - store.set('items', { ...store.state.items, ...newState }); - }, [instance, store, params.items, params.disabledItemsFocusable, itemsConfig]); - - // Wrap `props.onItemClick` with `useEventCallback` to prevent unneeded context updates. - const handleItemClick = useEventCallback((event: React.MouseEvent, itemId: TreeViewItemId) => { - if (params.onItemClick) { - params.onItemClick(event, itemId); - } - }); - - return { - getRootProps: () => ({ - style: { - '--TreeView-itemChildrenIndentation': - typeof params.itemChildrenIndentation === 'number' - ? `${params.itemChildrenIndentation}px` - : params.itemChildrenIndentation, - } as React.CSSProperties, - }), - publicAPI: { - getItem, - getItemDOMElement, - getItemTree, - getItemOrderedChildrenIds, - setIsItemDisabled, - getParentId, - }, - instance: { - getItemDOMElement, - preventItemUpdates, - areItemUpdatesPrevented, - setItemChildren, - removeChildren, - addExpandableItems, - handleItemClick, - }, - }; -}; - -useTreeViewItems.getInitialState = (params) => ({ - items: buildItemsState({ - items: params.items, - disabledItemsFocusable: params.disabledItemsFocusable, - config: { - isItemDisabled: params.isItemDisabled, - getItemId: params.getItemId, - getItemLabel: params.getItemLabel, - getItemChildren: params.getItemChildren, - }, - }), -}); - -useTreeViewItems.applyDefaultValuesToParams = ({ params }) => ({ - ...params, - disabledItemsFocusable: params.disabledItemsFocusable ?? false, - itemChildrenIndentation: params.itemChildrenIndentation ?? '12px', -}); - -useTreeViewItems.wrapRoot = ({ children }) => { - return ( - - {children} - - ); -}; - -useTreeViewItems.params = { - disabledItemsFocusable: true, - items: true, - isItemDisabled: true, - getItemLabel: true, - getItemChildren: true, - getItemId: true, - onItemClick: true, - itemChildrenIndentation: true, -}; 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 deleted file mode 100644 index 208ecef22896d..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts +++ /dev/null @@ -1,195 +0,0 @@ -import * as React from 'react'; -import { DefaultizedProps } from '@mui/x-internals/types'; -import { TreeViewItemMeta, TreeViewPluginSignature } from '../../models'; -import { - TreeViewBaseItem, - TreeViewDefaultItemModelProperties, - TreeViewItemId, -} from '../../../models'; - -export type SetItemChildrenParameters = { - items: readonly R[]; - parentId: TreeViewItemId | null; - getChildrenCount: (item: R) => number; -}; - -export interface UseTreeViewItemsPublicAPI { - /** - * Get the item with the given id. - * When used in the Simple Tree View, it returns an object with the `id` and `label` properties. - * @param {TreeViewItemId} itemId The id of the item to retrieve. - * @returns {R} The item with the given id. - */ - getItem: (itemId: TreeViewItemId) => R; - /** - * Get the DOM element of the item with the given id. - * @param {TreeViewItemId} itemId The id of the item to get the DOM element of. - * @returns {HTMLElement | null} The DOM element of the item with the given id. - */ - getItemDOMElement: (itemId: TreeViewItemId) => HTMLElement | null; - /** - * Get the ids of a given item's children. - * Those ids are returned in the order they should be rendered. - * To get the root items, pass `null` as the `itemId`. - * @param {TreeViewItemId | null} itemId The id of the item to get the children of. - * @returns {TreeViewItemId[]} The ids of the item's children. - */ - getItemOrderedChildrenIds: (itemId: TreeViewItemId | null) => TreeViewItemId[]; - /** - * Get all the items in the same format as provided by `props.items`. - * @returns {TreeViewBaseItem[]} The items in the tree. - */ - getItemTree: () => TreeViewBaseItem[]; - /** - * Toggle the disabled state of the item with the given id. - * @param {object} parameters The params of the method. - * @param {TreeViewItemId } parameters.itemId The id of the item to get the children of. - * @param {boolean } parameters.shouldBeDisabled true if the item should be disabled. - */ - setIsItemDisabled: (parameters: { itemId: TreeViewItemId; shouldBeDisabled?: boolean }) => void; - /** * Get the id of the parent item. - * @param {TreeViewItemId} itemId The id of the item to whose parentId we want to retrieve. - * @returns {TreeViewItemId | null} The id of the parent item. - */ - getParentId: (itemId: TreeViewItemId) => TreeViewItemId | null; -} - -export interface UseTreeViewItemsInstance - extends Pick, 'getItemDOMElement'> { - /** - * Freeze any future update to the state based on the `items` prop. - * This is useful when `useTreeViewJSXItems` is used to avoid having conflicting sources of truth. - */ - preventItemUpdates: () => void; - /** - * Check if the updates to the state based on the `items` prop are prevented. - * This is useful when `useTreeViewJSXItems` is used to avoid having conflicting sources of truth. - * @returns {boolean} `true` if the updates to the state based on the `items` prop are prevented. - */ - areItemUpdatesPrevented: () => boolean; - /** - * Add an array of items to the tree. - * @param {SetItemChildrenParameters} args The items to add to the tree and information about their ancestors. - */ - setItemChildren: (args: SetItemChildrenParameters) => void; - /** - * Remove the children of an item. - * @param {TreeViewItemId | null} parentId The id of the item to remove the children of. - */ - removeChildren: (parentId: TreeViewItemId | null) => void; - /** - * Event handler to fire when the `content` slot of a given Tree Item is clicked. - * @param {React.MouseEvent} event The DOM event that triggered the change. - * @param {TreeViewItemId} itemId The id of the item being clicked. - */ - handleItemClick: (event: React.MouseEvent, itemId: TreeViewItemId) => void; - /** - * Mark a list of items as expandable. - * @param {TreeViewItemId[]} items The ids of the items to mark as expandable. - */ - addExpandableItems: (items: TreeViewItemId[]) => void; -} - -export interface UseTreeViewItemsParameters { - /** - * Whether the items should be focusable when disabled. - * @default false - */ - disabledItemsFocusable?: boolean; - items: readonly R[]; - /** - * Used to determine if a given item should be disabled. - * @template R - * @param {R} item The item to check. - * @returns {boolean} `true` if the item should be disabled. - */ - isItemDisabled?: (item: R) => boolean; - /** - * Used to determine the string label for a given item. - * - * @template R - * @param {R} item The item to check. - * @returns {string} The label of the item. - * @default (item) => item.label - */ - getItemLabel?: (item: R) => string; - /** - * Used to determine the children of a given item. - * - * @template R - * @param {R} item The item to check. - * @returns {R[]} The children of the item. - * @default (item) => item.children - */ - getItemChildren?: (item: R) => R[] | undefined; - /** - * Used to determine the id of a given item. - * - * @template R - * @param {R} item The item to check. - * @returns {TreeViewItemId} The id of the item. - * @default (item) => item.id - */ - getItemId?: (item: R) => TreeViewItemId; - /** - * Callback fired when the `content` slot of a given Tree Item is clicked. - * @param {React.MouseEvent} event The DOM event that triggered the change. - * @param {TreeViewItemId} itemId The id of the focused item. - */ - onItemClick?: (event: React.MouseEvent, itemId: TreeViewItemId) => void; - /** - * Horizontal indentation between an item and its children. - * Examples: 24, "24px", "2rem", "2em". - * @default 12px - */ - itemChildrenIndentation?: string | number; -} - -export type UseTreeViewItemsParametersWithDefaults = DefaultizedProps< - UseTreeViewItemsParameters, - 'disabledItemsFocusable' | 'itemChildrenIndentation' ->; - -export interface UseTreeViewItemsState { - items: { - /** - * Whether the items should be focusable when disabled. - * Always equal to `props.disabledItemsFocusable` (or `false` if not provided). - */ - disabledItemsFocusable: boolean; - /** - * Model of each item as provided by `props.items` or by imperative items updates. - * It is not updated when properties derived from the model are updated: - * - when the label of an item is updated, `itemMetaLookup` is updated, not `itemModelLookup`. - * - when the children of an item are updated, `itemOrderedChildrenIdsLookup` and `itemChildrenIndexesLookup` are updated, not `itemModelLookup`. - * This means that the `children`, `label` or `id` properties of an item model should never be used directly, always use the structured sub-states instead. - */ - itemModelLookup: { [itemId: string]: TreeViewBaseItem }; - /** - * Meta data of each item. - */ - itemMetaLookup: { [itemId: string]: TreeViewItemMeta }; - /** - * Ordered children ids of each item. - */ - itemOrderedChildrenIdsLookup: { [parentItemId: string]: TreeViewItemId[] }; - /** - * Index of each child in the ordered children ids of its parent. - */ - itemChildrenIndexesLookup: { [parentItemId: string]: { [itemId: string]: number } }; - /** - * When equal to 'flat', the tree is rendered as a flat list (children are rendered as siblings of their parents). - * When equal to 'nested', the tree is rendered with nested children (children are rendered inside the groupTransition slot of their children). - * Nested DOM structure is not compatible with collapse / expansion animations. - */ - domStructure: 'flat' | 'nested'; - }; -} - -export type UseTreeViewItemsSignature = TreeViewPluginSignature<{ - params: UseTreeViewItemsParameters; - paramsWithDefaults: UseTreeViewItemsParametersWithDefaults; - instance: UseTreeViewItemsInstance; - publicAPI: UseTreeViewItemsPublicAPI; - state: UseTreeViewItemsState; -}>; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/index.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/index.ts deleted file mode 100644 index 9a9e2273f6552..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { useTreeViewJSXItems } from './useTreeViewJSXItems'; -export type { - UseTreeViewJSXItemsSignature, - UseTreeViewJSXItemsParameters, - UseTreeViewItemsParametersWithDefaults, -} from './useTreeViewJSXItems.types'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx deleted file mode 100644 index 7dc5655c9a6a0..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx +++ /dev/null @@ -1,116 +0,0 @@ -'use client'; -import * as React from 'react'; -import { useEventCallback } from '@base-ui-components/utils/useEventCallback'; -import { TreeViewItemMeta, TreeViewPlugin } from '../../models'; -import { UseTreeViewJSXItemsSignature } from './useTreeViewJSXItems.types'; -import { TreeViewChildrenItemProvider } from '../../TreeViewProvider/TreeViewChildrenItemProvider'; -import { - buildSiblingIndexes, - TREE_VIEW_ROOT_PARENT_ID, -} from '../useTreeViewItems/useTreeViewItems.utils'; -import { TreeViewItemDepthContext } from '../../TreeViewItemDepthContext'; -import { useTreeViewJSXItemsItemPlugin } from './itemPlugin'; - -export const useTreeViewJSXItems: TreeViewPlugin = ({ - instance, - store, -}) => { - instance.preventItemUpdates(); - - const insertJSXItem = useEventCallback((item: TreeViewItemMeta) => { - if (store.state.items.itemMetaLookup[item.id] != null) { - throw new Error( - [ - 'MUI X: The Tree View component requires all items to have a unique `id` property.', - 'Alternatively, you can use the `getItemId` prop to specify a custom id for each item.', - `Two items were provided with the same id in the \`items\` prop: "${item.id}"`, - ].join('\n'), - ); - } - - store.set('items', { - ...store.state.items, - itemMetaLookup: { ...store.state.items.itemMetaLookup, [item.id]: item }, - // For Simple Tree View, we don't have a proper `item` object, so we create a very basic one. - itemModelLookup: { - ...store.state.items.itemModelLookup, - [item.id]: { id: item.id, label: item.label ?? '' }, - }, - }); - - return () => { - const newItemMetaLookup = { ...store.state.items.itemMetaLookup }; - const newItemModelLookup = { ...store.state.items.itemModelLookup }; - delete newItemMetaLookup[item.id]; - delete newItemModelLookup[item.id]; - - store.set('items', { - ...store.state.items, - itemMetaLookup: newItemMetaLookup, - itemModelLookup: newItemModelLookup, - }); - }; - }); - - const setJSXItemsOrderedChildrenIds = (parentId: string | null, orderedChildrenIds: string[]) => { - const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID; - - store.set('items', { - ...store.state.items, - itemOrderedChildrenIdsLookup: { - ...store.state.items.itemOrderedChildrenIdsLookup, - [parentIdWithDefault]: orderedChildrenIds, - }, - itemChildrenIndexesLookup: { - ...store.state.items.itemChildrenIndexesLookup, - [parentIdWithDefault]: buildSiblingIndexes(orderedChildrenIds), - }, - }); - }; - - const mapLabelFromJSX = useEventCallback((itemId: string, label: string) => { - instance.updateLabelMap((labelMap) => { - labelMap[itemId] = label; - return labelMap; - }); - - return () => { - instance.updateLabelMap((labelMap) => { - const newMap = { ...labelMap }; - delete newMap[itemId]; - return newMap; - }); - }; - }); - - return { - instance: { - insertJSXItem, - setJSXItemsOrderedChildrenIds, - mapLabelFromJSX, - }, - }; -}; - -useTreeViewJSXItems.itemPlugin = useTreeViewJSXItemsItemPlugin; - -useTreeViewJSXItems.wrapItem = ({ children, itemId, idAttribute }) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const depthContext = React.useContext(TreeViewItemDepthContext); - - return ( - - - {children} - - - ); -}; - -useTreeViewJSXItems.wrapRoot = ({ children }) => ( - - {children} - -); - -useTreeViewJSXItems.params = {}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.types.ts deleted file mode 100644 index 517ce2cf790d3..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.types.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { TreeViewItemMeta, TreeViewPluginSignature } from '../../models'; -import { UseTreeViewItemsSignature } from '../useTreeViewItems'; -import { UseTreeViewKeyboardNavigationSignature } from '../useTreeViewKeyboardNavigation'; -import { TreeViewItemId } from '../../../models'; - -export interface UseTreeViewItemsInstance { - /** - * Insert a new item in the state from a Tree Item component. - * @param {TreeViewItemMeta} item The meta-information of the item to insert. - * @returns {() => void} A function to remove the item from the state. - */ - insertJSXItem: (item: TreeViewItemMeta) => () => void; - /** - * Updates the `labelMap` to register the first character of the given item's label. - * This map is used to navigate the tree using type-ahead search. - * @param {TreeViewItemId} itemId The id of the item to map the label of. - * @param {string} label The item's label. - * @returns {() => void} A function to remove the item from the `labelMap`. - */ - mapLabelFromJSX: (itemId: TreeViewItemId, label: string) => () => void; - /** - * Store the ids of a given item's children in the state. - * Those ids must be passed in the order they should be rendered. - * @param {TreeViewItemId | null} parentId The id of the item to store the children of. - * @param {TreeViewItemId[]} orderedChildrenIds The ids of the item's children. - */ - setJSXItemsOrderedChildrenIds: ( - parentId: TreeViewItemId | null, - orderedChildrenIds: TreeViewItemId[], - ) => void; -} - -export interface UseTreeViewJSXItemsParameters {} - -export interface UseTreeViewItemsParametersWithDefaults {} - -export type UseTreeViewJSXItemsSignature = TreeViewPluginSignature<{ - params: UseTreeViewJSXItemsParameters; - paramsWithDefaults: UseTreeViewItemsParametersWithDefaults; - instance: UseTreeViewItemsInstance; - dependencies: [UseTreeViewItemsSignature, UseTreeViewKeyboardNavigationSignature]; -}>; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/index.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/index.ts deleted file mode 100644 index 6f9d5e3c4c15c..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useTreeViewKeyboardNavigation } from './useTreeViewKeyboardNavigation'; -export type { UseTreeViewKeyboardNavigationSignature } from './useTreeViewKeyboardNavigation.types'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts deleted file mode 100644 index d1d4e2ce35c17..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts +++ /dev/null @@ -1,351 +0,0 @@ -'use client'; -import * as React from 'react'; -import { useStore } from '@mui/x-internals/store'; -import { useRtl } from '@mui/system/RtlProvider'; -import { useTimeout } from '@base-ui-components/utils/useTimeout'; -import { useEventCallback } from '@base-ui-components/utils/useEventCallback'; -import { TreeViewCancellableEvent } from '../../../models'; -import { TreeViewItemMeta, TreeViewPlugin } from '../../models'; -import { - getFirstNavigableItem, - getLastNavigableItem, - getNextNavigableItem, - getPreviousNavigableItem, - isTargetInDescendants, -} from '../../utils/tree'; -import { - TreeViewLabelMap, - UseTreeViewKeyboardNavigationSignature, -} from './useTreeViewKeyboardNavigation.types'; -import { hasPlugin } from '../../utils/plugins'; -import { useTreeViewLabel } from '../useTreeViewLabel'; -import { itemsSelectors } from '../useTreeViewItems/useTreeViewItems.selectors'; -import { labelSelectors } from '../useTreeViewLabel/useTreeViewLabel.selectors'; -import { selectionSelectors } from '../useTreeViewSelection/useTreeViewSelection.selectors'; -import { expansionSelectors } from '../useTreeViewExpansion/useTreeViewExpansion.selectors'; - -function isPrintableKey(string: string) { - return !!string && string.length === 1 && !!string.match(/\S/); -} - -const TYPEAHEAD_TIMEOUT = 500; - -export const useTreeViewKeyboardNavigation: TreeViewPlugin< - UseTreeViewKeyboardNavigationSignature -> = ({ instance, store, params }) => { - const isRtl = useRtl(); - const labelMap = React.useRef({}); - - const typeaheadQueryRef = React.useRef(''); - const typeaheadTimeout = useTimeout(); - - const updateLabelMap = useEventCallback( - (callback: (labelMap: TreeViewLabelMap) => TreeViewLabelMap) => { - labelMap.current = callback(labelMap.current); - }, - ); - - const itemMetaLookup = useStore(store, itemsSelectors.itemMetaLookup); - React.useEffect(() => { - if (instance.areItemUpdatesPrevented()) { - return; - } - - const newLabelMap: { [itemId: string]: string } = {}; - - const processItem = (item: TreeViewItemMeta) => { - newLabelMap[item.id] = item.label!.toLowerCase(); - }; - - Object.values(itemMetaLookup).forEach(processItem); - labelMap.current = newLabelMap; - }, [itemMetaLookup, params.getItemId, instance]); - - const getNextItem = (itemIdToCheck: string) => { - const nextItemId = getNextNavigableItem(store.state, itemIdToCheck); - // We reached the end of the tree, check from the beginning - if (nextItemId === null) { - return getFirstNavigableItem(store.state); - } - - return nextItemId; - }; - - const getNextMatchingItemId = (itemId: string, query: string): string | null => { - let matchingItemId: string | null = null; - const checkedItems: Record = {}; - // If query length > 1, first check if current item matches - let currentItemId: string = query.length > 1 ? itemId : getNextItem(itemId); - // The "!checkedItems[currentItemId]" condition avoids an infinite loop when there is no matching item. - while (matchingItemId == null && !checkedItems[currentItemId]) { - const itemLabel = labelMap.current[currentItemId]; - - if (itemLabel?.startsWith(query)) { - matchingItemId = currentItemId; - } else { - checkedItems[currentItemId] = true; - currentItemId = getNextItem(currentItemId); - } - } - return matchingItemId; - }; - - const getFirstMatchingItem = (itemId: string, newKey: string): string | null => { - const cleanNewKey = newKey.toLowerCase(); - - // Try matching with accumulated query + new key - const concatenatedQuery = `${typeaheadQueryRef.current}${cleanNewKey}`; - - // check if the entire typed query matches an item - const concatenatedQueryMatchingItemId = getNextMatchingItemId(itemId, concatenatedQuery); - if (concatenatedQueryMatchingItemId != null) { - typeaheadQueryRef.current = concatenatedQuery; - return concatenatedQueryMatchingItemId; - } - - const newKeyMatchingItemId = getNextMatchingItemId(itemId, cleanNewKey); - if (newKeyMatchingItemId != null) { - typeaheadQueryRef.current = cleanNewKey; - return newKeyMatchingItemId; - } - - typeaheadQueryRef.current = ''; - return null; - }; - - const canToggleItemSelection = (itemId: string) => - selectionSelectors.enabled(store.state) && !itemsSelectors.isItemDisabled(store.state, itemId); - - const canToggleItemExpansion = (itemId: string) => { - return ( - !itemsSelectors.isItemDisabled(store.state, itemId) && - expansionSelectors.isItemExpandable(store.state, itemId) - ); - }; - - // ARIA specification: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/#keyboardinteraction - const handleItemKeyDown = async ( - event: React.KeyboardEvent & TreeViewCancellableEvent, - itemId: string, - ) => { - if (event.defaultMuiPrevented) { - return; - } - - if ( - event.altKey || - isTargetInDescendants(event.target as HTMLElement, event.currentTarget as HTMLElement) - ) { - return; - } - - const ctrlPressed = event.ctrlKey || event.metaKey; - const key = event.key; - const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(store.state); - - // eslint-disable-next-line default-case - switch (true) { - // Select the item when pressing "Space" - case key === ' ' && canToggleItemSelection(itemId): { - event.preventDefault(); - if (isMultiSelectEnabled && event.shiftKey) { - instance.expandSelectionRange(event, itemId); - } else { - instance.setItemSelection({ - event, - itemId, - keepExistingSelection: isMultiSelectEnabled, - shouldBeSelected: undefined, - }); - } - break; - } - - // If the focused item has children, we expand it. - // If the focused item has no children, we select it. - case key === 'Enter': { - if ( - hasPlugin(instance, useTreeViewLabel) && - labelSelectors.isItemEditable(store.state, itemId) && - !labelSelectors.isItemBeingEdited(store.state, itemId) - ) { - instance.setEditedItem(itemId); - } else if (canToggleItemExpansion(itemId)) { - instance.setItemExpansion({ event, itemId }); - event.preventDefault(); - } else if (canToggleItemSelection(itemId)) { - if (isMultiSelectEnabled) { - event.preventDefault(); - instance.setItemSelection({ event, itemId, keepExistingSelection: true }); - } else if (!selectionSelectors.isItemSelected(store.state, itemId)) { - instance.setItemSelection({ event, itemId }); - event.preventDefault(); - } - } - - break; - } - - // Focus the next focusable item - case key === 'ArrowDown': { - const nextItem = getNextNavigableItem(store.state, itemId); - if (nextItem) { - event.preventDefault(); - instance.focusItem(event, nextItem); - - // Multi select behavior when pressing Shift + ArrowDown - // Toggles the selection state of the next item - if (isMultiSelectEnabled && event.shiftKey && canToggleItemSelection(nextItem)) { - instance.selectItemFromArrowNavigation(event, itemId, nextItem); - } - } - - break; - } - - // Focuses the previous focusable item - case key === 'ArrowUp': { - const previousItem = getPreviousNavigableItem(store.state, itemId); - if (previousItem) { - event.preventDefault(); - instance.focusItem(event, previousItem); - - // Multi select behavior when pressing Shift + ArrowUp - // Toggles the selection state of the previous item - if (isMultiSelectEnabled && event.shiftKey && canToggleItemSelection(previousItem)) { - instance.selectItemFromArrowNavigation(event, itemId, previousItem); - } - } - - break; - } - - // If the focused item is expanded, we move the focus to its first child - // If the focused item is collapsed and has children, we expand it - case (key === 'ArrowRight' && !isRtl) || (key === 'ArrowLeft' && isRtl): { - if (ctrlPressed) { - return; - } - if (expansionSelectors.isItemExpanded(store.state, itemId)) { - const nextItemId = getNextNavigableItem(store.state, itemId); - if (nextItemId) { - instance.focusItem(event, nextItemId); - event.preventDefault(); - } - } else if (canToggleItemExpansion(itemId)) { - instance.setItemExpansion({ event, itemId }); - event.preventDefault(); - } - - break; - } - - // If the focused item is expanded, we collapse it - // If the focused item is collapsed and has a parent, we move the focus to this parent - case (key === 'ArrowLeft' && !isRtl) || (key === 'ArrowRight' && isRtl): { - if (ctrlPressed) { - return; - } - if ( - canToggleItemExpansion(itemId) && - expansionSelectors.isItemExpanded(store.state, itemId) - ) { - instance.setItemExpansion({ event, itemId }); - event.preventDefault(); - } else { - const parent = itemsSelectors.itemParentId(store.state, itemId); - if (parent) { - instance.focusItem(event, parent); - event.preventDefault(); - } - } - - break; - } - - // Focuses the first item in the tree - case key === 'Home': { - // Multi select behavior when pressing Ctrl + Shift + Home - // Selects the focused item and all items up to the first item. - if ( - canToggleItemSelection(itemId) && - isMultiSelectEnabled && - ctrlPressed && - event.shiftKey - ) { - instance.selectRangeFromStartToItem(event, itemId); - } else { - instance.focusItem(event, getFirstNavigableItem(store.state)); - } - - event.preventDefault(); - break; - } - - // Focuses the last item in the tree - case key === 'End': { - // Multi select behavior when pressing Ctrl + Shirt + End - // Selects the focused item and all the items down to the last item. - if ( - canToggleItemSelection(itemId) && - isMultiSelectEnabled && - ctrlPressed && - event.shiftKey - ) { - instance.selectRangeFromItemToEnd(event, itemId); - } else { - instance.focusItem(event, getLastNavigableItem(store.state)); - } - - event.preventDefault(); - break; - } - - // Expand all siblings that are at the same level as the focused item - case key === '*': { - instance.expandAllSiblings(event, itemId); - event.preventDefault(); - break; - } - - // Multi select behavior when pressing Ctrl + a - // Selects all the items - case String.fromCharCode(event.keyCode) === 'A' && - ctrlPressed && - isMultiSelectEnabled && - selectionSelectors.enabled(store.state): { - instance.selectAllNavigableItems(event); - event.preventDefault(); - break; - } - - // Type-ahead - case !ctrlPressed && !event.shiftKey && isPrintableKey(key): { - typeaheadTimeout.clear(); - - const matchingItem = getFirstMatchingItem(itemId, key); - - if (matchingItem != null) { - instance.focusItem(event, matchingItem); - event.preventDefault(); - } else { - typeaheadQueryRef.current = ''; - } - - typeaheadTimeout.start(TYPEAHEAD_TIMEOUT, () => { - typeaheadQueryRef.current = ''; - }); - break; - } - } - }; - - return { - instance: { - updateLabelMap, - handleItemKeyDown, - }, - }; -}; - -useTreeViewKeyboardNavigation.params = {}; 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 deleted file mode 100644 index 4c6ea645badaa..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from 'react'; -import { TreeViewPluginSignature } from '../../models'; -import { UseTreeViewItemsSignature } from '../useTreeViewItems'; -import { UseTreeViewSelectionSignature } from '../useTreeViewSelection'; -import { UseTreeViewFocusSignature } from '../useTreeViewFocus'; -import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion'; -import { UseTreeViewLabelSignature } from '../useTreeViewLabel'; -import { TreeViewItemId, TreeViewCancellableEvent } from '../../../models'; - -export interface UseTreeViewKeyboardNavigationInstance { - /** - * Updates the `labelMap` to add/remove the first character of some item's labels. - * This map is used to navigate the tree using type-ahead search. - * This method is only used by the `useTreeViewJSXItems` plugin, otherwise the updates are handled internally. - * @param {(map: TreeViewLabelMap) => TreeViewLabelMap} updater The function to update the map. - */ - updateLabelMap: (updater: (map: TreeViewLabelMap) => TreeViewLabelMap) => void; - /** - * Callback fired when a key is pressed on an item. - * Handles all the keyboard navigation logic. - * @param {React.KeyboardEvent & TreeViewCancellableEvent} event The keyboard event that triggered the callback. - * @param {TreeViewItemId} itemId The id of the item that the event was triggered on. - */ - handleItemKeyDown: ( - event: React.KeyboardEvent & TreeViewCancellableEvent, - itemId: TreeViewItemId, - ) => void; -} - -export type UseTreeViewKeyboardNavigationSignature = TreeViewPluginSignature<{ - instance: UseTreeViewKeyboardNavigationInstance; - dependencies: [ - UseTreeViewItemsSignature, - UseTreeViewSelectionSignature, - UseTreeViewFocusSignature, - UseTreeViewExpansionSignature, - ]; - optionalDependencies: [UseTreeViewLabelSignature]; -}>; - -export type TreeViewLabelMap = { [itemId: string]: string }; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/index.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/index.ts deleted file mode 100644 index d088d291f6f6b..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { useTreeViewLabel } from './useTreeViewLabel'; -export type { - UseTreeViewLabelSignature, - UseTreeViewLabelParameters, -} from './useTreeViewLabel.types'; -export { labelSelectors } from './useTreeViewLabel.selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.ts deleted file mode 100644 index b929a82b36273..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; -import { TreeViewPlugin } from '../../models'; -import { TreeViewItemId } from '../../../models'; -import { UseTreeViewLabelSignature } from './useTreeViewLabel.types'; -import { useTreeViewLabelItemPlugin } from './itemPlugin'; -import { labelSelectors } from './useTreeViewLabel.selectors'; - -export const useTreeViewLabel: TreeViewPlugin = ({ store, params }) => { - const setEditedItem = (editedItemId: TreeViewItemId | null) => { - if (editedItemId !== null) { - const isEditable = labelSelectors.isItemEditable(store.state, editedItemId); - - if (!isEditable) { - return; - } - } - - store.set('label', { ...store.state.label, editedItemId }); - }; - - const updateItemLabel = (itemId: TreeViewItemId, label: string) => { - if (!label) { - throw new Error( - [ - 'MUI X: The Tree View component requires all items to have a `label` property.', - 'The label of an item cannot be empty.', - itemId, - ].join('\n'), - ); - } - - const item = store.state.items.itemMetaLookup[itemId]; - if (item.label === label) { - return; - } - - store.set('items', { - ...store.state.items, - itemMetaLookup: { ...store.state.items.itemMetaLookup, [itemId]: { ...item, label } }, - }); - - if (params.onItemLabelChange) { - params.onItemLabelChange(itemId, label); - } - }; - - useIsoLayoutEffect(() => { - store.set('label', { ...store.state.label, isItemEditable: params.isItemEditable }); - }, [store, params.isItemEditable]); - - return { - instance: { - setEditedItem, - updateItemLabel, - }, - publicAPI: { - setEditedItem, - updateItemLabel, - }, - }; -}; - -useTreeViewLabel.itemPlugin = useTreeViewLabelItemPlugin; - -useTreeViewLabel.applyDefaultValuesToParams = ({ params }) => ({ - ...params, - isItemEditable: params.isItemEditable ?? false, -}); - -useTreeViewLabel.getInitialState = (params) => ({ - label: { - isItemEditable: params.isItemEditable, - editedItemId: null, - }, -}); - -useTreeViewLabel.params = { - onItemLabelChange: true, - isItemEditable: true, -}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.types.ts deleted file mode 100644 index 43c7aaae4d63c..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.types.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { DefaultizedProps } from '@mui/x-internals/types'; -import { TreeViewPluginSignature } from '../../models'; -import { TreeViewItemId } from '../../../models'; -import { UseTreeViewItemsSignature } from '../useTreeViewItems'; - -export interface UseTreeViewLabelPublicAPI { - /** - * Used to update the label of an item. - * @param {TreeViewItemId} itemId The id of the item to update the label of. - * @param {string} newLabel The new label of the item. - */ - updateItemLabel: (itemId: TreeViewItemId, newLabel: string) => void; - /** - * Set which item is currently being edited. - * You can pass `null` to exit editing mode. - * @param {TreeViewItemId | null} itemId The id of the item to edit, or `null` to exit editing mode. - */ - setEditedItem: (itemId: TreeViewItemId | null) => void; -} - -export interface UseTreeViewLabelInstance extends UseTreeViewLabelPublicAPI {} - -export interface UseTreeViewLabelParameters { - /** - * Callback fired when the label of an item changes. - * @param {TreeViewItemId} itemId The id of the item that was edited. - * @param {string} newLabel The new label of the items. - */ - onItemLabelChange?: (itemId: TreeViewItemId, newLabel: string) => void; - /** - * Determine if a given item can be edited. - * @template R - * @param {R} item The item to check. - * @returns {boolean} `true` if the item can be edited. - * @default () => false - */ - isItemEditable?: boolean | ((item: R) => boolean); -} - -export type UseTreeViewLabelParametersWithDefaults = DefaultizedProps< - UseTreeViewLabelParameters, - 'isItemEditable' ->; - -export interface UseTreeViewLabelState { - label: { - isItemEditable: ((item: any) => boolean) | boolean; - editedItemId: TreeViewItemId | null; - }; -} - -export type UseTreeViewLabelSignature = TreeViewPluginSignature<{ - params: UseTreeViewLabelParameters; - paramsWithDefaults: UseTreeViewLabelParametersWithDefaults; - publicAPI: UseTreeViewLabelPublicAPI; - instance: UseTreeViewLabelInstance; - state: UseTreeViewLabelState; - dependencies: [UseTreeViewItemsSignature]; -}>; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/index.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/index.ts deleted file mode 100644 index 106d53625e09d..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type { - UseTreeViewLazyLoadingSignature, - UseTreeViewLazyLoadingParameters, - UseTreeViewLazyLoadingInstance, - DataSource, -} from './useTreeViewLazyLoading.types'; -export { lazyLoadingSelectors } from './useTreeViewLazyLoading.selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.selectors.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.selectors.ts deleted file mode 100644 index 434fecc7d1e01..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.selectors.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createSelector } from '@mui/x-internals/store'; -import { TreeViewItemId } from '../../../models'; -import { UseTreeViewLazyLoadingSignature } from './useTreeViewLazyLoading.types'; -import { TreeViewState } from '../../models'; -import { TREE_VIEW_ROOT_PARENT_ID } from '../useTreeViewItems'; - -export const lazyLoadingSelectors = { - /** - * Gets the data source used to lazy load items. - */ - dataSource: createSelector( - (state: TreeViewState<[], [UseTreeViewLazyLoadingSignature]>) => state.lazyLoading?.dataSource, - ), - /** - * Checks whether an item is loading. - */ - isItemLoading: createSelector( - (state: TreeViewState<[], [UseTreeViewLazyLoadingSignature]>, itemId: TreeViewItemId | null) => - state.lazyLoading?.dataSource.loading[itemId ?? TREE_VIEW_ROOT_PARENT_ID] ?? false, - ), - /** - * Checks whether an item has errors. - */ - itemHasError: createSelector( - (state: TreeViewState<[], [UseTreeViewLazyLoadingSignature]>, itemId: TreeViewItemId | null) => - !!state.lazyLoading?.dataSource.errors[itemId ?? TREE_VIEW_ROOT_PARENT_ID], - ), - /** - * Get an item error. - */ - itemError: createSelector( - (state: TreeViewState<[], [UseTreeViewLazyLoadingSignature]>, itemId: TreeViewItemId | null) => - state.lazyLoading?.dataSource.errors[itemId ?? TREE_VIEW_ROOT_PARENT_ID], - ), -}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.types.ts deleted file mode 100644 index 887b0de86b3ac..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.types.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { DefaultizedProps } from '@mui/x-internals/types'; -import { TreeViewPluginSignature } from '../../models'; -import { DataSourceCache } from '../../../utils'; -import { TreeViewItemId } from '../../../models'; -import { UseTreeViewItemsSignature } from '../useTreeViewItems'; -import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion'; -import { UseTreeViewSelectionSignature } from '../useTreeViewSelection'; - -export type DataSource = { - /** - * Used to determine the number of children the item has. - * Only relevant for lazy-loaded trees. - * - * @template R - * @param {R} item The item to check. - * @returns {number} The number of children. - */ - getChildrenCount: (item: R) => number; - /** - * Method used for fetching the items. - * Only relevant for lazy-loaded tree views. - * - * @template R - * @param {TreeViewItemId} parentId The id of the item the children belong to. - * @returns { Promise} The children of the item. - */ - getTreeItems: (parentId?: TreeViewItemId) => Promise; -}; - -export interface UseTreeViewLazyLoadingPublicAPI { - /** - * Method used for updating an item's children. - * Only relevant for lazy-loaded tree views. - * - * @param {TreeViewItemId} itemId The The id of the item to update the children of. - * @returns {Promise} The promise resolved when the items are fetched. - */ - updateItemChildren: (itemId: TreeViewItemId) => Promise; -} - -export interface UseTreeViewLazyLoadingInstance extends UseTreeViewLazyLoadingPublicAPI { - /** - * Method used for fetching multiple items concurrently. - * Only relevant for lazy-loaded tree views. - * - * @param {TreeViewItemId[]} parentIds The ids of the items to fetch the children of. - * @returns {Promise} The promise resolved when the items are fetched. - */ - fetchItems: (parentIds: TreeViewItemId[]) => Promise; - /** - * Method used for fetching an item's children. - * Only relevant for lazy-loaded tree views. - * - * @param {object} parameters The parameters of the method. - * @param {TreeViewItemId} parameters.itemId The The id of the item to fetch the children of. - * @param {boolean} [parameters.forceRefresh] Whether to force a refresh of the children when the cache already contains some data. - * @returns {Promise} The promise resolved when the items are fetched. - */ - fetchItemChildren: (parameters: { - itemId: TreeViewItemId | null; - forceRefresh?: boolean; - }) => Promise; - /** - * Set the loading state of an item. - * @param {TreeViewItemId} itemId The id of the item to set the loading state of. If `null` is passed, it will set the loading state of the root. - * @param {boolean} isLoading True if the item is loading. - */ - setDataSourceLoading: (itemId: TreeViewItemId | null, isLoading: boolean) => void; - /** - * Set the error state of an item. - * @param {TreeViewItemId} itemId The id of the item to set the error state of. If `null` is passed, it will set the error state of the root. - * @param {Error | null} error The error caught on the item. - */ - setDataSourceError: (itemId: TreeViewItemId | null, error: Error | null) => void; -} - -export interface UseTreeViewLazyLoadingParameters { - /** - * The data source object. - */ - dataSource?: DataSource; - /** - * The data source cache object. - */ - dataSourceCache?: DataSourceCache; -} -export type UseTreeViewLazyLoadingParametersWithDefaults = DefaultizedProps< - UseTreeViewLazyLoadingParameters, - 'dataSource' ->; - -export interface UseTreeViewLazyLoadingState { - lazyLoading: { - enabled: boolean; - dataSource: { - loading: Record; - errors: Record; - }; - }; -} - -export type UseTreeViewLazyLoadingSignature = TreeViewPluginSignature<{ - params: UseTreeViewLazyLoadingParameters; - paramsWithDefaults: UseTreeViewLazyLoadingParametersWithDefaults; - publicAPI: UseTreeViewLazyLoadingPublicAPI; - instance: UseTreeViewLazyLoadingInstance; - state: UseTreeViewLazyLoadingState; - dependencies: [ - UseTreeViewItemsSignature, - UseTreeViewExpansionSignature, - UseTreeViewSelectionSignature, - ]; -}>; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/index.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/index.ts deleted file mode 100644 index ed0a52678a3d4..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { useTreeViewSelection } from './useTreeViewSelection'; -export type { - UseTreeViewSelectionSignature, - UseTreeViewSelectionParameters, - UseTreeViewSelectionParametersWithDefaults, -} from './useTreeViewSelection.types'; -export { selectionSelectors } from './useTreeViewSelection.selectors'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts deleted file mode 100644 index da0d8a2d7b484..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts +++ /dev/null @@ -1,306 +0,0 @@ -import * as React from 'react'; -import { useAssertModelConsistency } from '@mui/x-internals/useAssertModelConsistency'; -import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; -import { TreeViewPlugin } from '../../models'; -import { TreeViewItemId } from '../../../models'; -import { - findOrderInTremauxTree, - getAllNavigableItems, - getFirstNavigableItem, - getLastNavigableItem, - getNonDisabledItemsInRange, -} from '../../utils/tree'; -import { - TreeViewSelectionValue, - UseTreeViewSelectionInstance, - UseTreeViewSelectionParameters, - UseTreeViewSelectionSignature, -} from './useTreeViewSelection.types'; -import { - propagateSelection, - getAddedAndRemovedItems, - getLookupFromArray, -} from './useTreeViewSelection.utils'; -import { selectionSelectors } from './useTreeViewSelection.selectors'; -import { useTreeViewSelectionItemPlugin } from './itemPlugin'; - -export const useTreeViewSelection: TreeViewPlugin = ({ - store, - params, -}) => { - useAssertModelConsistency({ - componentName: 'Tree View', - propName: 'selectedItems', - controlled: params.selectedItems, - defaultValue: params.defaultSelectedItems, - }); - - const lastSelectedItem = React.useRef(null); - const lastSelectedRange = React.useRef<{ [itemId: string]: boolean }>({}); - - const setSelectedItems = ( - event: React.SyntheticEvent | null, - newModel: typeof params.defaultSelectedItems, - additionalItemsToPropagate?: TreeViewItemId[], - ) => { - const oldModel = selectionSelectors.selectedItemsRaw(store.state); - let cleanModel: typeof newModel; - const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(store.state); - - if ( - isMultiSelectEnabled && - (params.selectionPropagation.descendants || params.selectionPropagation.parents) - ) { - cleanModel = propagateSelection({ - store, - selectionPropagation: params.selectionPropagation, - newModel: newModel as string[], - oldModel: oldModel as string[], - additionalItemsToPropagate, - }); - } else { - cleanModel = newModel; - } - - if (params.onItemSelectionToggle) { - if (isMultiSelectEnabled) { - const changes = getAddedAndRemovedItems({ - store, - newModel: cleanModel as string[], - oldModel: oldModel as string[], - }); - - if (params.onItemSelectionToggle) { - changes.added.forEach((itemId) => { - params.onItemSelectionToggle!(event, itemId, true); - }); - - changes.removed.forEach((itemId) => { - params.onItemSelectionToggle!(event, itemId, false); - }); - } - } else if (params.onItemSelectionToggle && cleanModel !== oldModel) { - if (oldModel != null) { - params.onItemSelectionToggle(event, oldModel as string, false); - } - if (cleanModel != null) { - params.onItemSelectionToggle(event, cleanModel as string, true); - } - } - } - - if (params.selectedItems === undefined) { - store.set('selection', { ...store.state.selection, selectedItems: cleanModel }); - } - - params.onSelectedItemsChange?.(event, cleanModel); - }; - - const setItemSelection: UseTreeViewSelectionInstance['setItemSelection'] = ({ - itemId, - event = null, - keepExistingSelection = false, - shouldBeSelected, - }) => { - if (!selectionSelectors.enabled(store.state)) { - return; - } - - let newSelected: TreeViewSelectionValue; - const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(store.state); - if (keepExistingSelection) { - const oldSelected = selectionSelectors.selectedItems(store.state); - const isSelectedBefore = selectionSelectors.isItemSelected(store.state, itemId); - if (isSelectedBefore && (shouldBeSelected === false || shouldBeSelected == null)) { - newSelected = oldSelected.filter((id) => id !== itemId); - } else if (!isSelectedBefore && (shouldBeSelected === true || shouldBeSelected == null)) { - newSelected = [itemId].concat(oldSelected); - } else { - newSelected = oldSelected; - } - } else { - // eslint-disable-next-line no-lonely-if - if ( - shouldBeSelected === false || - (shouldBeSelected == null && selectionSelectors.isItemSelected(store.state, itemId)) - ) { - newSelected = isMultiSelectEnabled ? [] : null; - } else { - newSelected = isMultiSelectEnabled ? [itemId] : itemId; - } - } - - setSelectedItems( - event, - newSelected, - // If shouldBeSelected === selectionSelectors.isItemSelected(store, itemId), we still want to propagate the select. - // This is useful when the element is in an indeterminate state. - [itemId], - ); - lastSelectedItem.current = itemId; - lastSelectedRange.current = {}; - }; - - const selectRange = (event: React.SyntheticEvent, [start, end]: [string, string]) => { - const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(store.state); - if (!isMultiSelectEnabled) { - return; - } - - let newSelectedItems = selectionSelectors.selectedItems(store.state).slice(); - - // If the last selection was a range selection, - // remove the items that were part of the last range from the model - if (Object.keys(lastSelectedRange.current).length > 0) { - newSelectedItems = newSelectedItems.filter((id) => !lastSelectedRange.current[id]); - } - - // Add to the model the items that are part of the new range and not already part of the model. - const selectedItemsLookup = getLookupFromArray(newSelectedItems); - const range = getNonDisabledItemsInRange(store.state, start, end); - const itemsToAddToModel = range.filter((id) => !selectedItemsLookup[id]); - newSelectedItems = newSelectedItems.concat(itemsToAddToModel); - - setSelectedItems(event, newSelectedItems); - lastSelectedRange.current = getLookupFromArray(range); - }; - - const expandSelectionRange = (event: React.SyntheticEvent, itemId: string) => { - if (lastSelectedItem.current != null) { - const [start, end] = findOrderInTremauxTree(store.state, itemId, lastSelectedItem.current); - selectRange(event, [start, end]); - } - }; - - const selectRangeFromStartToItem = (event: React.SyntheticEvent, itemId: string) => { - selectRange(event, [getFirstNavigableItem(store.state), itemId]); - }; - - const selectRangeFromItemToEnd = (event: React.SyntheticEvent, itemId: string) => { - selectRange(event, [itemId, getLastNavigableItem(store.state)]); - }; - - const selectAllNavigableItems = (event: React.SyntheticEvent) => { - const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(store.state); - if (!isMultiSelectEnabled) { - return; - } - - const navigableItems = getAllNavigableItems(store.state); - setSelectedItems(event, navigableItems); - - lastSelectedRange.current = getLookupFromArray(navigableItems); - }; - - const selectItemFromArrowNavigation = ( - event: React.SyntheticEvent, - currentItem: string, - nextItem: string, - ) => { - const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(store.state); - if (!isMultiSelectEnabled) { - return; - } - - let newSelectedItems = selectionSelectors.selectedItems(store.state).slice(); - - if (Object.keys(lastSelectedRange.current).length === 0) { - newSelectedItems.push(nextItem); - lastSelectedRange.current = { [currentItem]: true, [nextItem]: true }; - } else { - if (!lastSelectedRange.current[currentItem]) { - lastSelectedRange.current = {}; - } - - if (lastSelectedRange.current[nextItem]) { - newSelectedItems = newSelectedItems.filter((id) => id !== currentItem); - delete lastSelectedRange.current[currentItem]; - } else { - newSelectedItems.push(nextItem); - lastSelectedRange.current[nextItem] = true; - } - } - - setSelectedItems(event, newSelectedItems); - }; - - useIsoLayoutEffect(() => { - store.set('selection', { - selectedItems: - params.selectedItems === undefined - ? store.state.selection.selectedItems - : params.selectedItems, - isEnabled: !params.disableSelection, - isMultiSelectEnabled: params.multiSelect, - isCheckboxSelectionEnabled: params.checkboxSelection, - selectionPropagation: { - descendants: params.selectionPropagation.descendants, - parents: params.selectionPropagation.parents, - }, - }); - }, [ - store, - params.selectedItems, - params.multiSelect, - params.checkboxSelection, - params.disableSelection, - params.selectionPropagation.descendants, - params.selectionPropagation.parents, - ]); - - return { - getRootProps: () => ({ - 'aria-multiselectable': params.multiSelect, - }), - publicAPI: { - setItemSelection, - }, - instance: { - setItemSelection, - selectAllNavigableItems, - expandSelectionRange, - selectRangeFromStartToItem, - selectRangeFromItemToEnd, - selectItemFromArrowNavigation, - }, - }; -}; - -useTreeViewSelection.itemPlugin = useTreeViewSelectionItemPlugin; - -const DEFAULT_SELECTED_ITEMS: string[] = []; - -const EMPTY_SELECTION_PROPAGATION: UseTreeViewSelectionParameters['selectionPropagation'] = - {}; - -useTreeViewSelection.applyDefaultValuesToParams = ({ params }) => ({ - ...params, - disableSelection: params.disableSelection ?? false, - multiSelect: params.multiSelect ?? false, - checkboxSelection: params.checkboxSelection ?? false, - defaultSelectedItems: - params.defaultSelectedItems ?? (params.multiSelect ? DEFAULT_SELECTED_ITEMS : null), - selectionPropagation: params.selectionPropagation ?? EMPTY_SELECTION_PROPAGATION, -}); - -useTreeViewSelection.getInitialState = (params) => ({ - selection: { - selectedItems: - params.selectedItems === undefined ? params.defaultSelectedItems : params.selectedItems, - isEnabled: !params.disableSelection, - isMultiSelectEnabled: params.multiSelect, - isCheckboxSelectionEnabled: params.checkboxSelection, - selectionPropagation: params.selectionPropagation, - }, -}); - -useTreeViewSelection.params = { - disableSelection: true, - multiSelect: true, - checkboxSelection: true, - defaultSelectedItems: true, - selectedItems: true, - onSelectedItemsChange: true, - onItemSelectionToggle: true, - selectionPropagation: true, -}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts deleted file mode 100644 index 912f4f304e0f8..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts +++ /dev/null @@ -1,163 +0,0 @@ -import * as React from 'react'; -import type { DefaultizedProps } from '@mui/x-internals/types'; -import type { TreeViewPluginSignature } from '../../models'; -import { UseTreeViewItemsSignature } from '../useTreeViewItems'; -import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion'; -import { TreeViewSelectionPropagation, TreeViewItemId } from '../../../models'; - -export interface UseTreeViewSelectionPublicAPI { - /** - * Select or deselect an item. - * @param {object} parameters The parameters of the method. - * @param {TreeViewItemId} parameters.itemId The id of the item to select or deselect. - * @param {React.SyntheticEvent} parameters.event The DOM event that triggered the change. - * @param {boolean} parameters.keepExistingSelection If `true`, the other already selected items will remain selected, otherwise, they will be deselected. This parameter is only relevant when `multiSelect` is `true` - * @param {boolean | undefined} parameters.shouldBeSelected If `true` the item will be selected. If `false` the item will be deselected. If not defined, the item's selection status will be toggled. - */ - setItemSelection: (parameters: { - itemId: TreeViewItemId; - event?: React.SyntheticEvent; - shouldBeSelected?: boolean; - keepExistingSelection?: boolean; - }) => void; -} - -export interface UseTreeViewSelectionInstance extends UseTreeViewSelectionPublicAPI { - /** - * Select all the navigable items in the tree. - * @param {React.SyntheticEvent} event The DOM event that triggered the change. - */ - selectAllNavigableItems: (event: React.SyntheticEvent) => void; - /** - * Expand the current selection range up to the given item. - * @param {React.SyntheticEvent} event The DOM event that triggered the change. - * @param {TreeViewItemId} itemId The id of the item to expand the selection to. - */ - expandSelectionRange: (event: React.SyntheticEvent, itemId: TreeViewItemId) => void; - /** - * Expand the current selection range from the first navigable item to the given item. - * @param {React.SyntheticEvent} event The DOM event that triggered the change. - * @param {TreeViewItemId} itemId The id of the item up to which the selection range should be expanded. - */ - selectRangeFromStartToItem: (event: React.SyntheticEvent, itemId: TreeViewItemId) => void; - /** - * Expand the current selection range from the given item to the last navigable item. - * @param {React.SyntheticEvent} event The DOM event that triggered the change. - * @param {TreeViewItemId} itemId The id of the item from which the selection range should be expanded. - */ - selectRangeFromItemToEnd: (event: React.SyntheticEvent, itemId: TreeViewItemId) => void; - /** - * Update the selection when navigating with ArrowUp / ArrowDown keys. - * @param {React.SyntheticEvent} event The DOM event that triggered the change. - * @param {TreeViewItemId} currentItemId The id of the active item before the keyboard navigation. - * @param {TreeViewItemId} nextItemId The id of the active item after the keyboard navigation. - */ - selectItemFromArrowNavigation: ( - event: React.SyntheticEvent, - currentItemId: TreeViewItemId, - nextItemId: TreeViewItemId, - ) => void; -} - -export type TreeViewSelectionValue = Multiple extends true - ? TreeViewItemId[] - : TreeViewItemId | null; - -export interface UseTreeViewSelectionParameters { - /** - * Whether selection is disabled. - * @default false - */ - disableSelection?: boolean; - /** - * Selected item ids. (Uncontrolled) - * When `multiSelect` is true this takes an array of strings; when false (default) a string. - * @default [] - */ - defaultSelectedItems?: TreeViewSelectionValue; - /** - * Selected item ids. (Controlled) - * When `multiSelect` is true this takes an array of strings; when false (default) a string. - */ - selectedItems?: TreeViewSelectionValue; - /** - * Whether multiple items can be selected. - * @default false - */ - multiSelect?: Multiple; - /** - * Whether the Tree View renders a checkbox at the left of its label that allows selecting it. - * @default false - */ - checkboxSelection?: boolean; - /** - * When `selectionPropagation.descendants` is set to `true`. - * - * - Selecting a parent selects all its descendants automatically. - * - Deselecting a parent deselects all its descendants automatically. - * - * When `selectionPropagation.parents` is set to `true`. - * - * - Selecting all the descendants of a parent selects the parent automatically. - * - Deselecting a descendant of a selected parent deselects the parent automatically. - * - * Only works when `multiSelect` is `true`. - * On the , only the expanded items are considered (since the collapsed item are not passed to the Tree View component at all) - * - * @default { parents: false, descendants: false } - */ - selectionPropagation?: TreeViewSelectionPropagation; - /** - * Callback fired when Tree Items are selected/deselected. - * @param {React.SyntheticEvent} event The DOM event that triggered the change. Can be null when the change is caused by the `publicAPI.setItemSelection()` method. - * @param {TreeViewItemId[] | TreeViewItemId} itemIds The ids of the selected items. - * When `multiSelect` is `true`, this is an array of strings; when false (default) a string. - */ - onSelectedItemsChange?: ( - event: React.SyntheticEvent | null, - itemIds: TreeViewSelectionValue, - ) => void; - /** - * Callback fired when a Tree Item is selected or deselected. - * @param {React.SyntheticEvent} event The DOM event that triggered the change. Can be null when the change is caused by the `publicAPI.setItemSelection()` method. - * @param {TreeViewItemId} itemId The itemId of the modified item. - * @param {boolean} isSelected `true` if the item has just been selected, `false` if it has just been deselected. - */ - onItemSelectionToggle?: ( - event: React.SyntheticEvent | null, - itemId: TreeViewItemId, - isSelected: boolean, - ) => void; -} - -export type UseTreeViewSelectionParametersWithDefaults = DefaultizedProps< - UseTreeViewSelectionParameters, - | 'disableSelection' - | 'defaultSelectedItems' - | 'multiSelect' - | 'checkboxSelection' - | 'selectionPropagation' ->; - -export interface UseTreeViewSelectionState { - selection: { - selectedItems: TreeViewSelectionValue; - isEnabled: boolean; - isMultiSelectEnabled: boolean; - isCheckboxSelectionEnabled: boolean; - selectionPropagation: TreeViewSelectionPropagation; - }; -} - -export type UseTreeViewSelectionSignature = TreeViewPluginSignature<{ - params: UseTreeViewSelectionParameters; - paramsWithDefaults: UseTreeViewSelectionParametersWithDefaults; - instance: UseTreeViewSelectionInstance; - publicAPI: UseTreeViewSelectionPublicAPI; - state: UseTreeViewSelectionState; - dependencies: [ - UseTreeViewItemsSignature, - UseTreeViewExpansionSignature, - UseTreeViewItemsSignature, - ]; -}>; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts deleted file mode 100644 index a754b8bbfbd48..0000000000000 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { TreeViewItemId, TreeViewSelectionPropagation } from '../../../models'; -import { TreeViewUsedStore } from '../../models'; -import { UseTreeViewSelectionSignature } from './useTreeViewSelection.types'; -import { selectionSelectors } from './useTreeViewSelection.selectors'; -import { itemsSelectors } from '../useTreeViewItems/useTreeViewItems.selectors'; - -export const getLookupFromArray = (array: string[]) => { - const lookup: { [itemId: string]: true } = {}; - array.forEach((itemId) => { - lookup[itemId] = true; - }); - return lookup; -}; - -export const getAddedAndRemovedItems = ({ - store, - oldModel, - newModel, -}: { - store: TreeViewUsedStore; - oldModel: TreeViewItemId[]; - newModel: TreeViewItemId[]; -}) => { - const newModelMap = new Map(); - newModel.forEach((id) => { - newModelMap.set(id, true); - }); - - return { - added: newModel.filter((itemId) => !selectionSelectors.isItemSelected(store.state, itemId)), - removed: oldModel.filter((itemId) => !newModelMap.has(itemId)), - }; -}; - -export const propagateSelection = ({ - store, - selectionPropagation, - newModel, - oldModel, - additionalItemsToPropagate, -}: { - store: TreeViewUsedStore; - selectionPropagation: TreeViewSelectionPropagation; - newModel: TreeViewItemId[]; - oldModel: TreeViewItemId[]; - additionalItemsToPropagate?: TreeViewItemId[]; -}): string[] => { - if (!selectionPropagation.descendants && !selectionPropagation.parents) { - return newModel; - } - - let shouldRegenerateModel = false; - const newModelLookup = getLookupFromArray(newModel); - - const changes = getAddedAndRemovedItems({ - store, - newModel, - oldModel, - }); - - additionalItemsToPropagate?.forEach((itemId) => { - if (newModelLookup[itemId]) { - if (!changes.added.includes(itemId)) { - changes.added.push(itemId); - } - } else if (!changes.removed.includes(itemId)) { - changes.removed.push(itemId); - } - }); - - changes.added.forEach((addedItemId) => { - if (selectionPropagation.descendants) { - const selectDescendants = (itemId: TreeViewItemId) => { - if (itemId !== addedItemId) { - shouldRegenerateModel = true; - newModelLookup[itemId] = true; - } - - itemsSelectors.itemOrderedChildrenIds(store.state, itemId).forEach(selectDescendants); - }; - - selectDescendants(addedItemId); - } - - if (selectionPropagation.parents) { - const checkAllDescendantsSelected = (itemId: TreeViewItemId): boolean => { - if (!newModelLookup[itemId]) { - return false; - } - - const children = itemsSelectors.itemOrderedChildrenIds(store.state, itemId); - return children.every(checkAllDescendantsSelected); - }; - - const selectParents = (itemId: TreeViewItemId) => { - const parentId = itemsSelectors.itemParentId(store.state, itemId); - if (parentId == null) { - return; - } - - const siblings = itemsSelectors.itemOrderedChildrenIds(store.state, parentId); - if (siblings.every(checkAllDescendantsSelected)) { - shouldRegenerateModel = true; - newModelLookup[parentId] = true; - selectParents(parentId); - } - }; - selectParents(addedItemId); - } - }); - - changes.removed.forEach((removedItemId) => { - if (selectionPropagation.parents) { - let parentId = itemsSelectors.itemParentId(store.state, removedItemId); - while (parentId != null) { - if (newModelLookup[parentId]) { - shouldRegenerateModel = true; - delete newModelLookup[parentId]; - } - - parentId = itemsSelectors.itemParentId(store.state, parentId); - } - } - - if (selectionPropagation.descendants) { - const deSelectDescendants = (itemId: TreeViewItemId) => { - if (itemId !== removedItemId) { - shouldRegenerateModel = true; - delete newModelLookup[itemId]; - } - - itemsSelectors.itemOrderedChildrenIds(store.state, itemId).forEach(deSelectDescendants); - }; - - deSelectDescendants(removedItemId); - } - }); - - return shouldRegenerateModel ? Object.keys(newModelLookup) : newModel; -}; diff --git a/packages/x-tree-view/src/internals/useTreeView/index.ts b/packages/x-tree-view/src/internals/useTreeView/index.ts deleted file mode 100644 index a5af56b16433d..0000000000000 --- a/packages/x-tree-view/src/internals/useTreeView/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useTreeView } from './useTreeView'; diff --git a/packages/x-tree-view/src/internals/useTreeView/useExtractPluginParamsFromProps.ts b/packages/x-tree-view/src/internals/useTreeView/useExtractPluginParamsFromProps.ts deleted file mode 100644 index 98f6274b21b6f..0000000000000 --- a/packages/x-tree-view/src/internals/useTreeView/useExtractPluginParamsFromProps.ts +++ /dev/null @@ -1,82 +0,0 @@ -import * as React from 'react'; -import { - ConvertSignaturesIntoPlugins, - MergeSignaturesProperty, - TreeViewAnyPluginSignature, - TreeViewPlugin, - TreeViewPluginSignature, -} from '../models'; -import { UseTreeViewBaseProps } from './useTreeView.types'; -import { TreeViewCorePluginSignatures } from '../corePlugins'; - -export const useExtractPluginParamsFromProps = < - TSignatures extends readonly TreeViewPluginSignature[], - TProps extends Partial>, ->({ - props: { apiRef, ...props }, - plugins, -}: ExtractPluginParamsFromPropsParameters< - TSignatures, - TProps ->): ExtractPluginParamsFromPropsReturnValue => { - type PluginParams = MergeSignaturesProperty; - - const paramsLookup = React.useMemo(() => { - const tempParamsLookup = {} as Record; - plugins.forEach((plugin) => { - Object.assign(tempParamsLookup, plugin.params); - }); - return tempParamsLookup; - }, [plugins]); - - const { forwardedProps, pluginParams } = React.useMemo(() => { - const tempPluginParams = {} as PluginParams; - const tempForwardedProps = {} as Omit; - - Object.keys(props).forEach((propName) => { - const prop = props[propName as keyof typeof props] as any; - - if (paramsLookup[propName as keyof PluginParams]) { - tempPluginParams[propName as keyof PluginParams] = prop; - } else { - tempForwardedProps[propName as keyof typeof tempForwardedProps] = prop; - } - }); - - const pluginParamsWithDefaults = plugins.reduce( - (acc, plugin: TreeViewPlugin) => { - if (plugin.applyDefaultValuesToParams) { - return plugin.applyDefaultValuesToParams({ - params: acc, - }); - } - - return acc; - }, - tempPluginParams, - ) as unknown as MergeSignaturesProperty; - - return { - forwardedProps: tempForwardedProps, - pluginParams: pluginParamsWithDefaults, - }; - }, [plugins, props, paramsLookup]); - - return { forwardedProps, pluginParams, apiRef }; -}; - -interface ExtractPluginParamsFromPropsParameters< - TSignatures extends readonly TreeViewAnyPluginSignature[], - TProps extends Partial>, -> { - plugins: ConvertSignaturesIntoPlugins; - props: TProps; -} - -interface ExtractPluginParamsFromPropsReturnValue< - TSignatures extends readonly TreeViewAnyPluginSignature[], - TProps extends Partial>, -> extends UseTreeViewBaseProps { - pluginParams: MergeSignaturesProperty; - forwardedProps: Omit>; -} diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.test.tsx b/packages/x-tree-view/src/internals/useTreeView/useTreeView.test.tsx deleted file mode 100644 index 346bcec6443f2..0000000000000 --- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from 'react'; -import { fireEvent, act } from '@mui/internal-test-utils'; -import { - describeTreeView, - DescribeTreeViewRendererUtils, -} from 'test/utils/tree-view/describeTreeView'; - -// TODO #20051: Replace with imported type -type TreeViewAnyStore = { parameters: any }; - -describeTreeView( - 'useTreeView hook', - ({ render, renderFromJSX, treeViewComponentName, TreeViewComponent, TreeItemComponent }) => { - it('should have the role="tree" on the root slot', () => { - const view = render({ items: [{ id: '1' }] }); - - expect(view.getRoot()).to.have.attribute('role', 'tree'); - }); - - it('should work inside a Portal', () => { - let response: DescribeTreeViewRendererUtils; - if (treeViewComponentName === 'SimpleTreeView') { - response = renderFromJSX( - - - - - - - - - , - ); - } else { - response = renderFromJSX( - - - ({ 'data-testid': ownerState.itemId }) as any, - }} - getItemLabel={(item) => item.id} - /> - , - ); - } - - act(() => { - response.getItemRoot('1').focus(); - }); - - fireEvent.keyDown(response.getItemRoot('1'), { key: 'ArrowDown' }); - expect(response.getFocusedItemId()).to.equal('2'); - - fireEvent.keyDown(response.getItemRoot('2'), { key: 'ArrowDown' }); - expect(response.getFocusedItemId()).to.equal('3'); - - fireEvent.keyDown(response.getItemRoot('3'), { key: 'ArrowDown' }); - expect(response.getFocusedItemId()).to.equal('4'); - }); - }, -); diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts deleted file mode 100644 index 35a4128efc165..0000000000000 --- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts +++ /dev/null @@ -1,154 +0,0 @@ -import * as React from 'react'; -import { useRefWithInit } from '@base-ui-components/utils/useRefWithInit'; -import { Store } from '@mui/x-internals/store'; -import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs'; -import { EventHandlers } from '@mui/utils/types'; -import { - TreeViewAnyPluginSignature, - TreeViewInstance, - TreeViewPlugin, - TreeViewPublicAPI, - ConvertSignaturesIntoPlugins, - TreeViewState, -} from '../models'; -import { - UseTreeViewBaseProps, - UseTreeViewParameters, - UseTreeViewReturnValue, - UseTreeViewRootSlotProps, -} from './useTreeView.types'; -import { TREE_VIEW_CORE_PLUGINS, TreeViewCorePluginSignatures } from '../corePlugins'; -import { useExtractPluginParamsFromProps } from './useExtractPluginParamsFromProps'; -import { useTreeViewBuildContext } from './useTreeViewBuildContext'; - -function initializeInputApiRef(inputApiRef: React.RefObject | undefined>) { - if (inputApiRef.current == null) { - inputApiRef.current = {} as T; - } - return inputApiRef as React.RefObject; -} - -export function useTreeViewApiInitialization( - inputApiRef: React.RefObject | undefined> | undefined, -): React.RefObject { - const fallbackPublicApiRef = React.useRef({}) as React.RefObject; - - if (inputApiRef) { - return initializeInputApiRef(inputApiRef); - } - - return fallbackPublicApiRef; -} - -/** - * This is the main hook that sets the plugin system up for the tree-view. - * - * It manages the data used to create the tree-view. - * - * @param plugins All the plugins that will be used in the tree-view. - * @param props The props passed to the tree-view. - * @param rootRef The ref of the root element. - */ -export const useTreeView = < - TSignatures extends readonly TreeViewAnyPluginSignature[], - TProps extends Partial>, ->({ - plugins: inPlugins, - rootRef, - props, -}: UseTreeViewParameters): UseTreeViewReturnValue => { - type TSignaturesWithCorePluginSignatures = readonly [ - ...TreeViewCorePluginSignatures, - ...TSignatures, - ]; - const plugins = React.useMemo( - () => - [ - ...TREE_VIEW_CORE_PLUGINS, - ...inPlugins, - ] as unknown as ConvertSignaturesIntoPlugins, - [inPlugins], - ); - - const { pluginParams, forwardedProps, apiRef } = useExtractPluginParamsFromProps< - TSignatures, - typeof props - >({ - plugins, - props, - }); - - const instance = useRefWithInit(() => ({}) as TreeViewInstance).current; - const publicAPI = useTreeViewApiInitialization>(apiRef); - const innerRootRef = React.useRef(null); - const handleRootRef = useMergedRefs(innerRootRef, rootRef); - - const store = useRefWithInit(() => { - const initialState = {} as TreeViewState; - for (const plugin of plugins) { - if (plugin.getInitialState) { - Object.assign(initialState, plugin.getInitialState(pluginParams)); - } - } - return new Store>(initialState); - }).current; - - const contextValue = useTreeViewBuildContext({ - plugins, - instance, - publicAPI: publicAPI.current, - store, - rootRef: innerRootRef, - }); - - const rootPropsGetters: (( - otherHandlers: TOther, - ) => React.HTMLAttributes)[] = []; - - const runPlugin = (plugin: TreeViewPlugin) => { - const pluginResponse = plugin({ - instance, - params: pluginParams, - rootRef: innerRootRef, - plugins, - store, - }); - - if (pluginResponse.getRootProps) { - rootPropsGetters.push(pluginResponse.getRootProps); - } - - if (pluginResponse.publicAPI) { - Object.assign(publicAPI.current, pluginResponse.publicAPI); - } - - if (pluginResponse.instance) { - Object.assign(instance, pluginResponse.instance); - } - }; - - plugins.forEach(runPlugin); - - const getRootProps = ( - otherHandlers: TOther = {} as TOther, - ) => { - const rootProps: UseTreeViewRootSlotProps = { - role: 'tree', - ...forwardedProps, - ...otherHandlers, - ref: handleRootRef, - }; - - rootPropsGetters.forEach((rootPropsGetter) => { - Object.assign(rootProps, rootPropsGetter(otherHandlers)); - }); - - return rootProps; - }; - - return { - getRootProps, - rootRef: handleRootRef, - contextValue, - }; -}; diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.types.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.types.ts deleted file mode 100644 index e404bebf7057f..0000000000000 --- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.types.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from 'react'; -import { EventHandlers } from '@mui/utils/types'; -import type { TreeViewContextValue } from '../TreeViewProvider'; -import { - TreeViewAnyPluginSignature, - ConvertSignaturesIntoPlugins, - TreeViewPublicAPI, -} from '../models'; - -export interface UseTreeViewParameters< - TSignatures extends readonly TreeViewAnyPluginSignature[], - TProps extends Partial>, -> { - plugins: ConvertSignaturesIntoPlugins; - rootRef?: React.Ref | undefined; - props: TProps; // Omit, keyof UseTreeViewBaseParameters> -} - -export interface UseTreeViewBaseProps { - apiRef: React.RefObject> | undefined> | undefined; -} - -export interface UseTreeViewRootSlotProps - extends Pick< - React.HTMLAttributes, - 'onFocus' | 'onBlur' | 'onKeyDown' | 'id' | 'aria-multiselectable' | 'role' | 'tabIndex' - > { - ref: React.Ref; -} - -export interface UseTreeViewReturnValue { - getRootProps: ( - otherHandlers?: TOther, - ) => UseTreeViewRootSlotProps; - rootRef: React.RefCallback | null; - contextValue: TreeViewContextValue; -} diff --git a/packages/x-tree-view/src/internals/utils/cleanupTracking/CleanupTracking.ts b/packages/x-tree-view/src/internals/utils/cleanupTracking/CleanupTracking.ts deleted file mode 100644 index 39ec77052f09b..0000000000000 --- a/packages/x-tree-view/src/internals/utils/cleanupTracking/CleanupTracking.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type UnregisterToken = { cleanupToken: number }; - -export type UnsubscribeFn = () => void; - -export interface CleanupTracking { - register(object: any, unsubscribe: UnsubscribeFn, unregisterToken: UnregisterToken): void; - unregister(unregisterToken: UnregisterToken): void; - reset(): void; -} diff --git a/packages/x-tree-view/src/internals/utils/cleanupTracking/FinalizationRegistryBasedCleanupTracking.ts b/packages/x-tree-view/src/internals/utils/cleanupTracking/FinalizationRegistryBasedCleanupTracking.ts deleted file mode 100644 index 0d08515360f92..0000000000000 --- a/packages/x-tree-view/src/internals/utils/cleanupTracking/FinalizationRegistryBasedCleanupTracking.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CleanupTracking, UnsubscribeFn, UnregisterToken } from './CleanupTracking'; - -export class FinalizationRegistryBasedCleanupTracking implements CleanupTracking { - registry = new FinalizationRegistry((unsubscribe) => { - if (typeof unsubscribe === 'function') { - unsubscribe(); - } - }); - - register(object: any, unsubscribe: UnsubscribeFn, unregisterToken: UnregisterToken): void { - this.registry.register(object, unsubscribe, unregisterToken); - } - - unregister(unregisterToken: UnregisterToken): void { - this.registry.unregister(unregisterToken); - } - - reset() {} -} diff --git a/packages/x-tree-view/src/internals/utils/cleanupTracking/TimerBasedCleanupTracking.ts b/packages/x-tree-view/src/internals/utils/cleanupTracking/TimerBasedCleanupTracking.ts deleted file mode 100644 index 6da40496b960e..0000000000000 --- a/packages/x-tree-view/src/internals/utils/cleanupTracking/TimerBasedCleanupTracking.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { CleanupTracking, UnregisterToken, UnsubscribeFn } from './CleanupTracking'; - -// If no effect ran after this amount of time, we assume that the render was not committed by React -const CLEANUP_TIMER_LOOP_MILLIS = 1000; - -export class TimerBasedCleanupTracking implements CleanupTracking { - timeouts? = new Map>(); - - cleanupTimeout = CLEANUP_TIMER_LOOP_MILLIS; - - constructor(timeout = CLEANUP_TIMER_LOOP_MILLIS) { - this.cleanupTimeout = timeout; - } - - register(object: any, unsubscribe: UnsubscribeFn, unregisterToken: UnregisterToken): void { - if (!this.timeouts) { - this.timeouts = new Map>(); - } - - const timeout = setTimeout(() => { - if (typeof unsubscribe === 'function') { - unsubscribe(); - } - this.timeouts!.delete(unregisterToken.cleanupToken); - }, this.cleanupTimeout); - - this.timeouts!.set(unregisterToken!.cleanupToken, timeout); - } - - unregister(unregisterToken: UnregisterToken): void { - const timeout = this.timeouts!.get(unregisterToken.cleanupToken); - if (timeout) { - this.timeouts!.delete(unregisterToken.cleanupToken); - clearTimeout(timeout); - } - } - - reset() { - if (this.timeouts) { - this.timeouts.forEach((value, key) => { - this.unregister({ cleanupToken: key }); - }); - this.timeouts = undefined; - } - } -} diff --git a/packages/x-tree-view/src/internals/utils/plugins.ts b/packages/x-tree-view/src/internals/utils/plugins.ts deleted file mode 100644 index 6975427d0002e..0000000000000 --- a/packages/x-tree-view/src/internals/utils/plugins.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TreeViewAnyPluginSignature, TreeViewInstance, TreeViewPlugin } from '../models'; - -export const hasPlugin = < - TSignature extends TreeViewAnyPluginSignature, - TInstance extends TreeViewInstance<[], [TSignature]>, ->( - instance: TInstance, - plugin: TreeViewPlugin, -): instance is Omit & TSignature['instance'] => { - const plugins = instance.getAvailablePlugins(); - return plugins.has(plugin as any); -}; diff --git a/packages/x-tree-view/src/internals/utils/publishTreeViewEvent.ts b/packages/x-tree-view/src/internals/utils/publishTreeViewEvent.ts deleted file mode 100644 index dcc927fd1c00e..0000000000000 --- a/packages/x-tree-view/src/internals/utils/publishTreeViewEvent.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { UseTreeViewInstanceEventsInstance } from '../corePlugins/useTreeViewInstanceEvents/useTreeViewInstanceEvents.types'; -import { TreeViewAnyPluginSignature, TreeViewUsedEvents } from '../models'; - -export const publishTreeViewEvent = < - Instance extends UseTreeViewInstanceEventsInstance & { - $$signature: TreeViewAnyPluginSignature; - }, - E extends keyof TreeViewUsedEvents, ->( - instance: Instance, - eventName: E, - params: TreeViewUsedEvents[E]['params'], -) => { - instance.$$publishEvent(eventName as string, params); -}; diff --git a/packages/x-tree-view/src/internals/utils/tree.ts b/packages/x-tree-view/src/internals/utils/tree.ts index 28caf221f884c..2e3096bb1f69a 100644 --- a/packages/x-tree-view/src/internals/utils/tree.ts +++ b/packages/x-tree-view/src/internals/utils/tree.ts @@ -1,13 +1,9 @@ -import { TreeViewItemMeta, TreeViewState } from '../models'; -import type { UseTreeViewExpansionSignature } from '../plugins/useTreeViewExpansion'; -import { expansionSelectors } from '../plugins/useTreeViewExpansion/useTreeViewExpansion.selectors'; -import type { UseTreeViewItemsSignature } from '../plugins/useTreeViewItems'; -import { itemsSelectors } from '../plugins/useTreeViewItems/useTreeViewItems.selectors'; - -const getLastNavigableItemInArray = ( - state: TreeViewState<[UseTreeViewItemsSignature]>, - items: string[], -) => { +import { MinimalTreeViewState } from '../MinimalTreeViewStore'; +import { TreeViewItemMeta } from '../models'; +import { expansionSelectors } from '../plugins/expansion/selectors'; +import { itemsSelectors } from '../plugins/items/selectors'; + +const getLastNavigableItemInArray = (state: MinimalTreeViewState, items: string[]) => { // Equivalent to Array.prototype.findLastIndex let itemIndex = items.length - 1; while (itemIndex >= 0 && !itemsSelectors.canItemBeFocused(state, items[itemIndex])) { @@ -22,7 +18,7 @@ const getLastNavigableItemInArray = ( }; export const getPreviousNavigableItem = ( - state: TreeViewState<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, + state: MinimalTreeViewState, itemId: string, ): string | null => { const itemMeta = itemsSelectors.itemMeta(state, itemId); @@ -74,10 +70,7 @@ export const getPreviousNavigableItem = ( return currentItemId; }; -export const getNextNavigableItem = ( - state: TreeViewState<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, - itemId: string, -) => { +export const getNextNavigableItem = (state: MinimalTreeViewState, itemId: string) => { // If the item is expanded and has some navigable children, return the first of them. if (expansionSelectors.isItemExpanded(state, itemId)) { const firstNavigableChild = itemsSelectors @@ -115,9 +108,7 @@ export const getNextNavigableItem = ( return null; }; -export const getLastNavigableItem = ( - state: TreeViewState<[UseTreeViewExpansionSignature, UseTreeViewItemsSignature]>, -) => { +export const getLastNavigableItem = (state: MinimalTreeViewState) => { let itemId: string | null = null; while (itemId == null || expansionSelectors.isItemExpanded(state, itemId)) { const children = itemsSelectors.itemOrderedChildrenIds(state, itemId); @@ -134,9 +125,7 @@ export const getLastNavigableItem = ( return itemId!; }; -export const getFirstNavigableItem = ( - state: TreeViewState<[UseTreeViewExpansionSignature, UseTreeViewItemsSignature]>, -) => +export const getFirstNavigableItem = (state: MinimalTreeViewState) => itemsSelectors .itemOrderedChildrenIds(state, null) .find((itemId) => itemsSelectors.canItemBeFocused(state, itemId))!; @@ -156,7 +145,7 @@ export const getFirstNavigableItem = ( * https://en.wikipedia.org/wiki/Tr%C3%A9maux_tree */ export const findOrderInTremauxTree = ( - state: TreeViewState<[UseTreeViewExpansionSignature, UseTreeViewItemsSignature]>, + state: MinimalTreeViewState, itemAId: string, itemBId: string, ) => { @@ -221,7 +210,7 @@ export const findOrderInTremauxTree = ( }; export const getNonDisabledItemsInRange = ( - state: TreeViewState<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, + state: MinimalTreeViewState, itemAId: string, itemBId: string, ) => { @@ -265,9 +254,7 @@ export const getNonDisabledItemsInRange = ( return items; }; -export const getAllNavigableItems = ( - state: TreeViewState<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, -) => { +export const getAllNavigableItems = (state: MinimalTreeViewState) => { let item: string | null = getFirstNavigableItem(state); const navigableItems: string[] = []; while (item != null) { diff --git a/packages/x-tree-view/src/models/items.ts b/packages/x-tree-view/src/models/items.ts index dc38abc0a5329..956f325a1e6ff 100644 --- a/packages/x-tree-view/src/models/items.ts +++ b/packages/x-tree-view/src/models/items.ts @@ -7,10 +7,15 @@ export type TreeViewDefaultItemModelProperties = { children?: TreeViewDefaultItemModelProperties[]; }; +/** + * @deprecated Use `TreeViewDefaultItemModelProperties` instead, or define your own item model interface. + */ export type TreeViewBaseItem = R & { children?: TreeViewBaseItem[]; }; +export type TreeViewValidItem = { children?: R[] }; + export type TreeViewItemsReorderingAction = | 'reorder-above' | 'reorder-below' diff --git a/packages/x-tree-view/src/useTreeItem/useTreeItem.test.tsx b/packages/x-tree-view/src/useTreeItem/useTreeItem.test.tsx index bfb1c41554fa3..46747ff32be91 100644 --- a/packages/x-tree-view/src/useTreeItem/useTreeItem.test.tsx +++ b/packages/x-tree-view/src/useTreeItem/useTreeItem.test.tsx @@ -6,9 +6,7 @@ import { DescribeTreeViewRendererUtils, } from 'test/utils/tree-view/describeTreeView'; import { treeItemClasses } from '@mui/x-tree-view/TreeItem'; - -// TODO #20051: Replace with imported type -type TreeViewAnyStore = { parameters: any }; +import { TreeViewAnyStore } from '../internals/models'; describeTreeView( 'useTreeItem hook', diff --git a/packages/x-tree-view/src/useTreeItem/useTreeItem.ts b/packages/x-tree-view/src/useTreeItem/useTreeItem.ts index 0922849c3ead3..f8820b92b977b 100644 --- a/packages/x-tree-view/src/useTreeItem/useTreeItem.ts +++ b/packages/x-tree-view/src/useTreeItem/useTreeItem.ts @@ -4,7 +4,7 @@ import { useStore } from '@mui/x-internals/store'; import { EventHandlers } from '@mui/utils/types'; import extractEventHandlers from '@mui/utils/extractEventHandlers'; import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs'; -import { TreeViewCancellableEvent } from '../models'; +import { TreeViewCancellableEvent, TreeViewItemId } from '../models'; import { UseTreeItemParameters, UseTreeItemReturnValue, @@ -15,8 +15,6 @@ import { UseTreeItemIconContainerSlotProps, UseTreeItemCheckboxSlotProps, UseTreeItemLabelInputSlotProps, - UseTreeItemMinimalPlugins, - UseTreeItemOptionalPlugins, UseTreeItemDragAndDropOverlaySlotProps, UseTreeItemRootSlotPropsFromUseTreeItem, UseTreeItemContentSlotPropsFromUseTreeItem, @@ -24,27 +22,39 @@ import { UseTreeItemLoadingContainerSlotProps, } from './useTreeItem.types'; import { useTreeViewContext } from '../internals/TreeViewProvider'; -import { TreeViewItemPluginSlotPropsEnhancerParams } from '../internals/models'; +import { + TreeViewItemPluginSlotPropsEnhancerParams, + TreeViewAnyStore, + TreeViewPublicAPI, +} from '../internals/models'; import { useTreeItemUtils } from '../hooks/useTreeItemUtils'; import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext'; import { isTargetInDescendants } from '../internals/utils/tree'; -import { generateTreeItemIdAttribute } from '../internals/corePlugins/useTreeViewId/useTreeViewId.utils'; -import { focusSelectors } from '../internals/plugins/useTreeViewFocus'; -import { itemsSelectors } from '../internals/plugins/useTreeViewItems'; -import { idSelectors } from '../internals/corePlugins/useTreeViewId'; -import { expansionSelectors } from '../internals/plugins/useTreeViewExpansion'; -import { selectionSelectors } from '../internals/plugins/useTreeViewSelection'; - -export const useTreeItem = < - TSignatures extends UseTreeItemMinimalPlugins = UseTreeItemMinimalPlugins, - TOptionalSignatures extends UseTreeItemOptionalPlugins = UseTreeItemOptionalPlugins, ->( +import { focusSelectors } from '../internals/plugins/focus'; +import { itemsSelectors } from '../internals/plugins/items'; +import { idSelectors } from '../internals/plugins/id'; +import { expansionSelectors } from '../internals/plugins/expansion'; +import { selectionSelectors } from '../internals/plugins/selection'; +import { RichTreeViewStore } from '../internals/RichTreeViewStore'; + +// TODO v8: Remove the lazy loading plugin from the typing on the community useTreeItem and ask users to pass the TStore generic. +interface DefaultStore extends RichTreeViewStore { + buildPublicAPI: () => TreeViewPublicAPI> & { + /** + * Method used for updating an item's children. + * Only relevant for lazy-loaded tree views. + * + * @param {TreeViewItemId} itemId The The id of the item to update the children of. + * @returns {Promise} The promise resolved when the items are fetched. + */ + updateItemChildren: (itemId: TreeViewItemId) => Promise; + }; +} + +export const useTreeItem = ( parameters: UseTreeItemParameters, -): UseTreeItemReturnValue => { - const { runItemPlugins, instance, publicAPI, store } = useTreeViewContext< - TSignatures, - TOptionalSignatures - >(); +): UseTreeItemReturnValue => { + const { runItemPlugins, publicAPI, store } = useTreeViewContext(); const depthContext = React.useContext(TreeViewItemDepthContext); const depth = useStore( @@ -69,9 +79,8 @@ export const useTreeItem = < const handleContentRef = useMergedRefs(contentRef, contentRefObject)!; const checkboxRef = React.useRef(null); - const treeId = useStore(store, idSelectors.treeId); const isCheckboxSelectionEnabled = useStore(store, selectionSelectors.isCheckboxSelectionEnabled); - const idAttribute = generateTreeItemIdAttribute({ itemId, treeId, id }); + const idAttribute = useStore(store, idSelectors.treeItemIdAttribute, itemId, id); const shouldBeAccessibleWithTab = useStore( store, focusSelectors.isItemTheDefaultFocusableItem, @@ -96,7 +105,7 @@ export const useTreeItem = < itemsSelectors.canItemBeFocused(store.state, itemId) && event.currentTarget === event.target ) { - instance.focusItem(event, itemId); + store.focus.focusItem(event, itemId); } }; @@ -108,7 +117,7 @@ export const useTreeItem = < return; } - const rootElement = instance.getItemDOMElement(itemId); + const rootElement = store.items.getItemDOMElement(itemId); // Don't blur the root when switching to editing mode // the input that triggers the root blur can be either the relatedTarget (when entering editing state) or the target (when exiting editing state) @@ -127,7 +136,7 @@ export const useTreeItem = < return; } - instance.removeFocusedItem(); + store.focus.removeFocusedItem(); }; const createRootHandleKeyDown = @@ -141,7 +150,7 @@ export const useTreeItem = < return; } - instance.handleItemKeyDown(event, itemId); + store.keyboardNavigation.handleItemKeyDown(event, itemId); }; const createLabelHandleDoubleClick = @@ -156,7 +165,7 @@ export const useTreeItem = < const createContentHandleClick = (otherHandlers: EventHandlers) => (event: React.MouseEvent & TreeViewCancellableEvent) => { otherHandlers.onClick?.(event); - instance.handleItemClick(event, itemId); + store.items.handleItemClick(event, itemId); if (event.defaultMuiPrevented || checkboxRef.current?.contains(event.target as HTMLElement)) { return; diff --git a/packages/x-tree-view/src/useTreeItem/useTreeItem.types.ts b/packages/x-tree-view/src/useTreeItem/useTreeItem.types.ts index d781c296f381b..d4aa6a55ace53 100644 --- a/packages/x-tree-view/src/useTreeItem/useTreeItem.types.ts +++ b/packages/x-tree-view/src/useTreeItem/useTreeItem.types.ts @@ -1,13 +1,6 @@ import * as React from 'react'; import { TreeViewItemId, TreeViewCancellableEventHandler } from '../models'; -import { TreeViewPublicAPI } from '../internals/models'; -import { UseTreeViewSelectionSignature } from '../internals/plugins/useTreeViewSelection'; -import { UseTreeViewItemsSignature } from '../internals/plugins/useTreeViewItems'; -import { UseTreeViewFocusSignature } from '../internals/plugins/useTreeViewFocus'; -import { UseTreeViewKeyboardNavigationSignature } from '../internals/plugins/useTreeViewKeyboardNavigation'; -import { UseTreeViewLabelSignature } from '../internals/plugins/useTreeViewLabel'; -import { UseTreeViewExpansionSignature } from '../internals/plugins/useTreeViewExpansion'; -import { UseTreeViewLazyLoadingSignature } from '../internals/plugins/useTreeViewLazyLoading'; +import { TreeViewPublicAPI, TreeViewAnyStore } from '../internals/models'; export interface UseTreeItemParameters { /** @@ -143,10 +136,7 @@ export interface UseTreeItemStatus { error: boolean; } -export interface UseTreeItemReturnValue< - TSignatures extends UseTreeItemMinimalPlugins, - TOptionalSignatures extends UseTreeItemOptionalPlugins, -> { +export interface UseTreeItemReturnValue { /** * Resolver for the context provider's props. * @returns {UseTreeItemContextProviderProps} Props that should be spread on the context provider slot. @@ -246,22 +236,5 @@ export interface UseTreeItemReturnValue< /** * The object the allows Tree View manipulation. */ - publicAPI: TreeViewPublicAPI; + publicAPI: TreeViewPublicAPI; } - -/** - * Plugins that need to be present in the Tree View in order for `UseTreeItem` to work correctly. - */ -export type UseTreeItemMinimalPlugins = readonly [ - UseTreeViewSelectionSignature, - UseTreeViewExpansionSignature, - UseTreeViewItemsSignature, - UseTreeViewFocusSignature, - UseTreeViewKeyboardNavigationSignature, - UseTreeViewLabelSignature, -]; - -/** - * Plugins that `UseTreeItem` can use if they are present, but are not required. - */ -export type UseTreeItemOptionalPlugins = readonly [UseTreeViewLazyLoadingSignature]; diff --git a/scripts/x-tree-view-pro.exports.json b/scripts/x-tree-view-pro.exports.json index ea84060ba34dd..310cc0fab9fce 100644 --- a/scripts/x-tree-view-pro.exports.json +++ b/scripts/x-tree-view-pro.exports.json @@ -8,7 +8,6 @@ { "name": "richTreeViewProClasses", "kind": "Variable" }, { "name": "RichTreeViewProClasses", "kind": "Interface" }, { "name": "RichTreeViewProClassKey", "kind": "TypeAlias" }, - { "name": "RichTreeViewProPluginSignatures", "kind": "TypeAlias" }, { "name": "RichTreeViewProProps", "kind": "Interface" }, { "name": "RichTreeViewProPropsBase", "kind": "Interface" }, { "name": "RichTreeViewProRoot", "kind": "Variable" }, @@ -55,8 +54,11 @@ { "name": "TreeViewItemId", "kind": "TypeAlias" }, { "name": "TreeViewItemsReorderingAction", "kind": "TypeAlias" }, { "name": "TreeViewSelectionPropagation", "kind": "Interface" }, - { "name": "unstable_resetCleanupTracking", "kind": "Variable" }, + { "name": "TreeViewValidItem", "kind": "TypeAlias" }, { "name": "useApplyPropagationToSelectedItemsOnMount", "kind": "Function" }, + { "name": "useRichTreeViewApiRef", "kind": "Function" }, + { "name": "useRichTreeViewProApiRef", "kind": "Function" }, + { "name": "useSimpleTreeViewApiRef", "kind": "Function" }, { "name": "useTreeItem", "kind": "Variable" }, { "name": "UseTreeItemCheckboxSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItemContentSlotOwnProps", "kind": "Interface" }, diff --git a/scripts/x-tree-view.exports.json b/scripts/x-tree-view.exports.json index d8089bde3543f..40b80145a1636 100644 --- a/scripts/x-tree-view.exports.json +++ b/scripts/x-tree-view.exports.json @@ -3,13 +3,11 @@ { "name": "getSimpleTreeViewUtilityClass", "kind": "Function" }, { "name": "getTreeItemUtilityClass", "kind": "Function" }, { "name": "PropsFromSlot", "kind": "TypeAlias" }, - { "name": "RICH_TREE_VIEW_PLUGINS", "kind": "Variable" }, { "name": "RichTreeView", "kind": "Variable" }, { "name": "RichTreeViewApiRef", "kind": "TypeAlias" }, { "name": "richTreeViewClasses", "kind": "Variable" }, { "name": "RichTreeViewClasses", "kind": "Interface" }, { "name": "RichTreeViewClassKey", "kind": "TypeAlias" }, - { "name": "RichTreeViewPluginParameters", "kind": "Interface" }, { "name": "RichTreeViewProps", "kind": "Interface" }, { "name": "RichTreeViewPropsBase", "kind": "Interface" }, { "name": "RichTreeViewRoot", "kind": "Variable" }, @@ -56,8 +54,10 @@ { "name": "TreeViewItemId", "kind": "TypeAlias" }, { "name": "TreeViewItemsReorderingAction", "kind": "TypeAlias" }, { "name": "TreeViewSelectionPropagation", "kind": "Interface" }, - { "name": "unstable_resetCleanupTracking", "kind": "Variable" }, + { "name": "TreeViewValidItem", "kind": "TypeAlias" }, { "name": "useApplyPropagationToSelectedItemsOnMount", "kind": "Function" }, + { "name": "useRichTreeViewApiRef", "kind": "Function" }, + { "name": "useSimpleTreeViewApiRef", "kind": "Function" }, { "name": "useTreeItem", "kind": "Variable" }, { "name": "UseTreeItemCheckboxSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItemContentSlotOwnProps", "kind": "Interface" }, diff --git a/test/setupVitest.ts b/test/setupVitest.ts index 57be829995545..0443dd3fd2361 100644 --- a/test/setupVitest.ts +++ b/test/setupVitest.ts @@ -8,7 +8,6 @@ import { config } from 'react-transition-group'; import sinon from 'sinon'; import { unstable_resetCleanupTracking as unstable_resetCleanupTrackingDataGrid } from '@mui/x-data-grid'; import { unstable_resetCleanupTracking as unstable_resetCleanupTrackingDataGridPro } from '@mui/x-data-grid-pro'; -import { unstable_resetCleanupTracking as unstable_resetCleanupTrackingTreeView } from '@mui/x-tree-view'; import failOnConsole from 'vitest-fail-on-console'; import { clearWarningsCache } from '@mui/x-internals/warning'; import { hasTouchSupport, isJSDOM } from './utils/skipIf'; @@ -35,7 +34,6 @@ beforeEach(() => { afterEach(() => { unstable_resetCleanupTrackingDataGrid(); unstable_resetCleanupTrackingDataGridPro(); - unstable_resetCleanupTrackingTreeView(); // Restore Sinon default sandbox to avoid memory leak // See https://github.com/sinonjs/sinon/issues/1866 diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.tsx b/test/utils/tree-view/describeTreeView/describeTreeView.tsx index f7b266fc6a912..14503f28695c8 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.tsx +++ b/test/utils/tree-view/describeTreeView/describeTreeView.tsx @@ -5,6 +5,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 { TreeViewDefaultItemModelProperties } from '@mui/x-tree-view/models'; +import { TreeViewAnyStore, TreeViewPublicAPI } from '@mui/x-tree-view/internals/models'; import { MuiRenderResult } from '@mui/internal-test-utils/createRenderer'; import { DescribeTreeViewTestRunner, @@ -15,13 +16,6 @@ import { TreeViewItemIdTreeElement, } from './describeTreeView.types'; -// TODO #20051: Replace with imported type -type TreeViewAnyStore = { parameters: any }; - -// TODO #20051: Replace with imported type -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type TreeViewPublicAPI = any; - const innerDescribeTreeView = ( message: string, testRunner: DescribeTreeViewTestRunner, @@ -144,8 +138,7 @@ const innerDescribeTreeView = ( 'data-testid': ownerState.itemId, }) as any, }} - // TODO #20051: Remove any - getItemLabel={(item: any) => { + getItemLabel={(item) => { if (item.label) { if (typeof item.label !== 'string') { throw new Error('Only use string labels when testing RichTreeView(Pro)'); @@ -156,8 +149,7 @@ const innerDescribeTreeView = ( return item.id; }} - // TODO #20051: Remove any - isItemDisabled={(item: any) => !!item.disabled} + isItemDisabled={(item) => !!item.disabled} {...other} /> ); diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts index 9157d2aecc240..b5ffcba1d1918 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts +++ b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts @@ -1,18 +1,12 @@ import * as React from 'react'; +import { TreeViewAnyStore, 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 { TreeViewSlotProps, TreeViewSlots } from '@mui/x-tree-view/internals'; - -// TODO #20051: Replace with imported type -type TreeViewAnyStore = { parameters: any }; - -// TODO #20051: Replace with imported type -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type TreeViewPublicAPI = any; - -// TODO #20051: Replace with imported type -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type UseTreeViewStoreParameters = any; +import { + TreeViewSlotProps, + TreeViewSlots, + UseTreeViewStoreParameters, +} from '@mui/x-tree-view/internals'; export type DescribeTreeViewTestRunner = ( params: DescribeTreeViewTestRunnerParams, diff --git a/test/utils/tree-view/fakeContextValue.ts b/test/utils/tree-view/fakeContextValue.ts index 629a092a87b87..435abd2a0dbf0 100644 --- a/test/utils/tree-view/fakeContextValue.ts +++ b/test/utils/tree-view/fakeContextValue.ts @@ -1,41 +1,23 @@ -import { Store } from '@base-ui-components/utils/store'; +import { UseTreeViewStoreParameters } from '@mui/x-tree-view/internals'; +import { SimpleTreeViewStore } from '@mui/x-tree-view/internals/SimpleTreeViewStore'; import { TreeViewContextValue } from '@mui/x-tree-view/internals/TreeViewProvider'; -import { SimpleTreeViewPluginSignatures } from '@mui/x-tree-view/SimpleTreeView/SimpleTreeView.plugins'; export const getFakeContextValue = ( - features: { checkboxSelection?: boolean } = {}, -): TreeViewContextValue => ({ - instance: {} as any, - publicAPI: {} as any, - runItemPlugins: () => ({ - rootRef: null, - contentRef: null, - propsEnhancers: {}, - }), - wrapItem: ({ children }) => children, - wrapRoot: ({ children }) => children, - rootRef: { - current: null, - }, - store: new Store({ - cacheKey: { id: 1 }, - id: { treeId: 'mui-tree-view-1', providedTreeId: undefined }, - items: { - disabledItemsFocusable: false, - itemMetaLookup: {}, - itemModelLookup: {}, - itemOrderedChildrenIdsLookup: {}, - itemChildrenIndexesLookup: {}, - domStructure: 'nested', - }, - expansion: { expandedItems: [], expansionTrigger: 'content' }, - selection: { - selectedItems: null, - isEnabled: true, - isMultiSelectEnabled: false, - isCheckboxSelectionEnabled: features.checkboxSelection ?? false, - selectionPropagation: { parents: false, descendants: false }, + parameters: UseTreeViewStoreParameters> = {}, +): TreeViewContextValue> => { + const store = new SimpleTreeViewStore({ ...parameters, isRtl: false }); + + return { + store, + publicAPI: store.buildPublicAPI(), + runItemPlugins: () => ({ + rootRef: null, + contentRef: null, + propsEnhancers: {}, + }), + wrapItem: ({ children }) => children, + rootRef: { + current: null, }, - focus: { focusedItemId: null }, - }) as any, -}); + }; +};