@mui/x-tree-view
Version:
The community edition of the MUI X Tree View components.
114 lines (108 loc) • 4.44 kB
JavaScript
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);
};
}