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 3b993255789e..d463f24e85bb 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts @@ -18,7 +18,7 @@ export const useTreeViewExpansion: TreeViewPlugin return temp; }, [models.expandedItems.value]); - const setExpandedItems = (event: React.SyntheticEvent, value: string[]) => { + const setExpandedItems = (event: React.SyntheticEvent, value: TreeViewItemId[]) => { params.onExpandedItemsChange?.(event, value); models.expandedItems.setControlledValue(value); }; @@ -33,13 +33,15 @@ export const useTreeViewExpansion: TreeViewPlugin [instance], ); - const toggleItemExpansion = useEventCallback((event: React.SyntheticEvent, itemId: string) => { - const isExpandedBefore = instance.isItemExpanded(itemId); - instance.setItemExpansion(event, itemId, !isExpandedBefore); - }); + const toggleItemExpansion = useEventCallback( + (event: React.SyntheticEvent, itemId: TreeViewItemId) => { + const isExpandedBefore = instance.isItemExpanded(itemId); + instance.setItemExpansion(event, itemId, !isExpandedBefore); + }, + ); const setItemExpansion = useEventCallback( - (event: React.SyntheticEvent, itemId: string, isExpanded: boolean) => { + (event: React.SyntheticEvent, itemId: TreeViewItemId, isExpanded: boolean) => { const isExpandedBefore = instance.isItemExpanded(itemId); if (isExpandedBefore === isExpanded) { return; @@ -60,7 +62,7 @@ export const useTreeViewExpansion: TreeViewPlugin }, ); - const expandAllSiblings = (event: React.KeyboardEvent, itemId: string) => { + const expandAllSiblings = (event: React.KeyboardEvent, itemId: TreeViewItemId) => { const itemMeta = instance.getItemMeta(itemId); const siblings = instance.getItemOrderedChildrenIds(itemMeta.parentId); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts index 0116e7d722eb..1eabf3ce9daf 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import { DefaultizedProps, TreeViewPluginSignature } from '../../models'; import { UseTreeViewItemsSignature } from '../useTreeViewItems'; +import { TreeViewItemId } from '../../../models'; export interface UseTreeViewExpansionPublicAPI { /** @@ -13,10 +14,33 @@ export interface UseTreeViewExpansionPublicAPI { } export interface UseTreeViewExpansionInstance extends UseTreeViewExpansionPublicAPI { - isItemExpanded: (itemId: string) => boolean; - isItemExpandable: (itemId: string) => boolean; - toggleItemExpansion: (event: React.SyntheticEvent, itemId: string) => void; - expandAllSiblings: (event: React.KeyboardEvent, itemId: string) => void; + /** + * Check if an item is expanded. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item is expanded, `false` otherwise. + */ + isItemExpanded: (itemId: TreeViewItemId) => boolean; + /** + * Check if an item is expandable. + * Currently, an item is expandable if it has children. + * In the future, the user should be able to flag an item as expandable even if it has no loaded children to support children lazy loading. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item can be expanded, `false` otherwise. + */ + isItemExpandable: (itemId: TreeViewItemId) => boolean; + /** + * Toggle the current expansion of an item. + * If it is expanded, it will be collapsed, and vice versa. + * @param {React.SyntheticEvent} event The UI event that triggered the change. + * @param {TreeViewItemId} itemId The id of the item to toggle. + */ + toggleItemExpansion: (event: React.SyntheticEvent, itemId: TreeViewItemId) => void; + /** + * Expand all the siblings (i.e.: the items that have the same parent) of a given item. + * @param {React.SyntheticEvent} event The UI event that triggered the change. + * @param {TreeViewItemId} itemId The id of the item whose siblings will be expanded. + */ + expandAllSiblings: (event: React.KeyboardEvent, itemId: TreeViewItemId) => void; } export interface UseTreeViewExpansionParameters { 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 698d7fffa06b..2bec87a29de3 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts @@ -8,22 +8,20 @@ import { useInstanceEventHandler } from '../../hooks/useInstanceEventHandler'; import { getActiveElement } from '../../utils/utils'; import { getFirstNavigableItem } from '../../utils/tree'; import { MuiCancellableEvent } from '../../models/MuiCancellableEvent'; +import { convertSelectedItemsToArray } from '../useTreeViewSelection/useTreeViewSelection.utils'; -const useTabbableItemId = ( +const useDefaultFocusableItemId = ( instance: TreeViewUsedInstance, selectedItems: string | string[] | null, -) => { - const isItemVisible = (itemId: string) => { +): string => { + let tabbableItemId = convertSelectedItemsToArray(selectedItems).find((itemId) => { + if (!instance.isItemNavigable(itemId)) { + return false; + } + const itemMeta = instance.getItemMeta(itemId); return itemMeta && (itemMeta.parentId == null || instance.isItemExpanded(itemMeta.parentId)); - }; - - let tabbableItemId: string | null | undefined; - if (Array.isArray(selectedItems)) { - tabbableItemId = selectedItems.find(isItemVisible); - } else if (selectedItems != null && isItemVisible(selectedItems)) { - tabbableItemId = selectedItems; - } + }); if (tabbableItemId == null) { tabbableItemId = getFirstNavigableItem(instance); @@ -40,7 +38,7 @@ export const useTreeViewFocus: TreeViewPlugin = ({ models, rootRef, }) => { - const tabbableItemId = useTabbableItemId(instance, models.selectedItems.value); + const defaultFocusableItemId = useDefaultFocusableItemId(instance, models.selectedItems.value); const setFocusedItemId = useEventCallback((itemId: React.SetStateAction) => { const cleanItemId = typeof itemId === 'function' ? itemId(state.focusedItemId) : itemId; @@ -88,21 +86,6 @@ export const useTreeViewFocus: TreeViewPlugin = ({ } }); - const focusDefaultItem = useEventCallback((event: React.SyntheticEvent | null) => { - let itemToFocusId: string | null | undefined; - if (Array.isArray(models.selectedItems.value)) { - itemToFocusId = models.selectedItems.value.find(isItemVisible); - } else if (models.selectedItems.value != null && isItemVisible(models.selectedItems.value)) { - itemToFocusId = models.selectedItems.value; - } - - if (itemToFocusId == null) { - itemToFocusId = getFirstNavigableItem(instance); - } - - innerFocusItem(event, itemToFocusId); - }); - const removeFocusedItem = useEventCallback(() => { if (state.focusedItemId == null) { return; @@ -121,11 +104,11 @@ export const useTreeViewFocus: TreeViewPlugin = ({ setFocusedItemId(null); }); - const canItemBeTabbed = (itemId: string) => itemId === tabbableItemId; + const canItemBeTabbed = (itemId: string) => itemId === defaultFocusableItemId; useInstanceEventHandler(instance, 'removeItem', ({ id }) => { if (state.focusedItemId === id) { - instance.focusDefaultItem(null); + innerFocusItem(null, defaultFocusableItemId); } }); @@ -139,7 +122,7 @@ export const useTreeViewFocus: TreeViewPlugin = ({ // if the event bubbled (which is React specific) we don't want to steal focus if (event.target === event.currentTarget) { - instance.focusDefaultItem(event); + innerFocusItem(event, defaultFocusableItemId); } }; @@ -154,7 +137,6 @@ export const useTreeViewFocus: TreeViewPlugin = ({ isItemFocused, canItemBeTabbed, focusItem, - focusDefaultItem, removeFocusedItem, }, }; 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 0cb0c7e69fde..993487fcf86d 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 @@ -4,23 +4,38 @@ import { UseTreeViewIdSignature } from '../useTreeViewId/useTreeViewId.types'; import type { UseTreeViewItemsSignature } from '../useTreeViewItems'; import type { UseTreeViewSelectionSignature } from '../useTreeViewSelection'; import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion'; +import { TreeViewItemId } from '../../../models'; export interface UseTreeViewFocusPublicAPI { /** - * Focuses the item with the given id. + * Focus the item with the given id. * * If the item is the child of a collapsed item, then this method will do nothing. * Make sure to expand the ancestors of the item before calling this method if needed. * @param {React.SyntheticEvent} event The event source of the action. - * @param {string} itemId The id of the item to focus. + * @param {TreeViewItemId} itemId The id of the item to focus. */ focusItem: (event: React.SyntheticEvent, itemId: string) => void; } export interface UseTreeViewFocusInstance extends UseTreeViewFocusPublicAPI { - isItemFocused: (itemId: string) => boolean; - canItemBeTabbed: (itemId: string) => boolean; - focusDefaultItem: (event: React.SyntheticEvent | null) => void; + /** + * Check if an item is the currently focused item. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item is focused, `false` otherwise. + */ + isItemFocused: (itemId: TreeViewItemId) => boolean; + /** + * Check if an item should be sequentially focusable (usually with the Tab key). + * At any point in time, there is a single item that can be sequentially focused in the Tree View. + * This item is the first selected item (that is both visible and navigable), if any, or the first navigable item if no item is selected. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item can be sequentially focusable, `false` otherwise. + */ + canItemBeTabbed: (itemId: TreeViewItemId) => boolean; + /** + * Remove the focus from the currently focused item (both from the internal state and the DOM). + */ removeFocusedItem: () => void; } diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewId/useTreeViewId.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewId/useTreeViewId.types.ts index aafd09a0949b..0ef27d1f37db 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewId/useTreeViewId.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewId/useTreeViewId.types.ts @@ -1,7 +1,16 @@ import { TreeViewPluginSignature } from '../../models'; +import { TreeViewItemId } from '../../../models'; export interface UseTreeViewIdInstance { - getTreeItemIdAttribute: (itemId: string, idAttribute: string | undefined) => string; + /** + * 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 { 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 327907342d61..f2faed4d6202 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 @@ -15,16 +15,51 @@ export interface UseTreeViewItemsPublicAPI { * @param {string} itemId The id of the item to return. * @returns {R} The item with the given id. */ - getItem: (itemId: string) => R; + getItem: (itemId: TreeViewItemId) => R; } export interface UseTreeViewItemsInstance extends UseTreeViewItemsPublicAPI { - getItemMeta: (itemId: string) => TreeViewItemMeta; + /** + * Get the meta-information of an item. + * Check the `TreeViewItemMeta` type for more information. + * @param {TreeViewItemId} itemId The id of the item to get the meta-information of. + * @returns {TreeViewItemMeta} The meta-information of the item. + */ + getItemMeta: (itemId: TreeViewItemId) => TreeViewItemMeta; + /** + * Get the item that should be rendered. + * This method is only used on Rich Tree View components. + * Check the `TreeViewItemProps` type for more information. + * @returns {TreeViewItemProps[]} The items to render. + */ getItemsToRender: () => TreeViewItemProps[]; - getItemOrderedChildrenIds: (parentId: string | null) => string[]; - isItemDisabled: (itemId: string) => itemId is string; - isItemNavigable: (itemId: string) => boolean; - getItemIndex: (itemId: string) => number; + /** + * Get the ids of a given item's children. + * Those ids are returned in the order they should be rendered. + * @param {TreeViewItemId | null} itemId The id of the item to get the children of. + * @returns {TreeViewItemId[]} The ids of the item's children. + */ + getItemOrderedChildrenIds: (itemId: TreeViewItemId | null) => TreeViewItemId[]; + /** + * Check if a given item is disabled. + * An item is disabled if it was marked as disabled or if one of its ancestors is disabled. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item is disabled, `false` otherwise. + */ + isItemDisabled: (itemId: TreeViewItemId) => boolean; + /** + * Check if a given item is navigable (i.e.: if it can be accessed through keyboard navigation). + * An item is navigable if it is not disabled or if the `disabledItemsFocusable` prop is `true`. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item is navigable, `false` otherwise. + */ + isItemNavigable: (itemId: TreeViewItemId) => boolean; + /** + * Get the index of a given item in its parent's children list. + * @param {TreeViewItemId} itemId The id of the item to get the index of. + * @returns {number} The index of the item in its parent's children list. + */ + getItemIndex: (itemId: TreeViewItemId) => number; /** * Freeze any future update to the state based on the `items` prop. * This is useful when `useTreeViewJSXItems` is used to avoid having conflicting sources of truth. 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 e28b703fbed8..8814ba9b689a 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx @@ -46,6 +46,24 @@ export const useTreeViewJSXItems: TreeViewPlugin = }, }; }); + + return () => { + setState((prevState) => { + const newItemMetaMap = { ...prevState.items.itemMetaMap }; + const newItemMap = { ...prevState.items.itemMap }; + delete newItemMetaMap[item.id]; + delete newItemMap[item.id]; + return { + ...prevState, + items: { + ...prevState.items, + itemMetaMap: newItemMetaMap, + itemMap: newItemMap, + }, + }; + }); + publishTreeViewEvent(instance, 'removeItem', { id: item.id }); + }; }); const setJSXItemsOrderedChildrenIds = (parentId: string | null, orderedChildrenIds: string[]) => { @@ -67,24 +85,6 @@ export const useTreeViewJSXItems: TreeViewPlugin = })); }; - const removeJSXItem = useEventCallback((itemId: string) => { - setState((prevState) => { - const newItemMetaMap = { ...prevState.items.itemMetaMap }; - const newItemMap = { ...prevState.items.itemMap }; - delete newItemMetaMap[itemId]; - delete newItemMap[itemId]; - return { - ...prevState, - items: { - ...prevState.items, - itemMetaMap: newItemMetaMap, - itemMap: newItemMap, - }, - }; - }); - publishTreeViewEvent(instance, 'removeItem', { id: itemId }); - }); - const mapFirstCharFromJSX = useEventCallback((itemId: string, firstChar: string) => { instance.updateFirstCharMap((firstCharMap) => { firstCharMap[itemId] = firstChar; @@ -103,7 +103,6 @@ export const useTreeViewJSXItems: TreeViewPlugin = return { instance: { insertJSXItem, - removeJSXItem, setJSXItemsOrderedChildrenIds, mapFirstCharFromJSX, }, @@ -152,15 +151,13 @@ const useTreeViewJSXItemsItemPlugin: TreeViewItemPlugin { - instance.insertJSXItem({ + return instance.insertJSXItem({ id: itemId, idAttribute: id, parentId, expandable, disabled, }); - - return () => instance.removeJSXItem(itemId); }, [instance, parentId, itemId, expandable, disabled, id]); React.useEffect(() => { diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.types.ts index 87f7f4a07baa..3f613428603e 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.types.ts @@ -1,12 +1,33 @@ import { TreeViewItemMeta, TreeViewPluginSignature } from '../../models'; import { UseTreeViewItemsSignature } from '../useTreeViewItems'; import { UseTreeViewKeyboardNavigationSignature } from '../useTreeViewKeyboardNavigation'; +import { TreeViewItemId } from '../../../models'; export interface UseTreeViewItemsInstance { - insertJSXItem: (item: TreeViewItemMeta) => void; - removeJSXItem: (itemId: string) => void; - mapFirstCharFromJSX: (itemId: string, firstChar: string) => () => void; - setJSXItemsOrderedChildrenIds: (parentId: string | null, orderedChildrenIds: string[]) => void; + /** + * Insert a new item in the state from a Tree Item component. + * @param {TreeViewItemMeta} item The meta-information of the item to insert. + * @returns {() => void} A function to remove the item from the state. + */ + insertJSXItem: (item: TreeViewItemMeta) => () => void; + /** + * Updates the `firstCharMap` to register the first character of the given item's label. + * This map is used to navigate the tree using type-ahead search. + * @param {TreeViewItemId} itemId The id of the item to map the first character of. + * @param {string} firstChar The first character of the item's label. + * @returns {() => void} A function to remove the item from the `firstCharMap`. + */ + mapFirstCharFromJSX: (itemId: TreeViewItemId, firstChar: string) => () => void; + /** + * Store the ids of a given item's children in the state. + * Those ids must be passed in the order they should be rendered. + * @param {TreeViewItemId | null} parentId The id of the item to store the children of. + * @param {TreeViewItemId[]} orderedChildrenIds The ids of the item's children. + */ + setJSXItemsOrderedChildrenIds: ( + parentId: TreeViewItemId | null, + orderedChildrenIds: TreeViewItemId[], + ) => void; } export interface UseTreeViewJSXItemsParameters {} diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts index 053eeaaf43e6..9aceca62b73f 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.types.ts @@ -5,12 +5,25 @@ import { UseTreeViewSelectionSignature } from '../useTreeViewSelection'; import { UseTreeViewFocusSignature } from '../useTreeViewFocus'; import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion'; import { MuiCancellableEvent } from '../../models/MuiCancellableEvent'; +import { TreeViewItemId } from '../../../models'; export interface UseTreeViewKeyboardNavigationInstance { + /** + * Updates the `firstCharMap` to add/remove the first character of some item's labels. + * This map is used to navigate the tree using type-ahead search. + * This method is only used by the `useTreeViewJSXItems` plugin, otherwise the updates are handled internally. + * @param {(map: TreeViewFirstCharMap) => TreeViewFirstCharMap} updater The function to update the map. + */ updateFirstCharMap: (updater: (map: TreeViewFirstCharMap) => TreeViewFirstCharMap) => void; + /** + * Callback fired when a key is pressed on an item. + * Handles all the keyboard navigation logic. + * @param {React.KeyboardEvent & MuiCancellableEvent} event The keyboard event that triggered the callback. + * @param {TreeViewItemId} itemId The id of the item that the event was triggered on. + */ handleItemKeyDown: ( event: React.KeyboardEvent & MuiCancellableEvent, - itemId: string, + itemId: TreeViewItemId, ) => void; } diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts index 005ab581ce1e..56474f46a0ba 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts @@ -4,6 +4,11 @@ import { UseTreeViewItemsSignature } from '../useTreeViewItems'; import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion'; export interface UseTreeViewSelectionInstance { + /** + * Check if an item is selected. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item is selected, `false` otherwise. + */ isItemSelected: (itemId: string) => boolean; /** * Select or deselect an item.