@fluentui/react-northstar
Version:
A themable React component library.
234 lines (226 loc) • 9.63 kB
JavaScript
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