Skip to content

Commit

Permalink
[TreeView] New instance and publicAPI method: getItem (#12251)
Browse files Browse the repository at this point in the history
  • Loading branch information
flaviendelangle authored Mar 4, 2024
1 parent f9f60fd commit c44d71a
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 33 deletions.
4 changes: 3 additions & 1 deletion docs/pages/x/api/tree-view/rich-tree-view.json
Original file line number Diff line number Diff line change
@@ -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>" },
Expand Down
4 changes: 3 additions & 1 deletion docs/pages/x/api/tree-view/simple-tree-view.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
4 changes: 3 additions & 1 deletion docs/pages/x/api/tree-view/tree-view.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
1 change: 1 addition & 0 deletions packages/x-tree-view/src/RichTreeView/RichTreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ RichTreeView.propTypes = {
apiRef: PropTypes.shape({
current: PropTypes.shape({
focusNode: PropTypes.func.isRequired,
getItem: PropTypes.func.isRequired,
}),
}),
/**
Expand Down
1 change: 1 addition & 0 deletions packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ SimpleTreeView.propTypes = {
apiRef: PropTypes.shape({
current: PropTypes.shape({
focusNode: PropTypes.func.isRequired,
getItem: PropTypes.func.isRequired,
}),
}),
/**
Expand Down
1 change: 1 addition & 0 deletions packages/x-tree-view/src/TreeView/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ TreeView.propTypes = {
apiRef: PropTypes.shape({
current: PropTypes.shape({
focusNode: PropTypes.func.isRequired,
getItem: PropTypes.func.isRequired,
}),
}),
/**
Expand Down
8 changes: 4 additions & 4 deletions packages/x-tree-view/src/hooks/useTreeViewApiRef.tsx
Original file line number Diff line number Diff line change
@@ -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<T>,
>() => React.useRef(undefined) as React.MutableRefObject<Api>;
TPlugins extends readonly TreeViewAnyPluginSignature[] = DefaultTreeViewPlugins,
>() => React.useRef(undefined) as React.MutableRefObject<TreeViewPublicAPI<TPlugins> | undefined>;
Original file line number Diff line number Diff line change
Expand Up @@ -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<UseTreeViewFocusInstance, 'focusNode'> {}

export interface UseTreeViewFocusParameters {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const useTreeViewJSXNodes: TreeViewPlugin<UseTreeViewJSXNodesSignature> =
}) => {
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.',
Expand All @@ -28,17 +28,31 @@ export const useTreeViewJSXNodes: TreeViewPlugin<UseTreeViewJSXNodesSignature> =
);
}

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 });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
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,
getItemId,
}: Pick<
UseTreeViewNodesDefaultizedParameters<TreeViewBaseItem>,
'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemId'
>): UseTreeViewNodesState => {
>): UseTreeViewNodesState<any>['nodes'] => {
const nodeMap: TreeViewNodeMap = {};
const itemMap: TreeViewItemMap<any> = {};

const processItem = (
item: TreeViewBaseItem,
Expand Down Expand Up @@ -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)),
Expand All @@ -84,16 +88,26 @@ const updateState = ({
return {
nodeMap,
nodeTree,
itemMap,
};
};

export const useTreeViewNodes: TreeViewPlugin<UseTreeViewNodesSignature> = ({
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 => {
Expand Down Expand Up @@ -125,7 +139,7 @@ export const useTreeViewNodes: TreeViewPlugin<UseTreeViewNodesSignature> = ({
);

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),
Expand All @@ -142,14 +156,14 @@ export const useTreeViewNodes: TreeViewPlugin<UseTreeViewNodesSignature> = ({

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 });
}
Expand All @@ -171,7 +185,7 @@ export const useTreeViewNodes: TreeViewPlugin<UseTreeViewNodesSignature> = ({
id,
children,
}: TreeViewNodeIdAndChildren): ReturnType<typeof instance.getNodesToRender>[number] => {
const node = state.nodeMap[id];
const node = state.nodes.nodeMap[id];
return {
label: node.label!,
nodeId: node.id,
Expand All @@ -180,29 +194,35 @@ export const useTreeViewNodes: TreeViewPlugin<UseTreeViewNodesSignature> = ({
};
};

return state.nodeTree.map(getPropsFromNodeId);
return state.nodes.nodeTree.map(getPropsFromNodeId);
});

populateInstance<UseTreeViewNodesSignature>(instance, {
getNode,
getItem,
getNodesToRender,
getChildrenIds,
getNavigableChildrenIds,
isNodeDisabled,
});

populatePublicAPI<UseTreeViewNodesSignature>(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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ interface TreeViewNodeProps {
children?: TreeViewNodeProps[];
}

export interface UseTreeViewNodesInstance {
export interface UseTreeViewNodesInstance<R extends {}> {
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<R extends {}>
extends Pick<UseTreeViewNodesInstance<R>, 'getItem'> {}

export interface UseTreeViewNodesParameters<R extends {}> {
/**
* If `true`, will allow focus on disabled items.
Expand Down Expand Up @@ -66,9 +70,12 @@ export interface TreeViewNodeIdAndChildren {
children?: TreeViewNodeIdAndChildren[];
}

export interface UseTreeViewNodesState {
nodeTree: TreeViewNodeIdAndChildren[];
nodeMap: TreeViewNodeMap;
export interface UseTreeViewNodesState<R extends {}> {
nodes: {
nodeTree: TreeViewNodeIdAndChildren[];
nodeMap: TreeViewNodeMap;
itemMap: TreeViewItemMap<R>;
};
}

interface UseTreeViewNodesContextValue
Expand All @@ -77,10 +84,13 @@ interface UseTreeViewNodesContextValue
export type UseTreeViewNodesSignature = TreeViewPluginSignature<{
params: UseTreeViewNodesParameters<any>;
defaultizedParams: UseTreeViewNodesDefaultizedParameters<any>;
instance: UseTreeViewNodesInstance;
instance: UseTreeViewNodesInstance<any>;
publicAPI: UseTreeViewNodesPublicAPI<any>;
events: UseTreeViewNodesEventLookup;
state: UseTreeViewNodesState;
state: UseTreeViewNodesState<any>;
contextValue: UseTreeViewNodesContextValue;
}>;

export type TreeViewNodeMap = { [nodeId: string]: TreeViewNode };

export type TreeViewItemMap<R extends {}> = { [nodeId: string]: R };

0 comments on commit c44d71a

Please sign in to comment.