From bd52c2d9d4e5ad90a999f57b00472f0124e0ad1c Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Mon, 4 Mar 2024 08:51:58 +0100 Subject: [PATCH] [TreeView] New instance and publicAPI method: `getItem` (#12251) --- .../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 +- .../src/RichTreeView/RichTreeView.tsx | 1 + .../src/SimpleTreeView/SimpleTreeView.tsx | 1 + .../x-tree-view/src/TreeView/TreeView.tsx | 1 + .../src/hooks/useTreeViewApiRef.tsx | 8 ++-- .../useTreeViewFocus.types.ts | 5 +-- .../useTreeViewJSXNodes.tsx | 24 +++++++--- .../useTreeViewNodes/useTreeViewNodes.ts | 44 ++++++++++++++----- .../useTreeViewNodes.types.ts | 22 +++++++--- 11 files changed, 85 insertions(+), 33 deletions(-) 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 ab76e0f2953bf..3ef550f36821a 100644 --- a/docs/pages/x/api/tree-view/rich-tree-view.json +++ b/docs/pages/x/api/tree-view/rich-tree-view.json @@ -1,6 +1,8 @@ { "props": { - "apiRef": { "type": { "name": "shape", "description": "{ current?: { focusNode: func } }" } }, + "apiRef": { + "type": { "name": "shape", "description": "{ current?: { focusNode: func, getItem: func } }" } + }, "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, "defaultExpandedNodes": { "type": { "name": "arrayOf", "description": "Array<string>" }, 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 10a3eae04caab..2f6d68a24e5fa 100644 --- a/docs/pages/x/api/tree-view/simple-tree-view.json +++ b/docs/pages/x/api/tree-view/simple-tree-view.json @@ -1,6 +1,8 @@ { "props": { - "apiRef": { "type": { "name": "shape", "description": "{ current?: { focusNode: func } }" } }, + "apiRef": { + "type": { "name": "shape", "description": "{ current?: { focusNode: func, getItem: func } }" } + }, "children": { "type": { "name": "node" } }, "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, "defaultExpandedNodes": { diff --git a/docs/pages/x/api/tree-view/tree-view.json b/docs/pages/x/api/tree-view/tree-view.json index d15094b303d86..40d3c60828130 100644 --- a/docs/pages/x/api/tree-view/tree-view.json +++ b/docs/pages/x/api/tree-view/tree-view.json @@ -1,6 +1,8 @@ { "props": { - "apiRef": { "type": { "name": "shape", "description": "{ current?: { focusNode: func } }" } }, + "apiRef": { + "type": { "name": "shape", "description": "{ current?: { focusNode: func, getItem: func } }" } + }, "children": { "type": { "name": "node" } }, "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, "defaultExpandedNodes": { diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx index c05701e690841..d8fb2697b558a 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx @@ -152,6 +152,7 @@ RichTreeView.propTypes = { apiRef: PropTypes.shape({ current: PropTypes.shape({ focusNode: PropTypes.func.isRequired, + getItem: PropTypes.func.isRequired, }), }), /** diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx index 7a00d05f19bda..55c1be134b82a 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx @@ -115,6 +115,7 @@ SimpleTreeView.propTypes = { apiRef: PropTypes.shape({ current: PropTypes.shape({ focusNode: PropTypes.func.isRequired, + getItem: PropTypes.func.isRequired, }), }), /** diff --git a/packages/x-tree-view/src/TreeView/TreeView.tsx b/packages/x-tree-view/src/TreeView/TreeView.tsx index 814b58c494ae6..c6d7982da8b64 100644 --- a/packages/x-tree-view/src/TreeView/TreeView.tsx +++ b/packages/x-tree-view/src/TreeView/TreeView.tsx @@ -92,6 +92,7 @@ TreeView.propTypes = { apiRef: PropTypes.shape({ current: PropTypes.shape({ focusNode: PropTypes.func.isRequired, + getItem: PropTypes.func.isRequired, }), }), /** diff --git a/packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx b/packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx index f003fe5f124bb..8ef984c577967 100644 --- a/packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx +++ b/packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; -import { TreeViewAnyPluginSignature, TreeViewUsedPublicAPI } from '../internals/models'; +import { TreeViewAnyPluginSignature, TreeViewPublicAPI } from '../internals/models'; +import { DefaultTreeViewPlugins } from '../internals'; /** * Hook that instantiates a [[TreeViewApiRef]]. */ export const useTreeViewApiRef = < - T extends TreeViewAnyPluginSignature, - Api extends TreeViewUsedPublicAPI, ->() => React.useRef(undefined) as React.MutableRefObject; + TPlugins extends readonly TreeViewAnyPluginSignature[] = DefaultTreeViewPlugins, +>() => React.useRef(undefined) as React.MutableRefObject | undefined>; 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 index 2bb293715f34d..eaa26c0b3585b 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts @@ -11,9 +11,8 @@ export interface UseTreeViewFocusInstance { focusDefaultNode: (event: React.SyntheticEvent) => void; focusRoot: () => void; } -export interface UseTreeViewFocusPublicAPI { - focusNode: (event: React.SyntheticEvent, nodeId: string | null) => void; -} + +export interface UseTreeViewFocusPublicAPI extends Pick {} export interface UseTreeViewFocusParameters { /** diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.tsx index 518ca63bd678d..fda563de2b3f1 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.tsx @@ -18,7 +18,7 @@ export const useTreeViewJSXNodes: TreeViewPlugin = }) => { const insertJSXNode = useEventCallback((node: TreeViewNode) => { setState((prevState) => { - if (prevState.nodeMap[node.id] != null) { + if (prevState.nodes.nodeMap[node.id] != null) { throw new Error( [ 'MUI X: The Tree View component requires all items to have a unique `id` property.', @@ -28,17 +28,31 @@ export const useTreeViewJSXNodes: TreeViewPlugin = ); } - return { ...prevState, nodeMap: { ...prevState.nodeMap, [node.id]: node } }; + return { + ...prevState, + nodes: { + ...prevState.nodes, + nodeMap: { ...prevState.nodes.nodeMap, [node.id]: node }, + // For `SimpleTreeView`, we don't have a proper `item` object, so we create a very basic one. + itemMap: { ...prevState.nodes.itemMap, [node.id]: { id: node.id, label: node.label } }, + }, + }; }); }); const removeJSXNode = useEventCallback((nodeId: string) => { setState((prevState) => { - const newMap = { ...prevState.nodeMap }; - delete newMap[nodeId]; + const newNodeMap = { ...prevState.nodes.nodeMap }; + const newItemMap = { ...prevState.nodes.itemMap }; + delete newNodeMap[nodeId]; + delete newItemMap[nodeId]; return { ...prevState, - nodeMap: newMap, + nodes: { + ...prevState.nodes, + nodeMap: newNodeMap, + itemMap: newItemMap, + }, }; }); publishTreeViewEvent(instance, 'removeNode', { id: nodeId }); 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 43aef3ff53a2e..718170a878aeb 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts @@ -1,18 +1,19 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { TreeViewPlugin } from '../../models'; -import { populateInstance } from '../../useTreeView/useTreeView.utils'; +import { populateInstance, populatePublicAPI } from '../../useTreeView/useTreeView.utils'; import { UseTreeViewNodesSignature, UseTreeViewNodesDefaultizedParameters, TreeViewNodeMap, TreeViewNodeIdAndChildren, UseTreeViewNodesState, + TreeViewItemMap, } from './useTreeViewNodes.types'; import { publishTreeViewEvent } from '../../utils/publishTreeViewEvent'; import { TreeViewBaseItem } from '../../../models'; -const updateState = ({ +const updateNodesState = ({ items, isItemDisabled, getItemLabel, @@ -20,8 +21,9 @@ const updateState = ({ }: Pick< UseTreeViewNodesDefaultizedParameters, 'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemId' ->): UseTreeViewNodesState => { +>): UseTreeViewNodesState['nodes'] => { const nodeMap: TreeViewNodeMap = {}; + const itemMap: TreeViewItemMap = {}; const processItem = ( item: TreeViewBaseItem, @@ -73,6 +75,8 @@ const updateState = ({ disabled: isItemDisabled ? isItemDisabled(item) : false, }; + itemMap[id] = item; + return { id, children: item.children?.map((child, childIndex) => processItem(child, childIndex, id)), @@ -84,16 +88,26 @@ const updateState = ({ return { nodeMap, nodeTree, + itemMap, }; }; export const useTreeViewNodes: TreeViewPlugin = ({ instance, + publicAPI, params, state, setState, }) => { - const getNode = React.useCallback((nodeId: string) => state.nodeMap[nodeId], [state.nodeMap]); + const getNode = React.useCallback( + (nodeId: string) => state.nodes.nodeMap[nodeId], + [state.nodes.nodeMap], + ); + + const getItem = React.useCallback( + (nodeId: string) => state.nodes.itemMap[nodeId], + [state.nodes.itemMap], + ); const isNodeDisabled = React.useCallback( (nodeId: string | null): nodeId is string => { @@ -125,7 +139,7 @@ export const useTreeViewNodes: TreeViewPlugin = ({ ); const getChildrenIds = useEventCallback((nodeId: string | null) => - Object.values(state.nodeMap) + Object.values(state.nodes.nodeMap) .filter((node) => node.parentId === nodeId) .sort((a, b) => a.index - b.index) .map((child) => child.id), @@ -142,14 +156,14 @@ export const useTreeViewNodes: TreeViewPlugin = ({ React.useEffect(() => { setState((prevState) => { - const newState = updateState({ + const newState = updateNodesState({ items: params.items, isItemDisabled: params.isItemDisabled, getItemId: params.getItemId, getItemLabel: params.getItemLabel, }); - Object.values(prevState.nodeMap).forEach((node) => { + Object.values(prevState.nodes.nodeMap).forEach((node) => { if (!newState.nodeMap[node.id]) { publishTreeViewEvent(instance, 'removeNode', { id: node.id }); } @@ -171,7 +185,7 @@ export const useTreeViewNodes: TreeViewPlugin = ({ id, children, }: TreeViewNodeIdAndChildren): ReturnType[number] => { - const node = state.nodeMap[id]; + const node = state.nodes.nodeMap[id]; return { label: node.label!, nodeId: node.id, @@ -180,29 +194,35 @@ export const useTreeViewNodes: TreeViewPlugin = ({ }; }; - return state.nodeTree.map(getPropsFromNodeId); + return state.nodes.nodeTree.map(getPropsFromNodeId); }); populateInstance(instance, { getNode, + getItem, getNodesToRender, getChildrenIds, getNavigableChildrenIds, isNodeDisabled, }); + populatePublicAPI(publicAPI, { + getItem, + }); + return { contextValue: { disabledItemsFocusable: params.disabledItemsFocusable }, }; }; -useTreeViewNodes.getInitialState = (params) => - updateState({ +useTreeViewNodes.getInitialState = (params) => ({ + nodes: updateNodesState({ items: params.items, isItemDisabled: params.isItemDisabled, getItemId: params.getItemId, getItemLabel: params.getItemLabel, - }); + }), +}); useTreeViewNodes.getDefaultizedParams = (params) => ({ ...params, diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.types.ts index cc1bb0a3c435f..11fddd41fe577 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.types.ts @@ -8,14 +8,18 @@ interface TreeViewNodeProps { children?: TreeViewNodeProps[]; } -export interface UseTreeViewNodesInstance { +export interface UseTreeViewNodesInstance { getNode: (nodeId: string) => TreeViewNode; + getItem: (nodeId: string) => R; getNodesToRender: () => TreeViewNodeProps[]; getChildrenIds: (nodeId: string | null) => string[]; getNavigableChildrenIds: (nodeId: string | null) => string[]; isNodeDisabled: (nodeId: string | null) => nodeId is string; } +export interface UseTreeViewNodesPublicAPI + extends Pick, 'getItem'> {} + export interface UseTreeViewNodesParameters { /** * If `true`, will allow focus on disabled items. @@ -66,9 +70,12 @@ export interface TreeViewNodeIdAndChildren { children?: TreeViewNodeIdAndChildren[]; } -export interface UseTreeViewNodesState { - nodeTree: TreeViewNodeIdAndChildren[]; - nodeMap: TreeViewNodeMap; +export interface UseTreeViewNodesState { + nodes: { + nodeTree: TreeViewNodeIdAndChildren[]; + nodeMap: TreeViewNodeMap; + itemMap: TreeViewItemMap; + }; } interface UseTreeViewNodesContextValue @@ -77,10 +84,13 @@ interface UseTreeViewNodesContextValue export type UseTreeViewNodesSignature = TreeViewPluginSignature<{ params: UseTreeViewNodesParameters; defaultizedParams: UseTreeViewNodesDefaultizedParameters; - instance: UseTreeViewNodesInstance; + instance: UseTreeViewNodesInstance; + publicAPI: UseTreeViewNodesPublicAPI; events: UseTreeViewNodesEventLookup; - state: UseTreeViewNodesState; + state: UseTreeViewNodesState; contextValue: UseTreeViewNodesContextValue; }>; export type TreeViewNodeMap = { [nodeId: string]: TreeViewNode }; + +export type TreeViewItemMap = { [nodeId: string]: R };