UNPKG

@fluentui/react-northstar

Version:
234 lines (226 loc) 9.63 kB
import _uniq from "lodash/uniq"; import _invoke from "lodash/invoke"; import _without from "lodash/without"; import * as React from 'react'; import { useAutoControlled } from '@fluentui/react-bindings'; import { flattenTree } from './flattenTree'; /** * This hook returns a stable `getItemById()` function that will lookup in latest `flatTree`. * This is used to have stable callbacks that can be passed to React's Context. */ function useGetItemById(flatTree) { // An exception is thrown there to ensure that a proper callback will assigned to ref var callbackRef = React.useRef(function () { throw new Error('Callback is not assigned yet'); }); // We are assigning a callback during render as it can be used during render and in event handlers. In dev mode we // are freezing objects to prevent their mutations callbackRef.current = function (itemId) { return process.env.NODE_ENV === 'production' ? flatTree[itemId] : Object.freeze(flatTree[itemId]); }; return React.useCallback(function () { return callbackRef.current.apply(callbackRef, arguments); }, []); } function useStableProps(props) { var stableProps = React.useRef(props); React.useEffect(function () { stableProps.current = props; }); return stableProps; } export function useTree(options) { // We need this because we want to handle `expanded` prop on `items`, should be deprecated and removed var deprecated_initialActiveItemIds = React.useMemo(function () { return deprecated_getInitialActiveItemIds(options.items); }, // eslint-disable-next-line react-hooks/exhaustive-deps [] // initialValue only needs to be computed on mount ); var _useAutoControlled = useAutoControlled({ defaultValue: options.defaultActiveItemIds, value: options.activeItemIds, initialValue: deprecated_initialActiveItemIds // will become [] }), activeItemIds = _useAutoControlled[0], setActiveItemIdsState = _useAutoControlled[1]; // selectedItemIds is only valid for leaf nodes. // For non-leaf nodes, their 'selected' states are defered from all their descendents var _useAutoControlled2 = useAutoControlled({ defaultValue: options.defaultSelectedItemIds, value: options.selectedItemIds, initialValue: [] }), selectedItemIds = _useAutoControlled2[0], setSelectedItemIdsState = _useAutoControlled2[1]; // We want to set `visibleItemIds` to simplify rendering later var _React$useMemo = React.useMemo(function () { return flattenTree(options.items, activeItemIds, selectedItemIds); }, [activeItemIds, options.items, selectedItemIds]), flatTree = _React$useMemo.flatTree, visibleItemIds = _React$useMemo.visibleItemIds; var getItemById = useGetItemById(flatTree); var stableProps = useStableProps(options); var toggleItemActive = React.useCallback(function (e, idToToggle) { var item = getItemById(idToToggle); if (!item || !item.hasSubtree) { // leaf node does not have the concept of active/inactive return; } setActiveItemIdsState(function (activeItemIds) { var nextActiveItemIds; var isActiveId = activeItemIds.indexOf(idToToggle) !== -1; if (isActiveId) { nextActiveItemIds = _without(activeItemIds, idToToggle); } else { nextActiveItemIds = [].concat(activeItemIds, [idToToggle]); if (options.exclusive) { var _getItemById, _getItemById2, _getItemById2$childre; // remove active siblings, if any, from activeItemIds var parent = (_getItemById = getItemById(idToToggle)) == null ? void 0 : _getItemById.parent; var activeSibling = (_getItemById2 = getItemById(parent)) == null ? void 0 : (_getItemById2$childre = _getItemById2.childrenIds) == null ? void 0 : _getItemById2$childre.find(function (id) { return id !== idToToggle && nextActiveItemIds.indexOf(id) >= 0; }); if (activeSibling != null) { nextActiveItemIds = _without(nextActiveItemIds, activeSibling); } } } _invoke(stableProps.current, 'onActiveItemIdsChange', e, Object.assign({}, stableProps.current, { activeItemIds: nextActiveItemIds })); return nextActiveItemIds; }); }, [getItemById, options.exclusive, setActiveItemIdsState, stableProps]); var expandSiblings = React.useCallback(function (e, focusedItemId) { if (options.exclusive) { return; } var focusedItem = getItemById(focusedItemId); if (!focusedItem) { return; } var parentItem = getItemById(focusedItem == null ? void 0 : focusedItem.parent); var siblingsIds = parentItem == null ? void 0 : parentItem.childrenIds; if (!siblingsIds) { return; } setActiveItemIdsState(function (activeItemIds) { var nextActiveItemIds = _uniq(activeItemIds.concat(siblingsIds)); _invoke(stableProps.current, 'onActiveItemIdsChange', e, Object.assign({}, stableProps.current, { activeItemIds: nextActiveItemIds })); return nextActiveItemIds; }); }, [getItemById, options.exclusive, setActiveItemIdsState, stableProps]); var toggleItemSelect = React.useCallback(function (e, idToToggle) { var item = getItemById(idToToggle); if (!item) { return; } var leafs = getLeafNodes(getItemById, idToToggle); setSelectedItemIdsState(function (selectedItemIds) { var nextSelectedItemIds = item.selected === true ? _without.apply(void 0, [selectedItemIds].concat(leafs)) // remove all leaves from selected : _uniq(selectedItemIds.concat(leafs)); // add all leaves to selected _invoke(stableProps.current, 'onSelectedItemIdsChange', e, Object.assign({}, stableProps.current, { selectedItemIds: nextSelectedItemIds })); return nextSelectedItemIds; }); }, [getItemById, setSelectedItemIdsState, stableProps]); // Maintains stable collection of refs to avoid unnecessary React context updates var nodes = React.useRef({}); var registerItemRef = React.useCallback(function (id, node) { nodes.current[id] = node; }, []); var getItemRef = React.useCallback(function (id) { return nodes.current[id]; }, []); // can be used for keyboard navigation === var focusItemById = React.useCallback(function (id) { var itemRef = getItemRef(id); if (itemRef instanceof HTMLElement) { var _getItemById3; if ((_getItemById3 = getItemById(id)) != null && _getItemById3.hasSubtree) { itemRef.focus(); } else { var _itemRef$firstElement; // when node is leaf, need to focus on the inner treeTitle (_itemRef$firstElement = itemRef.firstElementChild) == null ? void 0 : _itemRef$firstElement.focus(); } } }, [getItemById, getItemRef]); var searchByFirstChar = React.useCallback(function (startIndex, endIndex, char) { for (var i = startIndex; i < endIndex; ++i) { var _getItemRef, _getItemRef$textConte, _getItemRef$textConte2, _getItemRef$textConte3; // get first charater of tree node using the same way aria does (https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/js/treeitemLinks.js) var itemFirstChar = (_getItemRef = getItemRef(visibleItemIds[i])) == null ? void 0 : (_getItemRef$textConte = _getItemRef.textContent) == null ? void 0 : (_getItemRef$textConte2 = _getItemRef$textConte.trim()) == null ? void 0 : (_getItemRef$textConte3 = _getItemRef$textConte2.charAt(0)) == null ? void 0 : _getItemRef$textConte3.toLowerCase(); if (itemFirstChar === char.toLowerCase()) { return i; } } return -1; }, [getItemRef, visibleItemIds]); var getToFocusIDByFirstCharacter = React.useCallback(function (e, idToStartSearch) { // Get start index for search var starIndex = visibleItemIds.indexOf(idToStartSearch) + 1; if (starIndex === visibleItemIds.length) { starIndex = 0; } // Check following nodes in tree var toFocusIndex = searchByFirstChar(starIndex, visibleItemIds.length, e.key); // If not found in following nodes, check from beginning if (toFocusIndex === -1) { toFocusIndex = searchByFirstChar(0, starIndex - 1, e.key); } if (toFocusIndex === -1) { return idToStartSearch; } return visibleItemIds[toFocusIndex]; }, [searchByFirstChar, visibleItemIds]); return { flatTree: flatTree, getItemById: getItemById, activeItemIds: activeItemIds, visibleItemIds: visibleItemIds, registerItemRef: registerItemRef, getItemRef: getItemRef, toggleItemActive: toggleItemActive, focusItemById: focusItemById, expandSiblings: expandSiblings, toggleItemSelect: toggleItemSelect, getToFocusIDByFirstCharacter: getToFocusIDByFirstCharacter }; } function deprecated_getInitialActiveItemIds(items) { if (!items) { return []; } var result = []; items.forEach(function (item) { if (item.expanded) { result.push(item.id); } if (item.items) { result = result.concat(deprecated_getInitialActiveItemIds(item.items)); } }); return result; } function getLeafNodes(getItemById, rootId) { var leafs = []; var traverseDown = function traverseDown(id) { var _getItemById4; if ((_getItemById4 = getItemById(id)) != null && _getItemById4.childrenIds) { var _getItemById5; (_getItemById5 = getItemById(id)) == null ? void 0 : _getItemById5.childrenIds.forEach(function (child) { traverseDown(child); }); } else { leafs.push(id); } }; traverseDown(rootId); return leafs; } //# sourceMappingURL=useTree.js.map