From 204b522c57cc62d356d48ab55da7a41d8635efc1 Mon Sep 17 00:00:00 2001 From: Nora <72460825+noraleonte@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:44:48 +0300 Subject: [PATCH] [TreeView] Add `expansionTrigger` prop (#13533) Signed-off-by: Nora <72460825+noraleonte@users.noreply.github.com> Co-authored-by: Flavien DELANGLE --- .../IconExpansionTreeView.tsx.preview | 1 - .../customization/customization.md | 6 -- .../IconExpansionTreeView.js | 32 +--------- .../IconExpansionTreeView.tsx | 35 +---------- .../IconExpansionTreeView.tsx.preview | 1 + .../rich-tree-view/expansion/expansion.md | 6 ++ .../customization/IconExpansionTreeView.js | 57 ------------------ .../customization/IconExpansionTreeView.tsx | 60 ------------------- .../customization/customization.md | 13 ---- .../expansion/IconExpansionTreeView.js | 28 +++++++++ .../expansion/IconExpansionTreeView.tsx | 28 +++++++++ .../simple-tree-view/expansion/expansion.md | 6 ++ .../pages/x/api/tree-view/rich-tree-view.json | 4 ++ .../x/api/tree-view/simple-tree-view.json | 4 ++ docs/pages/x/api/tree-view/tree-view.json | 4 ++ .../rich-tree-view/rich-tree-view.json | 3 + .../simple-tree-view/simple-tree-view.json | 3 + .../tree-view/tree-view/tree-view.json | 3 + .../src/RichTreeView/RichTreeView.tsx | 5 ++ .../src/SimpleTreeView/SimpleTreeView.tsx | 5 ++ .../x-tree-view/src/TreeItem/TreeItem.tsx | 16 +++-- .../src/TreeItem/TreeItemContent.tsx | 5 +- .../src/TreeItem/useTreeItemState.ts | 2 + .../x-tree-view/src/TreeView/TreeView.tsx | 5 ++ .../useTreeViewExpansion.ts | 14 +++++ .../useTreeViewExpansion.types.ts | 10 ++++ .../src/useTreeItem2/useTreeItem2.ts | 17 +++++- .../src/useTreeItem2/useTreeItem2.types.ts | 6 +- test/utils/tree-view/fakeContextValue.ts | 1 + 29 files changed, 171 insertions(+), 209 deletions(-) delete mode 100644 docs/data/tree-view/rich-tree-view/customization/IconExpansionTreeView.tsx.preview rename docs/data/tree-view/rich-tree-view/{customization => expansion}/IconExpansionTreeView.js (55%) rename docs/data/tree-view/rich-tree-view/{customization => expansion}/IconExpansionTreeView.tsx (51%) create mode 100644 docs/data/tree-view/rich-tree-view/expansion/IconExpansionTreeView.tsx.preview delete mode 100644 docs/data/tree-view/simple-tree-view/customization/IconExpansionTreeView.js delete mode 100644 docs/data/tree-view/simple-tree-view/customization/IconExpansionTreeView.tsx create mode 100644 docs/data/tree-view/simple-tree-view/expansion/IconExpansionTreeView.js create mode 100644 docs/data/tree-view/simple-tree-view/expansion/IconExpansionTreeView.tsx diff --git a/docs/data/tree-view/rich-tree-view/customization/IconExpansionTreeView.tsx.preview b/docs/data/tree-view/rich-tree-view/customization/IconExpansionTreeView.tsx.preview deleted file mode 100644 index 660542f7bf2cd..0000000000000 --- a/docs/data/tree-view/rich-tree-view/customization/IconExpansionTreeView.tsx.preview +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/customization/customization.md b/docs/data/tree-view/rich-tree-view/customization/customization.md index 8f1552b6f0df4..4bedda31d7645 100644 --- a/docs/data/tree-view/rich-tree-view/customization/customization.md +++ b/docs/data/tree-view/rich-tree-view/customization/customization.md @@ -64,12 +64,6 @@ The demo below shows how to add an avatar and custom typography elements. ## Common examples -### Limit expansion to icon container - -The demo below shows how to trigger the expansion interaction just by clicking on the icon container instead of the whole Tree Item surface. - -{{"demo": "IconExpansionTreeView.js", "defaultCodeOpen": false}} - ### File explorer :::warning diff --git a/docs/data/tree-view/rich-tree-view/customization/IconExpansionTreeView.js b/docs/data/tree-view/rich-tree-view/expansion/IconExpansionTreeView.js similarity index 55% rename from docs/data/tree-view/rich-tree-view/customization/IconExpansionTreeView.js rename to docs/data/tree-view/rich-tree-view/expansion/IconExpansionTreeView.js index fa2935769a638..db2d29f0fcfa8 100644 --- a/docs/data/tree-view/rich-tree-view/customization/IconExpansionTreeView.js +++ b/docs/data/tree-view/rich-tree-view/expansion/IconExpansionTreeView.js @@ -1,9 +1,6 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeItem2Utils } from '@mui/x-tree-view/hooks'; - -import { TreeItem2 } from '@mui/x-tree-view/TreeItem2'; const MUI_X_PRODUCTS = [ { @@ -35,37 +32,10 @@ const MUI_X_PRODUCTS = [ }, ]; -const CustomTreeItem = React.forwardRef(function MyTreeItem(props, ref) { - const { interactions } = useTreeItem2Utils({ - itemId: props.itemId, - children: props.children, - }); - - const handleContentClick = (event) => { - event.defaultMuiPrevented = true; - interactions.handleSelection(event); - }; - - const handleIconContainerClick = (event) => { - interactions.handleExpansion(event); - }; - - return ( - - ); -}); - export default function IconExpansionTreeView() { return ( - + ); } diff --git a/docs/data/tree-view/rich-tree-view/customization/IconExpansionTreeView.tsx b/docs/data/tree-view/rich-tree-view/expansion/IconExpansionTreeView.tsx similarity index 51% rename from docs/data/tree-view/rich-tree-view/customization/IconExpansionTreeView.tsx rename to docs/data/tree-view/rich-tree-view/expansion/IconExpansionTreeView.tsx index 0d7345a1047d5..94b9816b9a897 100644 --- a/docs/data/tree-view/rich-tree-view/customization/IconExpansionTreeView.tsx +++ b/docs/data/tree-view/rich-tree-view/expansion/IconExpansionTreeView.tsx @@ -1,9 +1,6 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; -import { useTreeItem2Utils } from '@mui/x-tree-view/hooks'; -import { UseTreeItem2ContentSlotOwnProps } from '@mui/x-tree-view/useTreeItem2'; -import { TreeItem2, TreeItem2Props } from '@mui/x-tree-view/TreeItem2'; import { TreeViewBaseItem } from '@mui/x-tree-view/models'; const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ @@ -36,40 +33,10 @@ const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ }, ]; -const CustomTreeItem = React.forwardRef(function MyTreeItem( - props: TreeItem2Props, - ref: React.Ref, -) { - const { interactions } = useTreeItem2Utils({ - itemId: props.itemId, - children: props.children, - }); - - const handleContentClick: UseTreeItem2ContentSlotOwnProps['onClick'] = (event) => { - event.defaultMuiPrevented = true; - interactions.handleSelection(event); - }; - - const handleIconContainerClick = (event: React.MouseEvent) => { - interactions.handleExpansion(event); - }; - - return ( - - ); -}); - export default function IconExpansionTreeView() { return ( - + ); } diff --git a/docs/data/tree-view/rich-tree-view/expansion/IconExpansionTreeView.tsx.preview b/docs/data/tree-view/rich-tree-view/expansion/IconExpansionTreeView.tsx.preview new file mode 100644 index 0000000000000..49f8c4e181622 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/expansion/IconExpansionTreeView.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file 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 e36fe65d20c1e..47897f4c9170f 100644 --- a/docs/data/tree-view/rich-tree-view/expansion/expansion.md +++ b/docs/data/tree-view/rich-tree-view/expansion/expansion.md @@ -33,6 +33,12 @@ Use the `onItemExpansionToggle` prop if you want to react to an item expansion c {{"demo": "TrackItemExpansionToggle.js"}} +## Limit expansion to icon container + +You can use the `expansionTrigger` prop to decide if the expansion interaction should be triggered by clicking on the icon container instead of the whole Tree Item content. + +{{"demo": "IconExpansionTreeView.js"}} + ## Imperative API :::success diff --git a/docs/data/tree-view/simple-tree-view/customization/IconExpansionTreeView.js b/docs/data/tree-view/simple-tree-view/customization/IconExpansionTreeView.js deleted file mode 100644 index 36e9e200da117..0000000000000 --- a/docs/data/tree-view/simple-tree-view/customization/IconExpansionTreeView.js +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; -import { useTreeItem2Utils } from '@mui/x-tree-view/hooks'; - -import { TreeItem2 } from '@mui/x-tree-view/TreeItem2'; - -const CustomTreeItem = React.forwardRef(function MyTreeItem(props, ref) { - const { interactions } = useTreeItem2Utils({ - itemId: props.itemId, - children: props.children, - }); - - const handleContentClick = (event) => { - event.defaultMuiPrevented = true; - interactions.handleSelection(event); - }; - - const handleIconContainerClick = (event) => { - interactions.handleExpansion(event); - }; - - return ( - - ); -}); - -export default function IconExpansionTreeView() { - return ( - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/tree-view/simple-tree-view/customization/IconExpansionTreeView.tsx b/docs/data/tree-view/simple-tree-view/customization/IconExpansionTreeView.tsx deleted file mode 100644 index f0a5de292e600..0000000000000 --- a/docs/data/tree-view/simple-tree-view/customization/IconExpansionTreeView.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; -import { useTreeItem2Utils } from '@mui/x-tree-view/hooks'; -import { UseTreeItem2ContentSlotOwnProps } from '@mui/x-tree-view/useTreeItem2'; -import { TreeItem2, TreeItem2Props } from '@mui/x-tree-view/TreeItem2'; - -const CustomTreeItem = React.forwardRef(function MyTreeItem( - props: TreeItem2Props, - ref: React.Ref, -) { - const { interactions } = useTreeItem2Utils({ - itemId: props.itemId, - children: props.children, - }); - - const handleContentClick: UseTreeItem2ContentSlotOwnProps['onClick'] = (event) => { - event.defaultMuiPrevented = true; - interactions.handleSelection(event); - }; - - const handleIconContainerClick = (event: React.MouseEvent) => { - interactions.handleExpansion(event); - }; - - return ( - - ); -}); - -export default function IconExpansionTreeView() { - return ( - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/docs/data/tree-view/simple-tree-view/customization/customization.md b/docs/data/tree-view/simple-tree-view/customization/customization.md index e5e70b9e4aa89..7cf009836436f 100644 --- a/docs/data/tree-view/simple-tree-view/customization/customization.md +++ b/docs/data/tree-view/simple-tree-view/customization/customization.md @@ -70,19 +70,6 @@ Target the `treeItemClasses.groupTransition` class to add connection borders bet {{"demo": "BorderedTreeView.js", "defaultCodeOpen": false}} -### Limit expansion to icon container - -:::warning -This example is built using the new `TreeItem2` component -which adds several slots to modify the content of the Tree Item or change its behavior. - -You can learn more about this new component in the [Overview page](/x/react-tree-view/#tree-item-components). -::: - -The demo below shows how to trigger the expansion interaction just by clicking on the icon container instead of the whole Tree Item surface. - -{{"demo": "IconExpansionTreeView.js", "defaultCodeOpen": false}} - ### Gmail clone :::warning diff --git a/docs/data/tree-view/simple-tree-view/expansion/IconExpansionTreeView.js b/docs/data/tree-view/simple-tree-view/expansion/IconExpansionTreeView.js new file mode 100644 index 0000000000000..3a046031d357a --- /dev/null +++ b/docs/data/tree-view/simple-tree-view/expansion/IconExpansionTreeView.js @@ -0,0 +1,28 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; +import { TreeItem } from '@mui/x-tree-view/TreeItem'; + +export default function IconExpansionTreeView() { + return ( + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/docs/data/tree-view/simple-tree-view/expansion/IconExpansionTreeView.tsx b/docs/data/tree-view/simple-tree-view/expansion/IconExpansionTreeView.tsx new file mode 100644 index 0000000000000..3a046031d357a --- /dev/null +++ b/docs/data/tree-view/simple-tree-view/expansion/IconExpansionTreeView.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; +import { TreeItem } from '@mui/x-tree-view/TreeItem'; + +export default function IconExpansionTreeView() { + return ( + + + + + + + + + + + + + + + + + + + + ); +} 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 a6b0a3f4e46ac..a3648bd3697ff 100644 --- a/docs/data/tree-view/simple-tree-view/expansion/expansion.md +++ b/docs/data/tree-view/simple-tree-view/expansion/expansion.md @@ -32,6 +32,12 @@ Use the `onItemExpansionToggle` prop to trigger an action upon an item being exp {{"demo": "TrackItemExpansionToggle.js"}} +## Limit expansion to icon container + +You can use the `expansionTrigger` prop to decide if the expansion interaction should be triggered by clicking on the icon container instead of the whole Tree Item content. + +{{"demo": "IconExpansionTreeView.js"}} + ## Imperative API :::success diff --git a/docs/pages/x/api/tree-view/rich-tree-view.json b/docs/pages/x/api/tree-view/rich-tree-view.json index 7da4e28d56f04..302a091490e72 100644 --- a/docs/pages/x/api/tree-view/rich-tree-view.json +++ b/docs/pages/x/api/tree-view/rich-tree-view.json @@ -16,6 +16,10 @@ "disabledItemsFocusable": { "type": { "name": "bool" }, "default": "false" }, "disableSelection": { "type": { "name": "bool" }, "default": "false" }, "expandedItems": { "type": { "name": "arrayOf", "description": "Array<string>" } }, + "expansionTrigger": { + "type": { "name": "enum", "description": "'content'
| 'iconContainer'" }, + "default": "'content'" + }, "experimentalFeatures": { "type": { "name": "shape", "description": "{ indentationAtItemLevel?: bool }" } }, diff --git a/docs/pages/x/api/tree-view/simple-tree-view.json b/docs/pages/x/api/tree-view/simple-tree-view.json index 1c52f9e03646b..24df95176eb56 100644 --- a/docs/pages/x/api/tree-view/simple-tree-view.json +++ b/docs/pages/x/api/tree-view/simple-tree-view.json @@ -17,6 +17,10 @@ "disabledItemsFocusable": { "type": { "name": "bool" }, "default": "false" }, "disableSelection": { "type": { "name": "bool" }, "default": "false" }, "expandedItems": { "type": { "name": "arrayOf", "description": "Array<string>" } }, + "expansionTrigger": { + "type": { "name": "enum", "description": "'content'
| 'iconContainer'" }, + "default": "'content'" + }, "experimentalFeatures": { "type": { "name": "shape", "description": "{ indentationAtItemLevel?: bool }" } }, diff --git a/docs/pages/x/api/tree-view/tree-view.json b/docs/pages/x/api/tree-view/tree-view.json index a528b9927844b..b2eead268281a 100644 --- a/docs/pages/x/api/tree-view/tree-view.json +++ b/docs/pages/x/api/tree-view/tree-view.json @@ -17,6 +17,10 @@ "disabledItemsFocusable": { "type": { "name": "bool" }, "default": "false" }, "disableSelection": { "type": { "name": "bool" }, "default": "false" }, "expandedItems": { "type": { "name": "arrayOf", "description": "Array<string>" } }, + "expansionTrigger": { + "type": { "name": "enum", "description": "'content'
| 'iconContainer'" }, + "default": "'content'" + }, "experimentalFeatures": { "type": { "name": "shape", "description": "{ indentationAtItemLevel?: bool }" } }, 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 b866f4dcc07a5..42345574e68d5 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 @@ -21,6 +21,9 @@ "expandedItems": { "description": "Expanded item ids. Used when the item's expansion is controlled." }, + "expansionTrigger": { + "description": "The slot that triggers the item's expansion when clicked." + }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect." }, 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 a12a983517785..8d0b15e73d6ac 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 @@ -22,6 +22,9 @@ "expandedItems": { "description": "Expanded item ids. Used when the item's expansion is controlled." }, + "expansionTrigger": { + "description": "The slot that triggers the item's expansion when clicked." + }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect." }, diff --git a/docs/translations/api-docs/tree-view/tree-view/tree-view.json b/docs/translations/api-docs/tree-view/tree-view/tree-view.json index c29df21217338..f61dba71a60ae 100644 --- a/docs/translations/api-docs/tree-view/tree-view/tree-view.json +++ b/docs/translations/api-docs/tree-view/tree-view/tree-view.json @@ -22,6 +22,9 @@ "expandedItems": { "description": "Expanded item ids. Used when the item's expansion is controlled." }, + "expansionTrigger": { + "description": "The slot that triggers the item's expansion when clicked." + }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect." }, diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx index 5f94c0db2be7a..1af8e390626f8 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx @@ -191,6 +191,11 @@ RichTreeView.propTypes = { * Used when the item's expansion is controlled. */ expandedItems: PropTypes.arrayOf(PropTypes.string), + /** + * The slot that triggers the item's expansion when clicked. + * @default 'content' + */ + expansionTrigger: PropTypes.oneOf(['content', 'iconContainer']), /** * Unstable features, breaking changes might be introduced. * For each feature, if the flag is not explicitly set to `true`, diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx index 472bf6208f582..9993677439e3d 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx @@ -154,6 +154,11 @@ SimpleTreeView.propTypes = { * Used when the item's expansion is controlled. */ expandedItems: PropTypes.arrayOf(PropTypes.string), + /** + * The slot that triggers the item's expansion when clicked. + * @default 'content' + */ + expansionTrigger: PropTypes.oneOf(['content', 'iconContainer']), /** * Unstable features, breaking changes might be introduced. * For each feature, if the flag is not explicitly set to `true`, diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx index 2f35d89749058..b048be14f8dc8 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx @@ -23,6 +23,7 @@ import { useTreeViewContext } from '../internals/TreeViewProvider/useTreeViewCon import { TreeViewCollapseIcon, TreeViewExpandIcon } from '../icons'; import { TreeItem2Provider } from '../TreeItem2Provider'; import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext'; +import { useTreeItemState } from './useTreeItemState'; const useThemeProps = createUseThemeProps('MuiTreeItem'); @@ -187,6 +188,7 @@ export const TreeItem = React.forwardRef(function TreeItem( icons: contextIcons, runItemPlugins, selection: { multiSelect }, + expansion: { expansionTrigger }, disabledItemsFocusable, indentationAtItemLevel, instance, @@ -213,6 +215,8 @@ export const TreeItem = React.forwardRef(function TreeItem( ...other } = props; + const { expanded, focused, selected, disabled, handleExpansion } = useTreeItemState(itemId); + const { contentRef, rootRef } = runItemPlugins(props); const handleRootRef = useForkRef(inRef, rootRef); const handleContentRef = useForkRef(ContentProps?.ref, contentRef); @@ -232,10 +236,6 @@ export const TreeItem = React.forwardRef(function TreeItem( return Boolean(reactChildren); }; const expandable = isExpandable(children); - const expanded = instance.isItemExpanded(itemId); - const focused = instance.isItemFocused(itemId); - const selected = instance.isItemSelected(itemId); - const disabled = instance.isItemDisabled(itemId); const ownerState: TreeItemOwnerState = { ...props, @@ -263,6 +263,11 @@ export const TreeItem = React.forwardRef(function TreeItem( className: classes.groupTransition, }); + const handleIconContainerClick = (event: React.MouseEvent) => { + if (expansionTrigger === 'iconContainer') { + handleExpansion(event); + } + }; const ExpansionIcon = expanded ? slots.collapseIcon : slots.expandIcon; const { ownerState: expansionIconOwnerState, ...expansionIconProps } = useSlotProps({ elementType: ExpansionIcon, @@ -280,6 +285,9 @@ export const TreeItem = React.forwardRef(function TreeItem( ...resolveComponentProps(inSlotProps?.expandIcon, tempOwnerState), }; }, + additionalProps: { + onClick: handleIconContainerClick, + }, }); const expansionIcon = expandable && !!ExpansionIcon ? : null; diff --git a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx index 11c31e401ceeb..c04b65ed08ea0 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx @@ -82,6 +82,7 @@ const TreeItemContent = React.forwardRef(function TreeItemContent( handleSelection, handleCheckboxSelection, preventSelection, + expansionTrigger, } = useTreeItemState(itemId); const icon = iconProp || expansionIcon || displayIcon; @@ -100,7 +101,9 @@ const TreeItemContent = React.forwardRef(function TreeItemContent( return; } - handleExpansion(event); + if (expansionTrigger === 'content') { + handleExpansion(event); + } if (!checkboxSelection) { handleSelection(event); diff --git a/packages/x-tree-view/src/TreeItem/useTreeItemState.ts b/packages/x-tree-view/src/TreeItem/useTreeItemState.ts index 09f9ce6f88c2e..5a05ee89bf28b 100644 --- a/packages/x-tree-view/src/TreeItem/useTreeItemState.ts +++ b/packages/x-tree-view/src/TreeItem/useTreeItemState.ts @@ -18,6 +18,7 @@ export function useTreeItemState(itemId: string) { const { instance, selection: { multiSelect, checkboxSelection, disableSelection }, + expansion: { expansionTrigger }, } = useTreeViewContext(); const expandable = instance.isItemExpandable(itemId); @@ -92,5 +93,6 @@ export function useTreeItemState(itemId: string) { handleSelection, handleCheckboxSelection, preventSelection, + expansionTrigger, }; } diff --git a/packages/x-tree-view/src/TreeView/TreeView.tsx b/packages/x-tree-view/src/TreeView/TreeView.tsx index 2149524f39fd0..55d215de61837 100644 --- a/packages/x-tree-view/src/TreeView/TreeView.tsx +++ b/packages/x-tree-view/src/TreeView/TreeView.tsx @@ -139,6 +139,11 @@ TreeView.propTypes = { * Used when the item's expansion is controlled. */ expandedItems: PropTypes.arrayOf(PropTypes.string), + /** + * The slot that triggers the item's expansion when clicked. + * @default 'content' + */ + expansionTrigger: PropTypes.oneOf(['content', 'iconContainer']), /** * Unstable features, breaking changes might be introduced. * For each feature, if the flag is not explicitly set to `true`, diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts index d463f24e85bb0..3fd669acf55f9 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts @@ -83,6 +83,14 @@ export const useTreeViewExpansion: TreeViewPlugin } }; + const expansionTrigger = React.useMemo(() => { + if (params.expansionTrigger) { + return params.expansionTrigger; + } + + return 'content'; + }, [params.expansionTrigger]); + return { publicAPI: { setItemExpansion, @@ -94,6 +102,11 @@ export const useTreeViewExpansion: TreeViewPlugin toggleItemExpansion, expandAllSiblings, }, + contextValue: { + expansion: { + expansionTrigger, + }, + }, }; }; @@ -115,4 +128,5 @@ useTreeViewExpansion.params = { 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 index f3e93a059afd5..2b7c858d56719 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts @@ -72,6 +72,11 @@ export interface UseTreeViewExpansionParameters { itemId: string, isExpanded: boolean, ) => void; + /** + * The slot that triggers the item's expansion when clicked. + * @default 'content' + */ + expansionTrigger?: 'content' | 'iconContainer'; } export type UseTreeViewExpansionDefaultizedParameters = DefaultizedProps< @@ -79,11 +84,16 @@ export type UseTreeViewExpansionDefaultizedParameters = DefaultizedProps< 'defaultExpandedItems' >; +interface UseTreeViewExpansionContextValue { + expansion: Pick; +} + export type UseTreeViewExpansionSignature = TreeViewPluginSignature<{ params: UseTreeViewExpansionParameters; defaultizedParams: UseTreeViewExpansionDefaultizedParameters; instance: UseTreeViewExpansionInstance; publicAPI: UseTreeViewExpansionPublicAPI; modelNames: 'expandedItems'; + contextValue: UseTreeViewExpansionContextValue; dependencies: [UseTreeViewItemsSignature]; }>; diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts index 54c6e3dc7435c..904a6706dcc5b 100644 --- a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts +++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts @@ -27,6 +27,7 @@ export const useTreeItem2 = < const { runItemPlugins, selection: { multiSelect, disableSelection, checkboxSelection }, + expansion: { expansionTrigger }, disabledItemsFocusable, indentationAtItemLevel, instance, @@ -85,7 +86,9 @@ export const useTreeItem2 = < return; } - interactions.handleExpansion(event); + if (expansionTrigger === 'content') { + interactions.handleExpansion(event); + } if (!checkboxSelection) { interactions.handleSelection(event); @@ -120,6 +123,17 @@ export const useTreeItem2 = < interactions.handleCheckboxSelection(event); }; + const createIconContainerHandleClick = + (otherHandlers: EventHandlers) => (event: React.MouseEvent & MuiCancellableEvent) => { + otherHandlers.onClick?.(event); + if (event.defaultMuiPrevented) { + return; + } + if (expansionTrigger === 'iconContainer') { + interactions.handleExpansion(event); + } + }; + const getRootProps = = {}>( externalProps: ExternalProps = {} as ExternalProps, ): UseTreeItem2RootSlotProps => { @@ -227,6 +241,7 @@ export const useTreeItem2 = < return { ...externalEventHandlers, ...externalProps, + onClick: createIconContainerHandleClick(externalEventHandlers), }; }; diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts index 6aff48add0b16..8d29f86796e3d 100644 --- a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts +++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts @@ -6,6 +6,7 @@ import { UseTreeViewSelectionSignature } from '../internals/plugins/useTreeViewS import { UseTreeViewItemsSignature } from '../internals/plugins/useTreeViewItems'; import { UseTreeViewFocusSignature } from '../internals/plugins/useTreeViewFocus'; import { UseTreeViewKeyboardNavigationSignature } from '../internals/plugins/useTreeViewKeyboardNavigation'; +import { UseTreeViewExpansionSignature } from '../internals/plugins/useTreeViewExpansion'; export interface UseTreeItem2Parameters { /** @@ -67,7 +68,9 @@ export interface UseTreeItem2ContentSlotOwnProps { export type UseTreeItem2ContentSlotProps = ExternalProps & UseTreeItem2ContentSlotOwnProps; -export interface UseTreeItem2IconContainerSlotOwnProps {} +export interface UseTreeItem2IconContainerSlotOwnProps { + onClick: MuiCancellableEventHandler; +} export type UseTreeItemIconContainerSlotProps = ExternalProps & UseTreeItem2IconContainerSlotOwnProps; @@ -185,6 +188,7 @@ export interface UseTreeItem2ReturnValue< */ export type UseTreeItem2MinimalPlugins = readonly [ UseTreeViewSelectionSignature, + UseTreeViewExpansionSignature, UseTreeViewItemsSignature, UseTreeViewFocusSignature, UseTreeViewKeyboardNavigationSignature, diff --git a/test/utils/tree-view/fakeContextValue.ts b/test/utils/tree-view/fakeContextValue.ts index 5f4e44545102c..f310590520351 100644 --- a/test/utils/tree-view/fakeContextValue.ts +++ b/test/utils/tree-view/fakeContextValue.ts @@ -39,4 +39,5 @@ export const getFakeContextValue = ( rootRef: { current: null, }, + expansion: { expansionTrigger: 'content' }, });