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