diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx index 3f27e6ccfae2..e3f8de04e341 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx @@ -29,6 +29,7 @@ import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext' import { useTreeItemState } from './useTreeItemState'; import { isTargetInDescendants } from '../internals/utils/tree'; import { TreeViewItemPluginSlotPropsEnhancerParams } from '../internals/models'; +import { generateTreeItemIdAttribute } from '../internals/corePlugins/useTreeViewId/useTreeViewId.utils'; const useThemeProps = createUseThemeProps('MuiTreeItem'); @@ -199,6 +200,7 @@ export const TreeItem = React.forwardRef(function TreeItem( items: { disabledItemsFocusable, indentationAtItemLevel }, selection: { multiSelect }, expansion: { expansionTrigger }, + treeId, instance, } = useTreeViewContext(); const depthContext = React.useContext(TreeViewItemDepthContext); @@ -382,7 +384,7 @@ export const TreeItem = React.forwardRef(function TreeItem( instance.handleItemKeyDown(event, itemId); }; - const idAttribute = instance.getTreeItemIdAttribute(itemId, id); + const idAttribute = generateTreeItemIdAttribute({ itemId, treeId, id }); const tabIndex = instance.canItemBeTabbed(itemId) ? 0 : -1; const sharedPropsEnhancerParams: Omit< diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx index bc080cfbd494..11d568f01469 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx @@ -4,6 +4,7 @@ import { useTreeViewContext } from './useTreeViewContext'; import { escapeOperandAttributeSelector } from '../utils/utils'; import type { UseTreeViewJSXItemsSignature } from '../plugins/useTreeViewJSXItems'; import type { UseTreeViewItemsSignature } from '../plugins/useTreeViewItems'; +import { generateTreeItemIdAttribute } from '../corePlugins/useTreeViewId/useTreeViewId.utils'; export const TreeViewChildrenItemContext = React.createContext(null); @@ -20,7 +21,7 @@ interface TreeViewChildrenItemProviderProps { export function TreeViewChildrenItemProvider(props: TreeViewChildrenItemProviderProps) { const { children, itemId = null } = props; - const { instance, rootRef } = + const { instance, treeId, rootRef } = useTreeViewContext<[UseTreeViewJSXItemsSignature, UseTreeViewItemsSignature]>(); const childrenIdAttrToIdRef = React.useRef>(new Map()); @@ -36,7 +37,7 @@ export function TreeViewChildrenItemProvider(props: TreeViewChildrenItemProvider // Undefined during 1st render const itemMeta = instance.getItemMeta(itemId); if (itemMeta !== undefined) { - idAttr = instance.getTreeItemIdAttribute(itemId, itemMeta.idAttribute); + idAttr = generateTreeItemIdAttribute({ itemId, treeId, id: itemMeta.idAttribute }); } } diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx index d85512736950..99b4813b2057 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx @@ -15,7 +15,7 @@ export function TreeViewProvider - {value.wrapRoot({ children })} + {value.wrapRoot({ children, instance: value.instance })} ); } 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 b60339cc5913..bbb2e0dca242 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts @@ -8,6 +8,7 @@ import { TreeViewItemPluginResponse, TreeViewPublicAPI, } from '../models'; +import { TreeViewCorePluginSignatures } from '../corePlugins'; export type TreeViewItemPluginsRunner = ( props: TProps, @@ -16,7 +17,7 @@ export type TreeViewItemPluginsRunner = ( export type TreeViewContextValue< TSignatures extends readonly TreeViewAnyPluginSignature[], TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [], -> = MergeSignaturesProperty & +> = MergeSignaturesProperty<[...TreeViewCorePluginSignatures, ...TSignatures], 'contextValue'> & Partial> & { instance: TreeViewInstance; publicAPI: TreeViewPublicAPI; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.ts index 8b74f34f1e64..cbf443431f4e 100644 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.ts +++ b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.ts @@ -1,22 +1,34 @@ import * as React from 'react'; -import useId from '@mui/utils/useId'; import { TreeViewPlugin } from '../../models'; import { UseTreeViewIdSignature } from './useTreeViewId.types'; +import { createTreeViewDefaultId } from './useTreeViewId.utils'; -export const useTreeViewId: TreeViewPlugin = ({ params }) => { - const treeId = useId(params.id); +export const useTreeViewId: TreeViewPlugin = ({ + params, + state, + setState, +}) => { + const treeId = state.id.treeId; - const getTreeItemIdAttribute = React.useCallback( - (itemId: string, idAttribute: string | undefined) => idAttribute ?? `${treeId}-${itemId}`, - [treeId], - ); + React.useEffect(() => { + setState((prevState) => { + if (prevState.id.treeId === params.id) { + return prevState; + } + + return { + ...prevState, + id: { ...prevState.id, treeId: params.id ?? createTreeViewDefaultId() }, + }; + }); + }, [setState, params.id]); return { getRootProps: () => ({ id: treeId, }), - instance: { - getTreeItemIdAttribute, + contextValue: { + treeId, }, }; }; @@ -24,3 +36,5 @@ export const useTreeViewId: TreeViewPlugin = ({ params } useTreeViewId.params = { id: true, }; + +useTreeViewId.getInitialState = ({ id }) => ({ id: { treeId: id ?? createTreeViewDefaultId() } }); diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.types.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.types.ts index 3e90ac28f384..392a77e8585c 100644 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.types.ts +++ b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.types.ts @@ -1,17 +1,4 @@ import { TreeViewPluginSignature } from '../../models'; -import { TreeViewItemId } from '../../../models'; - -export interface UseTreeViewIdInstance { - /** - * Get the id attribute (i.e.: the `id` attribute passed to the DOM element) of a tree item. - * If the user explicitly defined an id attribute, it will be returned. - * Otherwise, the method created a unique id for the item based on the Tree View id attribute and the item `itemId` - * @param {TreeViewItemId} itemId The id of the item to get the id attribute of. - * @param {string | undefined} idAttribute The id attribute of the item if explicitly defined by the user. - * @returns {string} The id attribute of the item. - */ - getTreeItemIdAttribute: (itemId: TreeViewItemId, idAttribute: string | undefined) => string; -} export interface UseTreeViewIdParameters { /** @@ -23,8 +10,19 @@ export interface UseTreeViewIdParameters { export type UseTreeViewIdDefaultizedParameters = UseTreeViewIdParameters; +export interface UseTreeViewIdState { + id: { + treeId: string; + }; +} + +interface UseTreeViewIdContextValue { + treeId: string | undefined; +} + export type UseTreeViewIdSignature = TreeViewPluginSignature<{ params: UseTreeViewIdParameters; defaultizedParams: UseTreeViewIdDefaultizedParameters; - instance: UseTreeViewIdInstance; + state: UseTreeViewIdState; + contextValue: UseTreeViewIdContextValue; }>; diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.utils.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.utils.ts new file mode 100644 index 000000000000..8a8e42ef69ec --- /dev/null +++ b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.utils.ts @@ -0,0 +1,33 @@ +import { TreeViewItemId } from '../../../models'; + +let globalTreeViewDefaultId = 0; +export const createTreeViewDefaultId = () => { + globalTreeViewDefaultId += 1; + return `mui-tree-view-${globalTreeViewDefaultId}`; +}; + +/** + * Generate the id attribute (i.e.: the `id` attribute passed to the DOM element) of a tree item. + * If the user explicitly defined an id attribute, it will be returned. + * Otherwise, the method creates a unique id for the item based on the Tree View id attribute and the item `itemId` + * @param {object} params The parameters to determine the id attribute of the item. + * @param {TreeViewItemId} params.itemId The id of the item to get the id attribute of. + * @param {string | undefined} params.idAttribute The id attribute of the item if explicitly defined by the user. + * @param {string} params.treeId The id attribute of the Tree View. + * @returns {string} The id attribute of the item. + */ +export const generateTreeItemIdAttribute = ({ + id, + treeId = '', + itemId, +}: { + id: TreeViewItemId | undefined; + treeId: string | undefined; + itemId: string; +}): string => { + if (id != null) { + return id; + } + + return `${treeId}-${itemId}`; +}; 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 b16854a16cc3..96fe7f1b34a3 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts @@ -91,10 +91,7 @@ export const useTreeViewFocus: TreeViewPlugin = ({ const itemMeta = instance.getItemMeta(state.focusedItemId); if (itemMeta) { - const itemElement = document.getElementById( - instance.getTreeItemIdAttribute(state.focusedItemId, itemMeta.idAttribute), - ); - + const itemElement = instance.getItemDOMElement(state.focusedItemId); if (itemElement) { itemElement.blur(); } diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx index 71b671722554..cd1112747d1f 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx @@ -9,6 +9,7 @@ import { publishTreeViewEvent } from '../../utils/publishTreeViewEvent'; import { TreeViewBaseItem, TreeViewItemId } from '../../../models'; import { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils'; import { TreeViewItemDepthContext } from '../../TreeViewItemDepthContext'; +import { generateTreeItemIdAttribute } from '../../corePlugins/useTreeViewId/useTreeViewId.utils'; interface UpdateNodesStateParameters extends Pick< @@ -180,7 +181,9 @@ export const useTreeViewItems: TreeViewPlugin = ({ return null; } - return document.getElementById(instance.getTreeItemIdAttribute(itemId, itemMeta.idAttribute)); + return document.getElementById( + generateTreeItemIdAttribute({ treeId: state.id.treeId, itemId, id: itemMeta.idAttribute }), + ); }; const isItemNavigable = (itemId: string) => { 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 545a74bef743..cdfd84fa5aca 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 { TreeViewItemDepthContext } from '../../TreeViewItemDepthContext'; +import { generateTreeItemIdAttribute } from '../../corePlugins/useTreeViewId/useTreeViewId.utils'; export const useTreeViewJSXItems: TreeViewPlugin = ({ instance, @@ -121,7 +122,7 @@ const useTreeViewJSXItemsItemPlugin: TreeViewItemPlugin { - const { instance } = useTreeViewContext<[UseTreeViewJSXItemsSignature]>(); + const { instance, treeId } = useTreeViewContext<[UseTreeViewJSXItemsSignature]>(); const { children, disabled = false, label, itemId, id } = props; const parentContext = React.useContext(TreeViewChildrenItemContext); @@ -142,13 +143,13 @@ const useTreeViewJSXItemsItemPlugin: TreeViewItemPlugin { - const idAttributeWithDefault = instance.getTreeItemIdAttribute(itemId, id); - registerChild(idAttributeWithDefault, itemId); + const idAttribute = generateTreeItemIdAttribute({ itemId, treeId, id }); + registerChild(idAttribute, itemId); return () => { - unregisterChild(idAttributeWithDefault); + unregisterChild(idAttribute); }; - }, [instance, registerChild, unregisterChild, itemId, id]); + }, [registerChild, unregisterChild, itemId, id, treeId]); React.useEffect(() => { return instance.insertJSXItem({ diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts index 64c696d245f0..a8b8fff491e4 100644 --- a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts +++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts @@ -26,6 +26,7 @@ import { import { useTreeItem2Utils } from '../hooks/useTreeItem2Utils'; import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext'; import { isTargetInDescendants } from '../internals/utils/tree'; +import { generateTreeItemIdAttribute } from '../internals/corePlugins/useTreeViewId/useTreeViewId.utils'; export const useTreeItem2 = < TSignatures extends UseTreeItem2MinimalPlugins = UseTreeItem2MinimalPlugins, @@ -38,6 +39,7 @@ export const useTreeItem2 = < items: { onItemClick, disabledItemsFocusable, indentationAtItemLevel }, selection: { multiSelect, disableSelection, checkboxSelection }, expansion: { expansionTrigger }, + treeId, instance, publicAPI, } = useTreeViewContext(); @@ -49,10 +51,11 @@ export const useTreeItem2 = < const { interactions, status } = useTreeItem2Utils({ itemId, children }); const rootRefObject = React.useRef(null); const contentRefObject = React.useRef(null); - const idAttribute = instance.getTreeItemIdAttribute(itemId, id); const handleRootRef = useForkRef(rootRef, pluginRootRef, rootRefObject)!; const handleContentRef = useForkRef(contentRef, contentRefObject)!; const checkboxRef = React.useRef(null); + + const idAttribute = generateTreeItemIdAttribute({ itemId, treeId, id }); const rootTabIndex = instance.canItemBeTabbed(itemId) ? 0 : -1; const sharedPropsEnhancerParams: Omit< diff --git a/test/utils/tree-view/fakeContextValue.ts b/test/utils/tree-view/fakeContextValue.ts index 058ba59ec240..eabcda86ac5d 100644 --- a/test/utils/tree-view/fakeContextValue.ts +++ b/test/utils/tree-view/fakeContextValue.ts @@ -10,7 +10,6 @@ export const getFakeContextValue = ( isItemFocused: () => false, isItemSelected: () => false, isItemDisabled: (itemId: string | null): itemId is string => !!itemId, - getTreeItemIdAttribute: () => '', mapFirstCharFromJSX: () => () => {}, canItemBeTabbed: () => false, } as any, @@ -43,6 +42,7 @@ export const getFakeContextValue = ( checkboxSelection: features.checkboxSelection ?? false, disableSelection: false, }, + treeId: 'mui-tree-view-1', rootRef: { current: null, },