@itwin/itwinui-react
Version:
A react component library for iTwinUI
284 lines (283 loc) • 8.74 kB
JavaScript
import * as React from 'react';
import {
getFocusableElements,
polymorphic,
cloneElementWithRef,
useVirtualScroll,
ShadowRoot,
useMergedRefs,
useLayoutEffect,
} from '../../utils/index.js';
import { TreeContext, VirtualizedTreeContext } from './TreeContext.js';
export const Tree = (props) => {
let {
data,
className,
nodeRenderer,
getNode,
size = 'default',
enableVirtualization = false,
style,
...rest
} = props;
let treeRef = React.useRef(null);
let focusedIndex = React.useRef(0);
React.useEffect(() => {
focusedIndex.current = 0;
}, [data]);
let getFocusableNodes = React.useCallback(() => {
let focusableItems = getFocusableElements(treeRef.current);
return focusableItems.filter(
(i) => !focusableItems.some((p) => p.contains(i.parentElement)),
);
}, []);
let handleKeyDown = (event) => {
if (event.altKey) return;
let items = getFocusableNodes();
if (!items?.length) return;
let activeIndex = items.findIndex((el) =>
el.contains(treeRef.current?.ownerDocument.activeElement),
);
let currentIndex = activeIndex > -1 ? activeIndex : 0;
switch (event.key) {
case 'ArrowUp': {
event.preventDefault();
let newIndex = Math.max(0, currentIndex - 1);
items[newIndex].focus();
focusedIndex.current = newIndex;
break;
}
case 'ArrowDown': {
event.preventDefault();
let newIndex = Math.min(items.length - 1, currentIndex + 1);
items[newIndex].focus();
focusedIndex.current = newIndex;
break;
}
default:
break;
}
};
let [flatNodesList, firstLevelNodesList] = React.useMemo(() => {
let flatList = [];
let firstLevelNodes = [];
let flattenNodes = (nodes = [], depth = 0, parentNode) => {
let nodeIdList = Array();
nodes.forEach((element, index) => {
let { subNodes, ...nodeProps } = getNode(element);
let flatNode = {
nodeProps,
depth,
parentNode,
indexInGroup: index,
};
nodeIdList.push(flatNode.nodeProps.nodeId);
flatList.push(flatNode);
if (0 === depth) firstLevelNodes.push(flatNode);
if (flatNode.nodeProps.isExpanded) {
let subNodeIds = flattenNodes(subNodes, depth + 1, flatNode);
flatNode.subNodeIds = subNodeIds;
}
});
return nodeIdList;
};
flattenNodes(data);
return [flatList, firstLevelNodes];
}, [data, getNode]);
let itemRenderer = React.useCallback(
(index, virtualItem, virtualizer) => {
let node = flatNodesList[index];
return React.createElement(
TreeContext.Provider,
{
key: node.nodeProps.nodeId,
value: {
nodeDepth: node.depth,
subNodeIds: node.subNodeIds,
groupSize:
0 === node.depth
? firstLevelNodesList.length
: node.parentNode?.subNodeIds?.length ?? 0,
indexInGroup: node.indexInGroup,
parentNodeId: node.parentNode?.nodeProps.nodeId,
scrollToParent: node.parentNode
? () => {
let parentNodeId = node.parentNode?.nodeProps.nodeId;
let parentNodeIndex = flatNodesList.findIndex(
(n) => n.nodeProps.nodeId === parentNodeId,
);
setScrollToIndex(parentNodeIndex);
}
: void 0,
size,
},
},
virtualItem && virtualizer
? cloneElementWithRef(nodeRenderer(node.nodeProps), (children) => ({
...children.props,
key: virtualItem.key,
'data-iui-index': virtualItem.index,
'data-iui-virtualizer': 'item',
ref: virtualizer.measureElement,
style: {
...children.props.style,
'--_iui-width': '100%',
transform: `translateY(${virtualItem.start}px)`,
},
}))
: nodeRenderer(node.nodeProps),
);
},
[firstLevelNodesList.length, flatNodesList, nodeRenderer, size],
);
let [scrollToIndex, setScrollToIndex] = React.useState();
let flatNodesListRef = React.useRef(flatNodesList);
React.useEffect(() => {
flatNodesListRef.current = flatNodesList;
}, [flatNodesList]);
React.useEffect(() => {
setTimeout(() => {
if (void 0 !== scrollToIndex) {
let nodeId = flatNodesListRef.current[scrollToIndex].nodeProps.nodeId;
let nodeElement = treeRef.current?.ownerDocument.querySelector(
`#${nodeId}`,
);
nodeElement?.focus();
setScrollToIndex(void 0);
}
});
}, [scrollToIndex]);
let handleFocus = (event) => {
if (treeRef.current?.contains(event.relatedTarget)) return;
let items = getFocusableNodes();
if (items.length > 0) items[focusedIndex.current]?.focus();
};
return React.createElement(
React.Fragment,
null,
enableVirtualization
? React.createElement(VirtualizedTree, {
flatNodesList: flatNodesList,
itemRenderer: itemRenderer,
scrollToIndex: scrollToIndex,
onFocus: handleFocus,
onKeyDown: handleKeyDown,
ref: treeRef,
className: className,
'data-iui-size': 'small' === size ? 'small' : void 0,
style: style,
...rest,
})
: React.createElement(
TreeElement,
{
onKeyDown: handleKeyDown,
onFocus: handleFocus,
className: className,
'data-iui-size': 'small' === size ? 'small' : void 0,
style: style,
ref: treeRef,
...rest,
},
flatNodesList.map((_, i) => itemRenderer(i)),
),
);
};
if ('development' === process.env.NODE_ENV) Tree.displayName = 'Tree';
let TreeElement = polymorphic.div('iui-tree', {
role: 'tree',
tabIndex: 0,
});
let VirtualizedTree = React.forwardRef(
({ flatNodesList, itemRenderer, scrollToIndex, ...rest }, ref) => {
let parentRef = React.useRef(null);
let virtualizerRootRef = React.useRef(null);
let getItemKey = React.useCallback(
(index) => flatNodesList[index].nodeProps.nodeId,
[flatNodesList],
);
let onVirtualizerChange = React.useMemo(
() =>
debounce((virtualizer) => {
if (!virtualizer || !virtualizerRootRef.current) return;
virtualizerRootRef.current.style.width = '';
let widestNodeWidth = 0;
virtualizer.elementsCache.forEach((el) => {
if (el.clientWidth > widestNodeWidth)
widestNodeWidth = el.clientWidth;
});
if (widestNodeWidth)
virtualizerRootRef.current.style.width = `${widestNodeWidth}px`;
}, 100),
[],
);
let { virtualizer, css: virtualizerCss } = useVirtualScroll({
count: flatNodesList.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 39,
getItemKey,
onChange: onVirtualizerChange,
});
useLayoutEffect(() => {
if (scrollToIndex) virtualizer.scrollToIndex(scrollToIndex);
}, [virtualizer, scrollToIndex]);
return React.createElement(
TreeElement,
{
...rest,
ref: useMergedRefs(ref, parentRef),
},
React.createElement(
'div',
{
style: {
display: 'contents',
},
},
React.createElement(
ShadowRoot,
{
css: virtualizerCss,
},
React.createElement(
'div',
{
'data-iui-virtualizer': 'root',
style: {
minBlockSize: virtualizer.getTotalSize(),
},
ref: virtualizerRootRef,
},
React.createElement('slot', null),
),
),
React.createElement(
VirtualizedTreeContext.Provider,
{
value: React.useMemo(
() => ({
virtualizer,
onVirtualizerChange,
}),
[virtualizer, onVirtualizerChange],
),
},
virtualizer
.getVirtualItems()
.map((virtualItem) =>
itemRenderer(virtualItem.index, virtualItem, virtualizer),
),
),
),
);
},
);
function debounce(callback, delay) {
let timeoutId;
return (...args) => {
if (timeoutId) window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
callback(...args);
}, delay);
};
}