UNPKG

@mui/x-tree-view

Version:

The community edition of the MUI X Tree View components.

312 lines (295 loc) 12 kB
"use strict"; 'use client'; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.useTreeViewKeyboardNavigation = void 0; var React = _interopRequireWildcard(require("react")); var _store = require("@mui/x-internals/store"); var _RtlProvider = require("@mui/system/RtlProvider"); var _useTimeout = require("@base-ui/utils/useTimeout"); var _useStableCallback = require("@base-ui/utils/useStableCallback"); var _tree = require("../../utils/tree"); var _plugins = require("../../utils/plugins"); var _useTreeViewLabel = require("../useTreeViewLabel"); var _useTreeViewItems = require("../useTreeViewItems/useTreeViewItems.selectors"); var _useTreeViewLabel2 = require("../useTreeViewLabel/useTreeViewLabel.selectors"); var _useTreeViewSelection = require("../useTreeViewSelection/useTreeViewSelection.selectors"); var _useTreeViewExpansion = require("../useTreeViewExpansion/useTreeViewExpansion.selectors"); function isPrintableKey(string) { return !!string && string.length === 1 && !!string.match(/\S/); } const TYPEAHEAD_TIMEOUT = 500; const useTreeViewKeyboardNavigation = ({ instance, store, params }) => { const isRtl = (0, _RtlProvider.useRtl)(); const labelMap = React.useRef({}); const typeaheadQueryRef = React.useRef(''); const typeaheadTimeout = (0, _useTimeout.useTimeout)(); const updateLabelMap = (0, _useStableCallback.useStableCallback)(callback => { labelMap.current = callback(labelMap.current); }); const itemMetaLookup = (0, _store.useStore)(store, _useTreeViewItems.itemsSelectors.itemMetaLookup); React.useEffect(() => { if (instance.areItemUpdatesPrevented()) { return; } const newLabelMap = {}; const processItem = item => { newLabelMap[item.id] = item.label.toLowerCase(); }; Object.values(itemMetaLookup).forEach(processItem); labelMap.current = newLabelMap; }, [itemMetaLookup, params.getItemId, instance]); const getNextItem = itemIdToCheck => { const nextItemId = (0, _tree.getNextNavigableItem)(store.state, itemIdToCheck); // We reached the end of the tree, check from the beginning if (nextItemId === null) { return (0, _tree.getFirstNavigableItem)(store.state); } return nextItemId; }; const getNextMatchingItemId = (itemId, query) => { let matchingItemId = null; const checkedItems = {}; // If query length > 1, first check if current item matches let currentItemId = query.length > 1 ? itemId : getNextItem(itemId); // The "!checkedItems[currentItemId]" condition avoids an infinite loop when there is no matching item. while (matchingItemId == null && !checkedItems[currentItemId]) { const itemLabel = labelMap.current[currentItemId]; if (itemLabel?.startsWith(query)) { matchingItemId = currentItemId; } else { checkedItems[currentItemId] = true; currentItemId = getNextItem(currentItemId); } } return matchingItemId; }; const getFirstMatchingItem = (itemId, newKey) => { const cleanNewKey = newKey.toLowerCase(); // Try matching with accumulated query + new key const concatenatedQuery = `${typeaheadQueryRef.current}${cleanNewKey}`; // check if the entire typed query matches an item const concatenatedQueryMatchingItemId = getNextMatchingItemId(itemId, concatenatedQuery); if (concatenatedQueryMatchingItemId != null) { typeaheadQueryRef.current = concatenatedQuery; return concatenatedQueryMatchingItemId; } const newKeyMatchingItemId = getNextMatchingItemId(itemId, cleanNewKey); if (newKeyMatchingItemId != null) { typeaheadQueryRef.current = cleanNewKey; return newKeyMatchingItemId; } typeaheadQueryRef.current = ''; return null; }; const canToggleItemSelection = itemId => _useTreeViewSelection.selectionSelectors.canItemBeSelected(store.state, itemId); const canToggleItemExpansion = itemId => { return !_useTreeViewItems.itemsSelectors.isItemDisabled(store.state, itemId) && _useTreeViewExpansion.expansionSelectors.isItemExpandable(store.state, itemId); }; // ARIA specification: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/#keyboardinteraction const handleItemKeyDown = async (event, itemId) => { if (event.defaultMuiPrevented) { return; } if (event.altKey || (0, _tree.isTargetInDescendants)(event.target, event.currentTarget)) { return; } const ctrlPressed = event.ctrlKey || event.metaKey; const key = event.key; const isMultiSelectEnabled = _useTreeViewSelection.selectionSelectors.isMultiSelectEnabled(store.state); // eslint-disable-next-line default-case switch (true) { // Select the item when pressing "Space" case key === ' ' && canToggleItemSelection(itemId): { event.preventDefault(); if (isMultiSelectEnabled && event.shiftKey) { instance.expandSelectionRange(event, itemId); } else { instance.setItemSelection({ event, itemId, keepExistingSelection: isMultiSelectEnabled, shouldBeSelected: undefined }); } break; } // If the focused item has children, we expand it. // If the focused item has no children, we select it. case key === 'Enter': { if ((0, _plugins.hasPlugin)(instance, _useTreeViewLabel.useTreeViewLabel) && _useTreeViewLabel2.labelSelectors.isItemEditable(store.state, itemId) && !_useTreeViewLabel2.labelSelectors.isItemBeingEdited(store.state, itemId)) { instance.setEditedItem(itemId); } else if (canToggleItemExpansion(itemId)) { instance.setItemExpansion({ event, itemId }); event.preventDefault(); } else if (canToggleItemSelection(itemId)) { if (isMultiSelectEnabled) { event.preventDefault(); instance.setItemSelection({ event, itemId, keepExistingSelection: true }); } else if (!_useTreeViewSelection.selectionSelectors.isItemSelected(store.state, itemId)) { instance.setItemSelection({ event, itemId }); event.preventDefault(); } } break; } // Focus the next focusable item case key === 'ArrowDown': { const nextItem = (0, _tree.getNextNavigableItem)(store.state, itemId); if (nextItem) { event.preventDefault(); instance.focusItem(event, nextItem); // Multi select behavior when pressing Shift + ArrowDown // Toggles the selection state of the next item if (isMultiSelectEnabled && event.shiftKey && canToggleItemSelection(nextItem)) { instance.selectItemFromArrowNavigation(event, itemId, nextItem); } } break; } // Focuses the previous focusable item case key === 'ArrowUp': { const previousItem = (0, _tree.getPreviousNavigableItem)(store.state, itemId); if (previousItem) { event.preventDefault(); instance.focusItem(event, previousItem); // Multi select behavior when pressing Shift + ArrowUp // Toggles the selection state of the previous item if (isMultiSelectEnabled && event.shiftKey && canToggleItemSelection(previousItem)) { instance.selectItemFromArrowNavigation(event, itemId, previousItem); } } break; } // If the focused item is expanded, we move the focus to its first child // If the focused item is collapsed and has children, we expand it case key === 'ArrowRight' && !isRtl || key === 'ArrowLeft' && isRtl: { if (ctrlPressed) { return; } if (_useTreeViewExpansion.expansionSelectors.isItemExpanded(store.state, itemId)) { const nextItemId = (0, _tree.getNextNavigableItem)(store.state, itemId); if (nextItemId) { instance.focusItem(event, nextItemId); event.preventDefault(); } } else if (canToggleItemExpansion(itemId)) { instance.setItemExpansion({ event, itemId }); event.preventDefault(); } break; } // If the focused item is expanded, we collapse it // If the focused item is collapsed and has a parent, we move the focus to this parent case key === 'ArrowLeft' && !isRtl || key === 'ArrowRight' && isRtl: { if (ctrlPressed) { return; } if (canToggleItemExpansion(itemId) && _useTreeViewExpansion.expansionSelectors.isItemExpanded(store.state, itemId)) { instance.setItemExpansion({ event, itemId }); event.preventDefault(); } else { const parent = _useTreeViewItems.itemsSelectors.itemParentId(store.state, itemId); if (parent) { instance.focusItem(event, parent); event.preventDefault(); } } break; } // Focuses the first item in the tree case key === 'Home': { // Multi select behavior when pressing Ctrl + Shift + Home // Selects the focused item and all items up to the first item. if (canToggleItemSelection(itemId) && isMultiSelectEnabled && ctrlPressed && event.shiftKey) { instance.selectRangeFromStartToItem(event, itemId); } else { instance.focusItem(event, (0, _tree.getFirstNavigableItem)(store.state)); } event.preventDefault(); break; } // Focuses the last item in the tree case key === 'End': { // Multi select behavior when pressing Ctrl + Shirt + End // Selects the focused item and all the items down to the last item. if (canToggleItemSelection(itemId) && isMultiSelectEnabled && ctrlPressed && event.shiftKey) { instance.selectRangeFromItemToEnd(event, itemId); } else { instance.focusItem(event, (0, _tree.getLastNavigableItem)(store.state)); } event.preventDefault(); break; } // Expand all siblings that are at the same level as the focused item case key === '*': { instance.expandAllSiblings(event, itemId); event.preventDefault(); break; } // Multi select behavior when pressing Ctrl + a // Selects all the items case String.fromCharCode(event.keyCode) === 'A' && ctrlPressed && isMultiSelectEnabled && _useTreeViewSelection.selectionSelectors.enabled(store.state): { instance.selectAllNavigableItems(event); event.preventDefault(); break; } // Type-ahead case !ctrlPressed && !event.shiftKey && isPrintableKey(key): { typeaheadTimeout.clear(); const matchingItem = getFirstMatchingItem(itemId, key); if (matchingItem != null) { instance.focusItem(event, matchingItem); event.preventDefault(); } else { typeaheadQueryRef.current = ''; } typeaheadTimeout.start(TYPEAHEAD_TIMEOUT, () => { typeaheadQueryRef.current = ''; }); break; } } }; return { instance: { updateLabelMap, handleItemKeyDown } }; }; exports.useTreeViewKeyboardNavigation = useTreeViewKeyboardNavigation; useTreeViewKeyboardNavigation.params = {};