From 1f92246f0d59df0b2284f5404dcadec81d24d677 Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Thu, 23 May 2024 16:54:29 +0200 Subject: [PATCH] [TreeView] Allow to define indentation at the item level (#13126) --- .../pages/x/api/tree-view/rich-tree-view.json | 3 ++ .../x/api/tree-view/simple-tree-view.json | 3 ++ docs/pages/x/api/tree-view/tree-view.json | 3 ++ .../rich-tree-view/rich-tree-view.json | 3 ++ .../simple-tree-view/simple-tree-view.json | 3 ++ .../tree-view/tree-view/tree-view.json | 3 ++ .../RichTreeViewPro/RichTreeViewPro.types.ts | 8 ++++- .../src/RichTreeView/RichTreeView.tsx | 8 +++++ .../src/RichTreeView/RichTreeView.types.ts | 12 ++++++- .../src/SimpleTreeView/SimpleTreeView.tsx | 8 +++++ .../SimpleTreeView/SimpleTreeView.types.ts | 8 ++++- .../src/TreeItem/TreeItem.test.tsx | 1 + .../x-tree-view/src/TreeItem/TreeItem.tsx | 31 +++++++++++++++++++ .../src/TreeItem/TreeItem.types.ts | 1 + .../x-tree-view/src/TreeItem2/TreeItem2.tsx | 31 ++++++++++++------- .../TreeItem2Provider/TreeItem2Provider.tsx | 4 +-- .../x-tree-view/src/TreeView/TreeView.tsx | 8 +++++ .../TreeViewItemDepthContext.ts | 10 ++++++ .../TreeViewItemDepthContext/index.ts | 1 + .../TreeViewProvider.types.ts | 4 +-- packages/x-tree-view/src/internals/index.ts | 1 + .../src/internals/models/helpers.ts | 1 + .../src/internals/models/plugin.ts | 21 ++++++++++--- .../src/internals/models/treeView.ts | 10 +++++- ...eTreeViewItems.ts => useTreeViewItems.tsx} | 22 ++++++++++--- .../useTreeViewItems.types.ts | 5 ++- .../useTreeViewJSXItems.tsx | 20 +++++++++--- .../src/internals/useTreeView/useTreeView.ts | 12 ++++--- .../useTreeView/useTreeView.types.ts | 2 ++ .../utils/extractPluginParamsFromProps.ts | 5 ++- .../src/useTreeItem2/useTreeItem2.ts | 30 ++++++++++++++++-- .../src/useTreeItem2/useTreeItem2.types.ts | 12 +++++++ 32 files changed, 253 insertions(+), 41 deletions(-) create mode 100644 packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts create mode 100644 packages/x-tree-view/src/internals/TreeViewItemDepthContext/index.ts rename packages/x-tree-view/src/internals/plugins/useTreeViewItems/{useTreeViewItems.ts => useTreeViewItems.tsx} (90%) 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 c80f7ce68570a..bc08974346195 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,9 @@ "disabledItemsFocusable": { "type": { "name": "bool" }, "default": "false" }, "disableSelection": { "type": { "name": "bool" }, "default": "false" }, "expandedItems": { "type": { "name": "arrayOf", "description": "Array<string>" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ indentationAtItemLevel?: bool }" } + }, "getItemId": { "type": { "name": "func" }, "default": "(item) => item.id", 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 f346fcd7a7004..6003d38a33a93 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,9 @@ "disabledItemsFocusable": { "type": { "name": "bool" }, "default": "false" }, "disableSelection": { "type": { "name": "bool" }, "default": "false" }, "expandedItems": { "type": { "name": "arrayOf", "description": "Array<string>" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ indentationAtItemLevel?: bool }" } + }, "id": { "type": { "name": "string" } }, "multiSelect": { "type": { "name": "bool" }, "default": "false" }, "onExpandedItemsChange": { diff --git a/docs/pages/x/api/tree-view/tree-view.json b/docs/pages/x/api/tree-view/tree-view.json index 4fd0587a6b119..45a574640afff 100644 --- a/docs/pages/x/api/tree-view/tree-view.json +++ b/docs/pages/x/api/tree-view/tree-view.json @@ -17,6 +17,9 @@ "disabledItemsFocusable": { "type": { "name": "bool" }, "default": "false" }, "disableSelection": { "type": { "name": "bool" }, "default": "false" }, "expandedItems": { "type": { "name": "arrayOf", "description": "Array<string>" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ indentationAtItemLevel?: bool }" } + }, "id": { "type": { "name": "string" } }, "multiSelect": { "type": { "name": "bool" }, "default": "false" }, "onExpandedItemsChange": { 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 1dc23fbbce097..17c5fb3d6eb9d 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." }, + "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." + }, "getItemId": { "description": "Used to determine the id of a given item.", "typeDescriptions": { "item": "The item to check.", "string": "The id of the item." } 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 637ca728dab07..2523d471b4e72 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." }, + "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." + }, "id": { "description": "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." }, 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 907ed9df8c7c3..2e1fed84729f8 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." }, + "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." + }, "id": { "description": "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." }, 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 5921da69be73f..7603b6fde12d8 100644 --- a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.types.ts +++ b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.types.ts @@ -5,7 +5,7 @@ import { SlotComponentProps } from '@mui/base/utils'; import { TreeItem, TreeItemProps } from '@mui/x-tree-view/TreeItem'; import { TreeItem2Props } from '@mui/x-tree-view/TreeItem2'; import { TreeViewItemId } from '@mui/x-tree-view/models'; -import { TreeViewPublicAPI } from '@mui/x-tree-view/internals'; +import { TreeViewPublicAPI, TreeViewExperimentalFeatures } from '@mui/x-tree-view/internals'; import { RichTreeViewProClasses } from './richTreeViewProClasses'; import { DefaultTreeViewProPluginParameters, @@ -71,4 +71,10 @@ export interface RichTreeViewProProps; } diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx index 624d106a3a0d8..7a3ea33fbf288 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx @@ -194,6 +194,14 @@ RichTreeView.propTypes = { * Used when the item's expansion is controlled. */ expandedItems: PropTypes.arrayOf(PropTypes.string), + /** + * 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. + */ + experimentalFeatures: PropTypes.shape({ + indentationAtItemLevel: PropTypes.bool, + }), /** * Used to determine the id of a given item. * diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts b/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts index a2ffd144c2f8d..974a665b69a46 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts @@ -12,7 +12,11 @@ import { import { TreeItemProps } from '../TreeItem'; import { TreeItem2Props } from '../TreeItem2'; import { TreeViewItemId } from '../models'; -import { SlotComponentPropsFromProps, TreeViewPublicAPI } from '../internals/models'; +import { + SlotComponentPropsFromProps, + TreeViewExperimentalFeatures, + TreeViewPublicAPI, +} from '../internals/models'; interface RichTreeViewItemSlotOwnerState { itemId: TreeViewItemId; @@ -75,4 +79,10 @@ export interface RichTreeViewProps; } diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx index 4db161facf850..d25cb79422364 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx @@ -161,6 +161,14 @@ SimpleTreeView.propTypes = { * Used when the item's expansion is controlled. */ expandedItems: PropTypes.arrayOf(PropTypes.string), + /** + * 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. + */ + experimentalFeatures: PropTypes.shape({ + indentationAtItemLevel: PropTypes.bool, + }), /** * 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. diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts index b5be79437a156..b7e0ddcbbcdb2 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.types.ts @@ -9,7 +9,7 @@ import { SimpleTreeViewPluginSlots, SimpleTreeViewPlugins, } from './SimpleTreeView.plugins'; -import { TreeViewPublicAPI } from '../internals/models'; +import { TreeViewExperimentalFeatures, TreeViewPublicAPI } from '../internals/models'; export interface SimpleTreeViewSlots extends SimpleTreeViewPluginSlots { /** @@ -55,4 +55,10 @@ export interface SimpleTreeViewProps * The ref object that allows Tree View manipulation. Can be instantiated with `useTreeViewApiRef()`. */ apiRef?: SimpleTreeViewApiRef; + /** + * 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. + */ + experimentalFeatures?: TreeViewExperimentalFeatures; } diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx index 74da492eaff36..a70b2e39d3f8e 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.test.tsx @@ -30,6 +30,7 @@ const TEST_TREE_VIEW_CONTEXT_VALUE: TreeViewContextValue wrapItem: ({ children }) => children, wrapRoot: ({ children }) => children, disabledItemsFocusable: false, + indentationAtItemLevel: false, icons: { slots: {}, slotProps: {}, diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx index 8201c9940460d..fdf9820fd0d67 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx @@ -4,6 +4,7 @@ import clsx from 'clsx'; import Collapse from '@mui/material/Collapse'; import { resolveComponentProps, useSlotProps } from '@mui/base/utils'; import useForkRef from '@mui/utils/useForkRef'; +import { shouldForwardProp } from '@mui/system/createStyled'; import { alpha, styled, useThemeProps } from '@mui/material/styles'; import { TransitionProps } from '@mui/material/transitions'; import unsupportedProp from '@mui/utils/unsupportedProp'; @@ -16,6 +17,7 @@ import { useTreeViewContext } from '../internals/TreeViewProvider/useTreeViewCon import { DefaultTreeViewPlugins } from '../internals/plugins'; import { TreeViewCollapseIcon, TreeViewExpandIcon } from '../icons'; import { TreeItem2Provider } from '../TreeItem2Provider'; +import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext'; const useUtilityClasses = (ownerState: TreeItemOwnerState) => { const { classes } = ownerState; @@ -61,6 +63,7 @@ const StyledTreeItemContent = styled(TreeItemContent, { }, ]; }, + shouldForwardProp: (prop) => shouldForwardProp(prop) && prop !== 'indentationAtItemLevel', })<{ ownerState: TreeItemOwnerState }>(({ theme }) => ({ padding: theme.spacing(0.5, 1), borderRadius: theme.shape.borderRadius, @@ -132,16 +135,31 @@ const StyledTreeItemContent = styled(TreeItemContent, { [`& .${treeItemClasses.checkbox}`]: { padding: 0, }, + variants: [ + { + props: { indentationAtItemLevel: true }, + style: { + paddingLeft: `calc(${theme.spacing(1)} + 12px * var(--TreeView-itemDepth))`, + }, + }, + ], })); const TreeItemGroup = styled(Collapse, { name: 'MuiTreeItem', slot: 'GroupTransition', overridesResolver: (props, styles) => styles.groupTransition, + shouldForwardProp: (prop) => shouldForwardProp(prop) && prop !== 'indentationAtItemLevel', })({ margin: 0, padding: 0, paddingLeft: 12, + variants: [ + { + props: { indentationAtItemLevel: true }, + style: { paddingLeft: 0 }, + }, + ], }); /** @@ -163,8 +181,10 @@ export const TreeItem = React.forwardRef(function TreeItem( runItemPlugins, selection: { multiSelect }, disabledItemsFocusable, + indentationAtItemLevel, instance, } = useTreeViewContext(); + const depthContext = React.useContext(TreeViewItemDepthContext); const props = useThemeProps({ props: inProps, name: 'MuiTreeItem' }); @@ -216,6 +236,7 @@ export const TreeItem = React.forwardRef(function TreeItem( focused, selected, disabled, + indentationAtItemLevel, }; const classes = useUtilityClasses(ownerState); @@ -230,6 +251,7 @@ export const TreeItem = React.forwardRef(function TreeItem( in: expanded, component: 'ul', role: 'group', + ...(indentationAtItemLevel ? { indentationAtItemLevel: true } : {}), }, className: classes.groupTransition, }); @@ -329,6 +351,15 @@ export const TreeItem = React.forwardRef(function TreeItem( onBlur={handleBlur} onKeyDown={handleKeyDown} ref={handleRootRef} + style={ + indentationAtItemLevel + ? ({ + ...other.style, + '--TreeView-itemDepth': + typeof depthContext === 'function' ? depthContext(itemId) : depthContext, + } as React.CSSProperties) + : other.style + } > styles.content, - shouldForwardProp: (prop) => shouldForwardProp(prop) && prop !== 'status', -})(({ theme }) => ({ + shouldForwardProp: (prop) => + shouldForwardProp(prop) && prop !== 'status' && prop !== 'indentationAtItemLevel', +})<{ status: UseTreeItem2Status; indentationAtItemLevel?: true }>(({ theme }) => ({ padding: theme.spacing(0.5, 1), borderRadius: theme.shape.borderRadius, width: '100%', @@ -50,12 +52,13 @@ export const TreeItem2Content = styled('div', { backgroundColor: 'transparent', }, }, - [`& .${treeItemClasses.groupTransition}`]: { - margin: 0, - padding: 0, - paddingLeft: 12, - }, variants: [ + { + props: { indentationAtItemLevel: true }, + style: { + paddingLeft: `calc(${theme.spacing(1)} + 12px * var(--TreeView-itemDepth))`, + }, + }, { props: ({ status }: UseTreeItem2ContentSlotOwnProps) => status.disabled, style: { @@ -134,10 +137,17 @@ export const TreeItem2GroupTransition = styled(Collapse, { name: 'MuiTreeItem2', slot: 'GroupTransition', overridesResolver: (props, styles) => styles.groupTransition, -})({ + shouldForwardProp: (prop) => shouldForwardProp(prop) && prop !== 'indentationAtItemLevel', +})<{ indentationAtItemLevel?: true }>({ margin: 0, padding: 0, paddingLeft: 12, + variants: [ + { + props: { indentationAtItemLevel: true }, + style: { paddingLeft: 0 }, + }, + ], }); export const TreeItem2Checkbox = styled( @@ -250,7 +260,6 @@ export const TreeItem2 = React.forwardRef(function TreeItem2( [classes.disabled]: status.disabled, }), }); - const IconContainer: React.ElementType = slots.iconContainer ?? TreeItem2IconContainer; const iconContainerProps = useSlotProps({ elementType: IconContainer, diff --git a/packages/x-tree-view/src/TreeItem2Provider/TreeItem2Provider.tsx b/packages/x-tree-view/src/TreeItem2Provider/TreeItem2Provider.tsx index 93ed2461541f9..2b5387522cef1 100644 --- a/packages/x-tree-view/src/TreeItem2Provider/TreeItem2Provider.tsx +++ b/packages/x-tree-view/src/TreeItem2Provider/TreeItem2Provider.tsx @@ -4,9 +4,9 @@ import { useTreeViewContext } from '../internals/TreeViewProvider/useTreeViewCon function TreeItem2Provider(props: TreeItem2ProviderProps) { const { children, itemId } = props; - const { wrapItem } = useTreeViewContext<[]>(); + const { wrapItem, instance } = useTreeViewContext<[]>(); - return wrapItem({ children, itemId }); + return wrapItem({ children, itemId, instance }); } TreeItem2Provider.propTypes = { diff --git a/packages/x-tree-view/src/TreeView/TreeView.tsx b/packages/x-tree-view/src/TreeView/TreeView.tsx index 8434b19200d0c..71a155acd522f 100644 --- a/packages/x-tree-view/src/TreeView/TreeView.tsx +++ b/packages/x-tree-view/src/TreeView/TreeView.tsx @@ -137,6 +137,14 @@ TreeView.propTypes = { * Used when the item's expansion is controlled. */ expandedItems: PropTypes.arrayOf(PropTypes.string), + /** + * 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. + */ + experimentalFeatures: PropTypes.shape({ + indentationAtItemLevel: PropTypes.bool, + }), /** * 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. diff --git a/packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts b/packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts new file mode 100644 index 0000000000000..e06f1da340382 --- /dev/null +++ b/packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { TreeViewItemId } from '../../models'; + +export const TreeViewItemDepthContext = React.createContext< + number | ((itemId: TreeViewItemId) => number) +>(() => -1); + +if (process.env.NODE_ENV !== 'production') { + TreeViewItemDepthContext.displayName = 'TreeViewItemDepthContext'; +} diff --git a/packages/x-tree-view/src/internals/TreeViewItemDepthContext/index.ts b/packages/x-tree-view/src/internals/TreeViewItemDepthContext/index.ts new file mode 100644 index 0000000000000..aefec1d8a5c5c --- /dev/null +++ b/packages/x-tree-view/src/internals/TreeViewItemDepthContext/index.ts @@ -0,0 +1 @@ +export { TreeViewItemDepthContext } from './TreeViewItemDepthContext'; 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 9ff9f5af735eb..2d42554e5d8d5 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts @@ -14,8 +14,8 @@ export type TreeViewContextValue; publicAPI: TreeViewPublicAPI; rootRef: React.RefObject; - wrapItem: TreeItemWrapper; - wrapRoot: TreeRootWrapper; + wrapItem: TreeItemWrapper; + wrapRoot: TreeRootWrapper; runItemPlugins: (props: TProps) => Required; }; diff --git a/packages/x-tree-view/src/internals/index.ts b/packages/x-tree-view/src/internals/index.ts index c1ddf24a82748..9500d00e37f34 100644 --- a/packages/x-tree-view/src/internals/index.ts +++ b/packages/x-tree-view/src/internals/index.ts @@ -9,6 +9,7 @@ export type { ConvertPluginsIntoSignatures, MergePluginsProperty, TreeViewPublicAPI, + TreeViewExperimentalFeatures, } from './models'; // Plugins diff --git a/packages/x-tree-view/src/internals/models/helpers.ts b/packages/x-tree-view/src/internals/models/helpers.ts index e3ee527a4d1fa..d8b259e45f467 100644 --- a/packages/x-tree-view/src/internals/models/helpers.ts +++ b/packages/x-tree-view/src/internals/models/helpers.ts @@ -50,4 +50,5 @@ export interface MergePlugins { slotProps: MergePluginsProperty; events: MergePluginsProperty; models: MergePluginsProperty; + experimentalFeatures: MergePluginsProperty; } diff --git a/packages/x-tree-view/src/internals/models/plugin.ts b/packages/x-tree-view/src/internals/models/plugin.ts index 009c43d511f7d..0d85c8b61754d 100644 --- a/packages/x-tree-view/src/internals/models/plugin.ts +++ b/packages/x-tree-view/src/internals/models/plugin.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { EventHandlers } from '@mui/base/utils'; -import { TreeViewModel } from './treeView'; +import { TreeViewExperimentalFeatures, TreeViewInstance, TreeViewModel } from './treeView'; import type { MergePluginsProperty, OptionalIfEmpty } from './helpers'; import { TreeViewEventLookupElement } from './events'; import type { TreeViewCorePluginsSignature } from '../corePlugins'; @@ -12,6 +12,7 @@ export interface TreeViewPluginOptions; slots: TSignature['slots']; slotProps: TSignature['slotProps']; + experimentalFeatures: TreeViewUsedExperimentalFeatures; models: TreeViewUsedModels; setState: React.Dispatch>>; rootRef: React.RefObject; @@ -45,6 +46,7 @@ export type TreeViewPluginSignature< slots?: { [key in keyof T['slots']]: React.ElementType }; slotProps?: { [key in keyof T['slotProps']]: {} | (() => {}) }; modelNames?: keyof T['defaultizedParams']; + experimentalFeatures?: string; dependantPlugins?: readonly TreeViewAnyPluginSignature[]; }, > = { @@ -64,6 +66,7 @@ export type TreeViewPluginSignature< >; } : {}; + experimentalFeatures: T['experimentalFeatures']; dependantPlugins: T extends { dependantPlugins: Array } ? T['dependantPlugins'] : []; }; @@ -78,6 +81,7 @@ export type TreeViewAnyPluginSignature = { slots: any; slotProps: any; models: any; + experimentalFeatures: any; publicAPI: any; }; @@ -105,6 +109,9 @@ export type TreeViewUsedInstance type TreeViewUsedState = TSignature['state'] & MergePluginsProperty, 'state'>; +type TreeViewUsedExperimentalFeatures = + TreeViewExperimentalFeatures<[TSignature, ...TSignature['dependantPlugins']]>; + type RemoveSetValue>> = { [K in keyof Models]: Omit; }; @@ -135,12 +142,16 @@ export type TreeViewItemPlugin = ( options: TreeViewItemPluginOptions, ) => void | TreeViewItemPluginResponse; -export type TreeItemWrapper = (params: { +export type TreeItemWrapper = (params: { itemId: TreeViewItemId; children: React.ReactNode; + instance: TreeViewInstance; }) => React.ReactNode; -export type TreeRootWrapper = (params: { children: React.ReactNode }) => React.ReactNode; +export type TreeRootWrapper = (params: { + children: React.ReactNode; + instance: TreeViewInstance; +}) => React.ReactNode; export type TreeViewPlugin = { (options: TreeViewPluginOptions): TreeViewResponse; @@ -156,11 +167,11 @@ export type TreeViewPlugin = { * @param {{ nodeId: TreeViewItemId; children: React.ReactNode; }} params The params of the item. * @returns {React.ReactNode} The wrapped item. */ - wrapItem?: TreeItemWrapper; + wrapItem?: TreeItemWrapper<[TSignature, ...TSignature['dependantPlugins']]>; /** * 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; + wrapRoot?: TreeRootWrapper<[TSignature, ...TSignature['dependantPlugins']]>; }; diff --git a/packages/x-tree-view/src/internals/models/treeView.ts b/packages/x-tree-view/src/internals/models/treeView.ts index 552dd29bf7e2c..80937aa10b554 100644 --- a/packages/x-tree-view/src/internals/models/treeView.ts +++ b/packages/x-tree-view/src/internals/models/treeView.ts @@ -8,7 +8,11 @@ export interface TreeViewItemMeta { expandable: boolean; disabled: boolean; /** - * Only defined for `RichTreeView`. + * Only defined for `RichTreeView` and `RichTreeViewPro`. + */ + depth?: number; + /** + * Only defined for `RichTreeView` and `RichTreeViewPro`. */ label?: string; } @@ -24,3 +28,7 @@ export type TreeViewInstance = MergePluginsProperty; + +export type TreeViewExperimentalFeatures< + TSignatures extends readonly TreeViewAnyPluginSignature[], +> = { [key in MergePluginsProperty]?: boolean }; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx similarity index 90% rename from packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.ts rename to packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx index 25f4d660487d2..c704fd57fe513 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx @@ -8,6 +8,7 @@ import { import { publishTreeViewEvent } from '../../utils/publishTreeViewEvent'; import { TreeViewBaseItem, TreeViewItemId } from '../../../models'; import { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils'; +import { TreeViewItemDepthContext } from '../../TreeViewItemDepthContext'; interface UpdateNodesStateParameters extends Pick< @@ -28,7 +29,7 @@ const updateItemsState = ({ [TREE_VIEW_ROOT_PARENT_ID]: [], }; - const processItem = (item: TreeViewBaseItem, parentId: string | null) => { + const processItem = (item: TreeViewBaseItem, depth: number, parentId: string | null) => { const id: string = getItemId ? getItemId(item) : (item as any).id; if (id == null) { @@ -71,6 +72,7 @@ const updateItemsState = ({ idAttribute: undefined, expandable: !!item.children?.length, disabled: isItemDisabled ? isItemDisabled(item) : false, + depth, }; itemMap[id] = item; @@ -81,10 +83,10 @@ const updateItemsState = ({ } itemOrderedChildrenIds[parentIdWithDefault].push(id); - item.children?.forEach((child) => processItem(child, id)); + item.children?.forEach((child) => processItem(child, depth + 1, id)); }; - items.forEach((item) => processItem(item, null)); + items.forEach((item) => processItem(item, 0, null)); const itemChildrenIndexes: State['itemChildrenIndexes'] = {}; Object.keys(itemOrderedChildrenIds).forEach((parentId) => { @@ -104,6 +106,7 @@ export const useTreeViewItems: TreeViewPlugin = ({ params, state, setState, + experimentalFeatures, }) => { const getItemMeta = React.useCallback( (itemId: string) => state.items.itemMetaMap[itemId], @@ -233,7 +236,10 @@ export const useTreeViewItems: TreeViewPlugin = ({ preventItemUpdates, areItemUpdatesPrevented, }, - contextValue: { disabledItemsFocusable: params.disabledItemsFocusable }, + contextValue: { + disabledItemsFocusable: params.disabledItemsFocusable, + indentationAtItemLevel: experimentalFeatures.indentationAtItemLevel ?? false, + }, }; }; @@ -251,6 +257,14 @@ useTreeViewItems.getDefaultizedParams = (params) => ({ disabledItemsFocusable: params.disabledItemsFocusable ?? false, }); +useTreeViewItems.wrapRoot = ({ children, instance }) => { + return ( + instance.getItemMeta(itemId)?.depth ?? 0}> + {children} + + ); +}; + useTreeViewItems.params = { disabledItemsFocusable: true, items: 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 index 327907342d612..5a27d1ed9992d 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts @@ -93,7 +93,9 @@ export interface UseTreeViewItemsState { } interface UseTreeViewItemsContextValue - extends Pick, 'disabledItemsFocusable'> {} + extends Pick, 'disabledItemsFocusable'> { + indentationAtItemLevel: boolean; +} export type UseTreeViewItemsSignature = TreeViewPluginSignature<{ params: UseTreeViewItemsParameters; @@ -103,6 +105,7 @@ export type UseTreeViewItemsSignature = TreeViewPluginSignature<{ events: UseTreeViewItemsEventLookup; state: UseTreeViewItemsState; contextValue: UseTreeViewItemsContextValue; + experimentalFeatures: 'indentationAtItemLevel'; }>; export type TreeViewItemMetaMap = { [itemId: string]: TreeViewItemMeta }; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx index e28b703fbed81..a1bac68c02188 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx @@ -17,6 +17,7 @@ import { import type { TreeItemProps } from '../../../TreeItem'; import type { TreeItem2Props } from '../../../TreeItem2'; import { UseTreeViewIdSignature } from '../useTreeViewId'; +import { TreeViewItemDepthContext } from '../../TreeViewItemDepthContext'; export const useTreeViewJSXItems: TreeViewPlugin = ({ instance, @@ -181,12 +182,23 @@ const useTreeViewJSXItemsItemPlugin: TreeViewItemPlugin ( - {children} -); +useTreeViewJSXItems.wrapItem = ({ children, itemId }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const depthContext = React.useContext(TreeViewItemDepthContext); + + return ( + + + {children} + + + ); +}; useTreeViewJSXItems.wrapRoot = ({ children }) => ( - {children} + + {children} + ); useTreeViewJSXItems.params = {}; diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts index 3898b90cc2321..464da635e65e2 100644 --- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts +++ b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts @@ -93,6 +93,7 @@ export const useTreeView = plugin.wrapItem) - .filter((wrapItem): wrapItem is TreeItemWrapper => !!wrapItem); + .filter((wrapItem): wrapItem is TreeItemWrapper => !!wrapItem); contextValue.wrapItem = ({ itemId, children }) => { let finalChildren: React.ReactNode = children; itemWrappers.forEach((itemWrapper) => { - finalChildren = itemWrapper({ itemId, children: finalChildren }); + finalChildren = itemWrapper({ itemId, children: finalChildren, instance }); }); return finalChildren; @@ -160,11 +161,14 @@ export const useTreeView = plugin.wrapRoot) - .filter((wrapRoot): wrapRoot is TreeRootWrapper => !!wrapRoot); + .filter((wrapRoot): wrapRoot is TreeRootWrapper => !!wrapRoot) + // The wrappers are reversed to ensure that the first wrapper is the outermost one. + .reverse(); + contextValue.wrapRoot = ({ children }) => { let finalChildren: React.ReactNode = children; rootWrappers.forEach((rootWrapper) => { - finalChildren = rootWrapper({ children: finalChildren }); + finalChildren = rootWrapper({ children: finalChildren, instance }); }); return finalChildren; diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.types.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.types.ts index 0ff481abccae7..a4221edcf5f46 100644 --- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.types.ts +++ b/packages/x-tree-view/src/internals/useTreeView/useTreeView.types.ts @@ -8,6 +8,7 @@ import { MergePluginsProperty, TreeViewInstance, TreeViewPublicAPI, + TreeViewExperimentalFeatures, } from '../models'; export type UseTreeViewParameters< @@ -25,6 +26,7 @@ export interface UseTreeViewBaseParameters< plugins: TPlugins; slots: MergePluginsProperty, 'slots'>; slotProps: MergePluginsProperty, 'slotProps'>; + experimentalFeatures: TreeViewExperimentalFeatures>; } export type UseTreeViewDefaultizedParameters< diff --git a/packages/x-tree-view/src/internals/utils/extractPluginParamsFromProps.ts b/packages/x-tree-view/src/internals/utils/extractPluginParamsFromProps.ts index 6f0ae94843dbd..2f1f139aeb048 100644 --- a/packages/x-tree-view/src/internals/utils/extractPluginParamsFromProps.ts +++ b/packages/x-tree-view/src/internals/utils/extractPluginParamsFromProps.ts @@ -2,6 +2,7 @@ import * as React from 'react'; import { ConvertPluginsIntoSignatures, MergePluginsProperty, + TreeViewExperimentalFeatures, TreeViewPlugin, TreeViewPublicAPI, } from '../models'; @@ -17,9 +18,10 @@ export const extractPluginParamsFromProps = < apiRef?: React.MutableRefObject< TreeViewPublicAPI> | undefined >; + experimentalFeatures?: TreeViewExperimentalFeatures>; }, >({ - props: { slots, slotProps, apiRef, ...props }, + props: { slots, slotProps, apiRef, experimentalFeatures, ...props }, plugins, rootRef, }: { @@ -39,6 +41,7 @@ export const extractPluginParamsFromProps = < rootRef, slots: slots ?? {}, slotProps: slotProps ?? {}, + experimentalFeatures: experimentalFeatures ?? {}, apiRef, } as UseTreeViewBaseParameters & PluginParams; const otherProps = {} as Omit; diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts index 047bf9660b52f..7efc38056a9d4 100644 --- a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts +++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts @@ -15,6 +15,7 @@ import { useTreeViewContext } from '../internals/TreeViewProvider/useTreeViewCon import { DefaultTreeViewPlugins } from '../internals/plugins/defaultPlugins'; import { MuiCancellableEvent } from '../internals/models/MuiCancellableEvent'; import { useTreeItem2Utils } from '../hooks/useTreeItem2Utils'; +import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext'; export const useTreeItem2 = ( parameters: UseTreeItem2Parameters, @@ -23,9 +24,11 @@ export const useTreeItem2 = (); + const depthContext = React.useContext(TreeViewItemDepthContext); const { id, itemId, label, children, rootRef } = parameters; @@ -134,7 +137,7 @@ export const useTreeItem2 = = { ...externalEventHandlers, ref: handleRootRef, role: 'treeitem', @@ -148,6 +151,15 @@ export const useTreeItem2 = = {}>( @@ -155,7 +167,7 @@ export const useTreeItem2 = => { const externalEventHandlers = extractEventHandlers(externalProps); - return { + const response: UseTreeItem2ContentSlotProps = { ...externalEventHandlers, ...externalProps, ref: contentRef, @@ -163,6 +175,12 @@ export const useTreeItem2 = = {}>( @@ -213,7 +231,7 @@ export const useTreeItem2 = => { const externalEventHandlers = extractEventHandlers(externalProps); - return { + const response: UseTreeItem2GroupTransitionSlotProps = { ...externalEventHandlers, unmountOnExit: true, component: 'ul', @@ -222,6 +240,12 @@ export const useTreeItem2 = >; onKeyDown: MuiCancellableEventHandler>; ref: React.RefCallback; + /** + * Only defined when the `indentationAtItemLevel` experimental feature is enabled. + */ + style?: React.CSSProperties; } export type UseTreeItem2RootSlotProps = ExternalProps & @@ -50,6 +54,10 @@ export interface UseTreeItem2ContentSlotOwnProps { onMouseDown: MuiCancellableEventHandler; ref: React.RefCallback | null; status: UseTreeItem2Status; + /** + * Only defined when the `indentationAtItemLevel` experimental feature is enabled. + */ + indentationAtItemLevel?: true; } export type UseTreeItem2ContentSlotProps = ExternalProps & @@ -85,6 +93,10 @@ export interface UseTreeItem2GroupTransitionSlotOwnProps { component: 'ul'; role: 'group'; children: React.ReactNode; + /** + * Only defined when the `indentationAtItemLevel` experimental feature is enabled. + */ + indentationAtItemLevel?: true; } export type UseTreeItem2GroupTransitionSlotProps = ExternalProps &