UNPKG

@itwin/itwinui-react

Version:

A react component library for iTwinUI

244 lines (243 loc) 6.76 kB
import * as React from 'react'; import { getFocusableElements, Box, useMergedRefs, mergeEventHandlers, } from '../../utils/index.js'; import cx from 'classnames'; import { TreeNodeExpander } from './TreeNodeExpander.js'; import { useTreeContext, VirtualizedTreeContext } from './TreeContext.js'; export const TreeNode = React.forwardRef((props, forwardedRef) => { let { nodeId, nodeProps = {}, label, labelProps = {}, sublabel, sublabelProps = {}, children, className, icon, iconProps = {}, hasSubNodes = false, isDisabled = false, isExpanded = false, isSelected = false, onSelected, onExpanded, checkbox, checkboxProps = {}, subTreeProps = {}, contentProps = {}, titleProps = {}, expanderProps = {}, expander, ...rest } = props; let { nodeDepth, subNodeIds = [], parentNodeId, scrollToParent, groupSize, indexInGroup, } = useTreeContext(); let { virtualizer, onVirtualizerChange } = React.useContext(VirtualizedTreeContext) ?? {}; let [isFocused, setIsFocused] = React.useState(false); let nodeRef = React.useRef(null); let onKeyDown = (event) => { if (event.altKey) return; let isNodeFocused = nodeRef.current === nodeRef.current?.ownerDocument.activeElement; switch (event.key) { case 'ArrowLeft': { event.preventDefault(); if (isNodeFocused) { if (isExpanded) { onExpanded(nodeId, false); onVirtualizerChange?.(virtualizer); break; } if (parentNodeId) scrollToParent?.(); break; } let focusableElements = getFocusableElements(nodeRef.current); let currentIndex = focusableElements.indexOf( nodeRef.current?.ownerDocument.activeElement, ); if (0 === currentIndex) nodeRef.current?.focus(); else focusableElements[currentIndex - 1]?.focus(); break; } case 'ArrowRight': { event.preventDefault(); let focusableElements = getFocusableElements(nodeRef.current); if (isNodeFocused) { if (!isExpanded && hasSubNodes) { onExpanded(nodeId, true); onVirtualizerChange?.(virtualizer); break; } focusableElements[0]?.focus(); break; } let currentIndex = focusableElements.indexOf( nodeRef.current?.ownerDocument.activeElement, ); if (currentIndex < focusableElements.length - 1) focusableElements[currentIndex + 1].focus(); break; } case ' ': case 'Spacebar': case 'Enter': if (event.target !== nodeRef.current) break; event.preventDefault(); if (!isDisabled) onSelected?.(nodeId, !isSelected); break; default: break; } }; let onExpanderClick = React.useCallback( (event) => { onExpanded(nodeId, !isExpanded); onVirtualizerChange?.(virtualizer); event.stopPropagation(); }, [isExpanded, nodeId, onExpanded, onVirtualizerChange, virtualizer], ); return React.createElement( Box, { as: 'div', role: 'treeitem', className: cx('iui-tree-item', className), 'aria-expanded': hasSubNodes ? isExpanded : void 0, 'aria-disabled': isDisabled, 'aria-selected': isSelected, 'aria-level': nodeDepth + 1, 'aria-setsize': groupSize, 'aria-posinset': indexInGroup + 1, tabIndex: -1, ...rest, id: nodeId, ref: useMergedRefs(nodeRef, forwardedRef), onFocus: mergeEventHandlers(props.onFocus, (e) => { setIsFocused(true); e.stopPropagation(); }), onBlur: mergeEventHandlers(props.onBlur, () => { setIsFocused(false); }), onKeyDown: mergeEventHandlers(props.onKeyDown, onKeyDown), }, React.createElement( Box, { as: 'div', style: { '--level': nodeDepth, }, onClick: () => !isDisabled && onSelected?.(nodeId, !isSelected), ...nodeProps, className: cx( 'iui-tree-node', { 'iui-active': isSelected, 'iui-disabled': isDisabled, }, nodeProps?.className, ), }, checkbox && React.createElement( Box, { as: 'div', ...checkboxProps, className: cx('iui-tree-node-checkbox', checkboxProps?.className), }, React.isValidElement(checkbox) ? React.cloneElement(checkbox, { tabIndex: isFocused ? 0 : -1, }) : checkbox, ), React.createElement( Box, { as: 'div', ...contentProps, className: cx('iui-tree-node-content', contentProps?.className), }, hasSubNodes && expander, hasSubNodes && !expander && React.createElement(TreeNodeExpander, { isExpanded: isExpanded, disabled: isDisabled, onClick: onExpanderClick, tabIndex: isFocused ? 0 : -1, ...expanderProps, }), icon && React.createElement( Box, { as: 'span', 'aria-hidden': true, ...iconProps, className: cx('iui-tree-node-content-icon', iconProps?.className), }, icon, ), React.createElement( Box, { as: 'div', ...labelProps, className: cx('iui-tree-node-content-label', labelProps?.className), }, React.createElement( Box, { as: 'div', ...titleProps, className: cx( 'iui-tree-node-content-title', titleProps?.className, ), }, label, ), sublabel && React.createElement( Box, { as: 'div', ...sublabelProps, className: cx( 'iui-tree-node-content-caption', sublabelProps?.className, ), }, sublabel, ), ), children, ), ), hasSubNodes && React.createElement(Box, { as: 'div', role: 'group', 'aria-owns': subNodeIds.join(' '), ...subTreeProps, className: cx('iui-sub-tree', subTreeProps?.className), }), ); }); if ('development' === process.env.NODE_ENV) TreeNode.displayName = 'TreeNode';