UNPKG

@mui/x-tree-view

Version:

The community edition of the MUI X Tree View components.

114 lines (108 loc) 4.44 kB
import { expansionSelectors } from "../expansion/index.js"; import { focusSelectors } from "./selectors.js"; import { itemsSelectors } from "../items/index.js"; import { getFirstNavigableItem, getNextNavigableItem, getPreviousNavigableItem } from "../../utils/tree.js"; export class TreeViewFocusPlugin { // We can't type `store`, otherwise we get the following TS error: // 'focus' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. constructor(store) { this.store = store; // Whenever the items change, we need to ensure the focused item is still present. // If the focused item was removed, focus the closest neighbor instead of the first item. let previousState = store.state; this.store.subscribe(newState => { // Only run when items actually changed. if (newState.itemMetaLookup === previousState.itemMetaLookup) { previousState = newState; return; } const focusedItemId = focusSelectors.focusedItemId(newState); if (focusedItemId == null || itemsSelectors.itemMeta(newState, focusedItemId)) { previousState = newState; return; } const checkItemInNewTree = itemId => itemId == null || !itemsSelectors.itemMeta(newState, itemId) ? null : itemId; const itemToFocusId = checkItemInNewTree(getNextNavigableItem(previousState, focusedItemId)) ?? checkItemInNewTree(getPreviousNavigableItem(previousState, focusedItemId)) ?? getFirstNavigableItem(newState); if (itemToFocusId == null) { this.setFocusedItemId(null); } else { this.applyItemFocus(null, itemToFocusId); } previousState = newState; }); } setFocusedItemId = itemId => { const focusedItemId = focusSelectors.focusedItemId(this.store.state); if (focusedItemId === itemId) { return; } this.store.set('focusedItemId', itemId); }; applyItemFocus = (event, itemId) => { this.store.items.getItemDOMElement(itemId)?.focus(); this.setFocusedItemId(itemId); this.store.parameters.onItemFocus?.(event, itemId); }; buildPublicAPI = () => { return { focusItem: this.focusItem }; }; /** * 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 | null} event The DOM event that triggered the change. * @param {TreeViewItemId} itemId The id of the item to focus. */ focusItem = (event, itemId) => { // If we receive an itemId, and it is visible, the focus will be set to it const itemMeta = itemsSelectors.itemMeta(this.store.state, itemId); const isItemVisible = itemMeta && (itemMeta.parentId == null || expansionSelectors.isItemExpanded(this.store.state, itemMeta.parentId)); if (isItemVisible) { this.applyItemFocus(event, itemId); } }; /** * Remove the focus from the currently focused item (both from the internal state and the DOM). */ removeFocusedItem = () => { const focusedItemId = focusSelectors.focusedItemId(this.store.state); if (focusedItemId == null) { return; } const itemMeta = itemsSelectors.itemMeta(this.store.state, focusedItemId); if (itemMeta) { const itemElement = this.store.items.getItemDOMElement(focusedItemId); if (itemElement) { itemElement.blur(); } } this.setFocusedItemId(null); }; /** * Event handler to fire when the `root` slot of the Tree View is focused. * @param {React.MouseEvent} event The DOM event that triggered the change. */ handleRootFocus = event => { if (event.defaultMuiPrevented) { return; } // if the event bubbled (which is React specific) we don't want to steal focus const defaultFocusableItemId = focusSelectors.defaultFocusableItemId(this.store.state); if (event.target === event.currentTarget && defaultFocusableItemId != null) { this.applyItemFocus(event, defaultFocusableItemId); } }; /** * Event handler to fire when the `root` slot of the Tree View is blurred. * @param {React.MouseEvent} event The DOM event that triggered the change. */ handleRootBlur = event => { if (event.defaultMuiPrevented) { return; } this.setFocusedItemId(null); }; }