diff --git a/docs/data/tree-view/headless/HeadlessTreeView.js b/docs/data/tree-view/headless/HeadlessTreeView.js new file mode 100644 index 0000000000000..4bb4a3051e43f --- /dev/null +++ b/docs/data/tree-view/headless/HeadlessTreeView.js @@ -0,0 +1,126 @@ +import * as React from 'react'; +import { styled, useThemeProps } from '@mui/material/styles'; +import { useSlotProps } from '@mui/base/utils'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +// eslint-disable-next-line +import { DEFAULT_TREE_VIEW_PLUGINS } from '@mui/x-tree-view/internals/plugins/defaultPlugins'; +// eslint-disable-next-line + +import { useTreeView } from '@mui/x-tree-view/internals/useTreeView'; +import { TreeViewProvider } from '@mui/x-tree-view/internals/TreeViewProvider'; + +import { TreeItem } from '@mui/x-tree-view/TreeItem'; + +const useTreeViewLogExpanded = ({ params, models }) => { + React.useEffect(() => { + if (params.log) { + const log = console.log; + log('Expanded items: ', models.expanded.value); + } + }, [models.expanded.value, params.log]); +}; + +// Sets the default value of this plugin parameters. +useTreeViewLogExpanded.getDefaultizedParams = (params) => ({ + ...params, + log: false, +}); + +// This could be exported from the package in the future +const TreeViewRoot = styled('ul', { + name: 'MuiTreeView', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})({ + padding: 0, + margin: 0, + listStyle: 'none', + outline: 0, +}); + +const plugins = [...DEFAULT_TREE_VIEW_PLUGINS, useTreeViewLogExpanded]; + +function TreeView(inProps) { + const themeProps = useThemeProps({ props: inProps, name: 'MuiTreeView' }); + const ownerState = themeProps; + + const { + // Headless implementation + disabledItemsFocusable, + expanded, + defaultExpanded, + onNodeToggle, + onNodeFocus, + disableSelection, + defaultSelected, + selected, + multiSelect, + onNodeSelect, + id, + defaultCollapseIcon, + defaultEndIcon, + defaultExpandIcon, + defaultParentIcon, + log, + // Component implementation + children, + ...other + } = themeProps; + + const { getRootProps, contextValue } = useTreeView({ + disabledItemsFocusable, + expanded, + defaultExpanded, + onNodeToggle, + onNodeFocus, + disableSelection, + defaultSelected, + selected, + multiSelect, + onNodeSelect, + id, + defaultCollapseIcon, + defaultEndIcon, + defaultExpandIcon, + defaultParentIcon, + log, + plugins, + }); + + const rootProps = useSlotProps({ + elementType: TreeViewRoot, + externalSlotProps: {}, + externalForwardedProps: other, + getSlotProps: getRootProps, + ownerState, + }); + + return ( + + {children} + + ); +} + +export default function HeadlessTreeView() { + return ( + } + defaultExpandIcon={} + sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} + > + + + + + + + + + + + ); +} diff --git a/docs/data/tree-view/headless/HeadlessTreeView.tsx b/docs/data/tree-view/headless/HeadlessTreeView.tsx new file mode 100644 index 0000000000000..961f4d305cf3d --- /dev/null +++ b/docs/data/tree-view/headless/HeadlessTreeView.tsx @@ -0,0 +1,166 @@ +import * as React from 'react'; +import { styled, useThemeProps } from '@mui/material/styles'; +import { useSlotProps } from '@mui/base/utils'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +// eslint-disable-next-line +import { + DefaultTreeViewPluginParameters, + DEFAULT_TREE_VIEW_PLUGINS, +} from '@mui/x-tree-view/internals/plugins/defaultPlugins'; +// eslint-disable-next-line +import { UseTreeViewExpansionSignature } from '@mui/x-tree-view/internals/plugins/useTreeViewExpansion'; +import { useTreeView } from '@mui/x-tree-view/internals/useTreeView'; +import { TreeViewProvider } from '@mui/x-tree-view/internals/TreeViewProvider'; +import { TreeViewPropsBase } from '@mui/x-tree-view/TreeView'; +import { TreeItem } from '@mui/x-tree-view/TreeItem'; +import { + TreeViewPlugin, + TreeViewPluginSignature, +} from '@mui/x-tree-view/internals/models'; + +interface TreeViewLogExpandedParameters { + log?: boolean; +} + +interface TreeViewLogExpandedDefaultizedParameters { + log: boolean; +} + +type TreeViewLogExpandedSignature = TreeViewPluginSignature< + // The parameters of this plugin as they are passed to `usedTreeView` + TreeViewLogExpandedParameters, + // The parameters of this plugin as they are passed to the plugin after calling `plugin.getDefaultizedParams` + TreeViewLogExpandedDefaultizedParameters, + // Instance methods of this plugin: we don't have any + {}, + // State of this plugin: we don't have any + {}, + // Models of this plugin: we don't have any + never, + // Dependencies of this plugin (we need the expansion plugin to access its model) + [UseTreeViewExpansionSignature] +>; + +const useTreeViewLogExpanded: TreeViewPlugin = ({ + params, + models, +}) => { + React.useEffect(() => { + if (params.log) { + const log = console.log; + log('Expanded items: ', models.expanded.value); + } + }, [models.expanded.value, params.log]); +}; + +// Sets the default value of this plugin parameters. +useTreeViewLogExpanded.getDefaultizedParams = (params) => ({ + ...params, + log: false, +}); + +// This could be exported from the package in the future +const TreeViewRoot = styled('ul', { + name: 'MuiTreeView', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})<{ ownerState: TreeViewProps }>({ + padding: 0, + margin: 0, + listStyle: 'none', + outline: 0, +}); + +export interface TreeViewProps + extends DefaultTreeViewPluginParameters, + TreeViewLogExpandedParameters, + TreeViewPropsBase {} + +const plugins = [...DEFAULT_TREE_VIEW_PLUGINS, useTreeViewLogExpanded] as const; + +function TreeView( + inProps: TreeViewProps, +) { + const themeProps = useThemeProps({ props: inProps, name: 'MuiTreeView' }); + const ownerState = themeProps as TreeViewProps; + + const { + // Headless implementation + disabledItemsFocusable, + expanded, + defaultExpanded, + onNodeToggle, + onNodeFocus, + disableSelection, + defaultSelected, + selected, + multiSelect, + onNodeSelect, + id, + defaultCollapseIcon, + defaultEndIcon, + defaultExpandIcon, + defaultParentIcon, + log, + // Component implementation + children, + ...other + } = themeProps as TreeViewProps; + + const { getRootProps, contextValue } = useTreeView({ + disabledItemsFocusable, + expanded, + defaultExpanded, + onNodeToggle, + onNodeFocus, + disableSelection, + defaultSelected, + selected, + multiSelect, + onNodeSelect, + id, + defaultCollapseIcon, + defaultEndIcon, + defaultExpandIcon, + defaultParentIcon, + log, + plugins, + }); + + const rootProps = useSlotProps({ + elementType: TreeViewRoot, + externalSlotProps: {}, + externalForwardedProps: other, + getSlotProps: getRootProps, + ownerState, + }); + + return ( + + {children} + + ); +} + +export default function HeadlessTreeView() { + return ( + } + defaultExpandIcon={} + sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }} + > + + + + + + + + + + + ); +} diff --git a/docs/data/tree-view/headless/headless.md b/docs/data/tree-view/headless/headless.md new file mode 100644 index 0000000000000..61a08edb5b334 --- /dev/null +++ b/docs/data/tree-view/headless/headless.md @@ -0,0 +1,14 @@ +--- +productId: material-ui +title: Tree View Headless usage +githubLabel: 'component: tree view' +packageName: '@mui/x-tree-view' +--- + +# Tree View - Headless + +

Experiment with headless tree view

+ +Open the console and interact with the tree view to see the expanded items being logged: + +{{"demo": "HeadlessTreeView.js"}} diff --git a/docs/pages/x/react-tree-view/experiments/headless.js b/docs/pages/x/react-tree-view/experiments/headless.js new file mode 100644 index 0000000000000..6d0bad999762a --- /dev/null +++ b/docs/pages/x/react-tree-view/experiments/headless.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docsx/data/tree-view/headless/headless.md?@mui/markdown'; + +export default function Page() { + return ; +} diff --git a/packages/x-tree-view/src/internals/models/plugin.ts b/packages/x-tree-view/src/internals/models/plugin.ts index 4bbc15b61603c..e11c7d54122f4 100644 --- a/packages/x-tree-view/src/internals/models/plugin.ts +++ b/packages/x-tree-view/src/internals/models/plugin.ts @@ -4,10 +4,9 @@ import { TreeViewModel } from './treeView'; import type { TreeViewContextValue } from '../TreeViewProvider'; import type { MergePluginsProperty } from './helpers'; -export interface TreeViewPluginParams { +export interface TreeViewPluginOptions { instance: TreeViewUsedInstance; - // TODO: Rename 'params' - props: TreeViewUsedDefaultizedParams; + params: TreeViewUsedDefaultizedParams; state: TreeViewUsedState; models: TreeViewUsedModels; setState: React.Dispatch>>; @@ -36,16 +35,16 @@ export type TreeViewPluginSignature< TModelNames extends keyof TDefaultizedParams, TDependantPlugins extends readonly TreeViewAnyPluginSignature[], > = { - state: TState; - instance: TInstance; params: TParams; defaultizedParams: TDefaultizedParams; - dependantPlugins: TDependantPlugins; + instance: TInstance; + state: TState; models: { [TControlled in TModelNames]-?: TreeViewModel< Exclude >; }; + dependantPlugins: TDependantPlugins; }; export type TreeViewAnyPluginSignature = { @@ -74,7 +73,7 @@ export type TreeViewUsedModels = TSignature['models'] & MergePluginsProperty; export type TreeViewPlugin = { - (options: TreeViewPluginParams): void | TreeViewResponse; + (options: TreeViewPluginOptions): void | TreeViewResponse; getDefaultizedParams?: ( params: TreeViewUsedParams, ) => TSignature['defaultizedParams']; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewContextValueBuilder/useTreeViewContextValueBuilder.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewContextValueBuilder/useTreeViewContextValueBuilder.ts index e45e2aacf9326..a0e27dfa8c356 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewContextValueBuilder/useTreeViewContextValueBuilder.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewContextValueBuilder/useTreeViewContextValueBuilder.ts @@ -4,8 +4,8 @@ import { UseTreeViewContextValueBuilderSignature } from './useTreeViewContextVal export const useTreeViewContextValueBuilder: TreeViewPlugin< UseTreeViewContextValueBuilderSignature -> = ({ instance, props }) => { - const treeId = useId(props.id); +> = ({ instance, params }) => { + const treeId = useId(params.id); return { getRootProps: () => ({ @@ -14,13 +14,13 @@ export const useTreeViewContextValueBuilder: TreeViewPlugin< contextValue: { treeId, instance: instance as TreeViewInstance, - multiSelect: props.multiSelect, - disabledItemsFocusable: props.disabledItemsFocusable, + multiSelect: params.multiSelect, + disabledItemsFocusable: params.disabledItemsFocusable, icons: { - defaultCollapseIcon: props.defaultCollapseIcon, - defaultEndIcon: props.defaultEndIcon, - defaultExpandIcon: props.defaultExpandIcon, - defaultParentIcon: props.defaultParentIcon, + defaultCollapseIcon: params.defaultCollapseIcon, + defaultEndIcon: params.defaultEndIcon, + defaultExpandIcon: params.defaultExpandIcon, + defaultParentIcon: params.defaultParentIcon, }, }, }; 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 ece8cc6e68717..2d94779186fd7 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts @@ -6,7 +6,7 @@ import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types'; export const useTreeViewExpansion: TreeViewPlugin = ({ instance, - props, + params, models, }) => { const isNodeExpanded = React.useCallback( @@ -37,8 +37,8 @@ export const useTreeViewExpansion: TreeViewPlugin newExpanded = [nodeId].concat(models.expanded.value); } - if (props.onNodeToggle) { - props.onNodeToggle(event, newExpanded); + if (params.onNodeToggle) { + params.onNodeToggle(event, newExpanded); } models.expanded.setValue(newExpanded); @@ -58,8 +58,8 @@ export const useTreeViewExpansion: TreeViewPlugin if (diff.length > 0) { models.expanded.setValue(newExpanded); - if (props.onNodeToggle) { - props.onNodeToggle(event, newExpanded); + if (params.onNodeToggle) { + params.onNodeToggle(event, newExpanded); } } }; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts index 98a6cc7baac93..b6ac0bd2c7070 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts @@ -7,7 +7,7 @@ import { UseTreeViewFocusSignature } from './useTreeViewFocus.types'; export const useTreeViewFocus: TreeViewPlugin = ({ instance, - props, + params, state, setState, models, @@ -26,8 +26,8 @@ export const useTreeViewFocus: TreeViewPlugin = ({ if (nodeId) { setFocusedNodeId(nodeId); - if (props.onNodeFocus) { - props.onNodeFocus(event, nodeId); + if (params.onNodeFocus) { + params.onNodeFocus(event, nodeId); } } }); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts index 3187c639b1e5c..71baf770a370e 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts @@ -27,7 +27,7 @@ function findNextFirstChar(firstChars: string[], startIndex: number, char: strin export const useTreeViewKeyboardNavigation: TreeViewPlugin< UseTreeViewKeyboardNavigationSignature -> = ({ instance, props, state }) => { +> = ({ instance, params, state }) => { const theme = useTheme(); const isRtl = theme.direction === 'rtl'; const firstCharMap = React.useRef<{ [nodeId: string]: string }>({}); @@ -93,7 +93,7 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< Object.keys(firstCharMap.current).forEach((mapNodeId) => { const map = instance.getNode(mapNodeId); const visible = map.parentId ? instance.isNodeExpanded(map.parentId) : true; - const shouldBeSkipped = props.disabledItemsFocusable + const shouldBeSkipped = params.disabledItemsFocusable ? false : instance.isNodeDisabled(mapNodeId); @@ -164,11 +164,11 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< const ctrlPressed = event.ctrlKey || event.metaKey; switch (key) { case ' ': - if (!props.disableSelection && !instance.isNodeDisabled(state.focusedNodeId)) { + if (!params.disableSelection && !instance.isNodeDisabled(state.focusedNodeId)) { flag = true; - if (props.multiSelect && event.shiftKey) { + if (params.multiSelect && event.shiftKey) { instance.selectRange(event, { end: state.focusedNodeId }); - } else if (props.multiSelect) { + } else if (params.multiSelect) { instance.selectNode(event, state.focusedNodeId, true); } else { instance.selectNode(event, state.focusedNodeId); @@ -181,9 +181,9 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< if (instance.isNodeExpandable(state.focusedNodeId)) { instance.toggleNodeExpansion(event, state.focusedNodeId); flag = true; - } else if (!props.disableSelection) { + } else if (!params.disableSelection) { flag = true; - if (props.multiSelect) { + if (params.multiSelect) { instance.selectNode(event, state.focusedNodeId, true); } else { instance.selectNode(event, state.focusedNodeId); @@ -193,14 +193,14 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< event.stopPropagation(); break; case 'ArrowDown': - if (props.multiSelect && event.shiftKey && !props.disableSelection) { + if (params.multiSelect && event.shiftKey && !params.disableSelection) { selectNextNode(event, state.focusedNodeId); } instance.focusNode(event, getNextNode(instance, state.focusedNodeId)); flag = true; break; case 'ArrowUp': - if (props.multiSelect && event.shiftKey && !props.disableSelection) { + if (params.multiSelect && event.shiftKey && !params.disableSelection) { selectPreviousNode(event, state.focusedNodeId); } instance.focusNode(event, getPreviousNode(instance, state.focusedNodeId)); @@ -222,10 +222,10 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< break; case 'Home': if ( - props.multiSelect && + params.multiSelect && ctrlPressed && event.shiftKey && - !props.disableSelection && + !params.disableSelection && !instance.isNodeDisabled(state.focusedNodeId) ) { instance.rangeSelectToFirst(event, state.focusedNodeId); @@ -235,10 +235,10 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< break; case 'End': if ( - props.multiSelect && + params.multiSelect && ctrlPressed && event.shiftKey && - !props.disableSelection && + !params.disableSelection && !instance.isNodeDisabled(state.focusedNodeId) ) { instance.rangeSelectToLast(event, state.focusedNodeId); @@ -251,10 +251,10 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< instance.expandAllSiblings(event, state.focusedNodeId); flag = true; } else if ( - props.multiSelect && + params.multiSelect && ctrlPressed && key.toLowerCase() === 'a' && - !props.disableSelection + !params.disableSelection ) { instance.selectRange(event, { start: getFirstNode(instance), diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts index b993aaeae030f..e356bc5fa7a2b 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts @@ -7,7 +7,7 @@ import { UseTreeViewNodesSignature } from './useTreeViewNodes.types'; export const useTreeViewNodes: TreeViewPlugin = ({ instance, - props, + params, rootRef, }) => { const nodeMap = React.useRef<{ [nodeId: string]: TreeViewNode }>({}); @@ -51,7 +51,7 @@ export const useTreeViewNodes: TreeViewPlugin = ({ const getNavigableChildrenIds = (nodeId: string | null) => { let childrenIds = instance.getChildrenIds(nodeId); - if (!props.disabledItemsFocusable) { + if (!params.disabledItemsFocusable) { childrenIds = childrenIds.filter((node) => !instance.isNodeDisabled(node)); } return childrenIds; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts index 11b3670c40b69..ce634bbecc694 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts @@ -14,7 +14,7 @@ import { findOrderInTremauxTree } from './useTreeViewSelection.utils'; export const useTreeViewSelection: TreeViewPlugin> = ({ instance, - props, + params, models, }) => { const lastSelectedNode = React.useRef(null); @@ -27,7 +27,7 @@ export const useTreeViewSelection: TreeViewPlugin { - if (props.disableSelection) { + if (params.disableSelection) { return; } @@ -40,8 +40,8 @@ export const useTreeViewSelection: TreeViewPlugin['onNodeSelect'])!( + if (params.onNodeSelect) { + (params.onNodeSelect as UseTreeViewSelectionDefaultizedParameters['onNodeSelect'])!( event, newSelected, ); @@ -50,10 +50,10 @@ export const useTreeViewSelection: TreeViewPlugin['onNodeSelect'])!( + if (params.onNodeSelect) { + (params.onNodeSelect as UseTreeViewSelectionDefaultizedParameters['onNodeSelect'])!( event, base, ); @@ -131,8 +131,8 @@ export const useTreeViewSelection: TreeViewPlugin newSelected.indexOf(id) === i); - if (props.onNodeSelect) { - (props.onNodeSelect as UseTreeViewSelectionDefaultizedParameters['onNodeSelect'])!( + if (params.onNodeSelect) { + (params.onNodeSelect as UseTreeViewSelectionDefaultizedParameters['onNodeSelect'])!( event, newSelected, ); @@ -142,7 +142,7 @@ export const useTreeViewSelection: TreeViewPlugin { - if (props.disableSelection) { + if (params.disableSelection) { return; } @@ -191,7 +191,7 @@ export const useTreeViewSelection: TreeViewPlugin ({ - 'aria-multiselectable': props.multiSelect, + 'aria-multiselectable': params.multiSelect, }), }; }; diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts index d191d0e9121a7..4921e9dce76f5 100644 --- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts +++ b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts @@ -63,7 +63,7 @@ export const useTreeView = ) => { const pluginResponse = - plugin({ instance, props: params, state, setState, rootRef: innerRootRef, models }) || {}; + plugin({ instance, params: params, state, setState, rootRef: innerRootRef, models }) || {}; if (pluginResponse.getRootProps) { rootPropsGetters.push(pluginResponse.getRootProps);