UNPKG

@mskcc/carbon-react

Version:

Carbon react components for the MSKCC DSM

301 lines (296 loc) 8.85 kB
/** * MSKCC 2021, 2024 */ import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js'; import React__default, { useRef, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { CaretDown } from '@carbon/icons-react'; import cx from 'classnames'; import uniqueId from '../../tools/uniqueId.js'; import { usePrefix } from '../../internal/usePrefix.js'; import { matches, match } from '../../internal/keyboard/match.js'; import { ArrowLeft, ArrowRight, Enter, Space } from '../../internal/keyboard/keys.js'; function TreeNode(_ref) { let { active, children, className, depth, disabled, isExpanded, label, onNodeFocusEvent, onSelect: onNodeSelect, onToggle, onTreeSelect, renderIcon: Icon, selected, value, ...rest } = _ref; const { current: id } = useRef(rest.id || uniqueId()); const [expanded, setExpanded] = useState(isExpanded); const currentNode = useRef(null); const currentNodeLabel = useRef(null); const prefix = usePrefix(); const nodesWithProps = React__default.Children.map(children, node => { if ( /*#__PURE__*/React__default.isValidElement(node)) { return /*#__PURE__*/React__default.cloneElement(node, { active, depth: depth + 1, disabled, onTreeSelect, selected, tabIndex: !node.props.disabled && -1 || null }); } }); const isActive = active === id; const isSelected = selected.includes(id); const treeNodeClasses = cx(className, `${prefix}--tree-node`, { [`${prefix}--tree-node--active`]: isActive, [`${prefix}--tree-node--disabled`]: disabled, [`${prefix}--tree-node--selected`]: isSelected, [`${prefix}--tree-node--with-icon`]: Icon, [`${prefix}--tree-leaf-node`]: !children, [`${prefix}--tree-parent-node`]: children }); const toggleClasses = cx(`${prefix}--tree-parent-node__toggle-icon`, { [`${prefix}--tree-parent-node__toggle-icon--expanded`]: expanded }); function handleToggleClick(event) { if (disabled) { return; } onToggle?.(event, { id, isExpanded: !expanded, label, value }); setExpanded(!expanded); } function handleClick(event) { event.stopPropagation(); if (!disabled) { onTreeSelect?.(event, { id, label, value }); onNodeSelect?.(event, { id, label, value }); rest?.onClick?.(event); } } function handleKeyDown(event) { if (disabled) { return; } if (matches(event, [ArrowLeft, ArrowRight, Enter])) { event.stopPropagation(); } if (match(event, ArrowLeft)) { const findParentTreeNode = node => { if (node.classList.contains(`${prefix}--tree-parent-node`)) { return node; } if (node.classList.contains(`${prefix}--tree`)) { return null; } return findParentTreeNode(node.parentNode); }; if (children && expanded) { onToggle?.(event, { id, isExpanded: false, label, value }); setExpanded(false); } else { /** * When focus is on a leaf node or a closed parent node, move focus to * its parent node (unless its depth is level 1) */ findParentTreeNode(currentNode.current.parentNode)?.focus(); } } if (children && match(event, ArrowRight)) { if (expanded) { /** * When focus is on an expanded parent node, move focus to the first * child node */ currentNode.current.lastChild.firstChild.focus(); } else { onToggle?.(event, { id, isExpanded: true, label, value }); setExpanded(true); } } if (matches(event, [Enter, Space])) { event.preventDefault(); handleClick(event); } rest?.onKeyDown?.(event); } function handleFocusEvent(event) { if (event.type === 'blur') { rest?.onBlur?.(event); } if (event.type === 'focus') { rest?.onFocus?.(event); } onNodeFocusEvent?.(event); } useEffect(() => { /** * Negative margin shifts node to align with the left side boundary of the * tree * Dynamically calculate padding to recreate tree node indentation * - parent nodes with icon have (depth + 1rem + depth * 0.5) left padding * - parent nodes have (depth + 1rem) left padding * - leaf nodes have (depth + 2.5rem) left padding without icons (because * of expand icon + spacing) * - leaf nodes have (depth + 2rem + depth * 0.5) left padding with icons (because of * reduced spacing between the expand icon and the node icon + label) */ const calcOffset = () => { // parent node with icon if (children && Icon) { return depth + 1 + depth * 0.5; } // parent node without icon if (children) { return depth + 1; } // leaf node with icon if (Icon) { return depth + 2 + depth * 0.5; } // leaf node without icon return depth + 2.5; }; if (currentNodeLabel.current) { currentNodeLabel.current.style.marginLeft = `-${calcOffset()}rem`; currentNodeLabel.current.style.paddingLeft = `${calcOffset()}rem`; } // sync props and state setExpanded(isExpanded); }, [children, depth, Icon, isExpanded]); const treeNodeProps = { ...rest, ['aria-current']: isActive || null, ['aria-selected']: disabled ? null : isSelected, ['aria-disabled']: disabled, className: treeNodeClasses, id, onBlur: handleFocusEvent, onClick: handleClick, onFocus: handleFocusEvent, onKeyDown: handleKeyDown, ref: currentNode, role: 'treeitem' }; if (!children) { return /*#__PURE__*/React__default.createElement("li", treeNodeProps, /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--tree-node__label`, ref: currentNodeLabel }, Icon && /*#__PURE__*/React__default.createElement(Icon, { className: `${prefix}--tree-node__icon` }), label)); } return ( /*#__PURE__*/ // eslint-disable-next-line jsx-a11y/role-supports-aria-props React__default.createElement("li", _extends({}, treeNodeProps, { "aria-expanded": !!expanded }), /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--tree-node__label`, ref: currentNodeLabel }, /*#__PURE__*/React__default.createElement("span", { className: `${prefix}--tree-parent-node__toggle`, disabled: disabled, onClick: handleToggleClick }, /*#__PURE__*/React__default.createElement(CaretDown, { className: toggleClasses })), /*#__PURE__*/React__default.createElement("span", { className: `${prefix}--tree-node__label__details` }, Icon && /*#__PURE__*/React__default.createElement(Icon, { className: `${prefix}--tree-node__icon` }), label)), expanded && /*#__PURE__*/React__default.createElement("ul", { role: "group", className: `${prefix}--tree-node__children` }, nodesWithProps)) ); } TreeNode.propTypes = { /** * The value of the active node in the tree */ active: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * Specify the children of the TreeNode */ children: PropTypes.node, /** * Specify an optional className to be applied to the TreeNode */ className: PropTypes.string, /** * TreeNode depth to determine spacing, automatically calculated by default */ depth: PropTypes.number, /** * Specify if the TreeNode is disabled */ disabled: PropTypes.bool, /** * Specify if the TreeNode is expanded (only applicable to parent nodes) */ isExpanded: PropTypes.bool, /** * Rendered label for the TreeNode */ label: PropTypes.node, /** * Callback function for when the node receives or loses focus */ onNodeFocusEvent: PropTypes.func, /** * Callback function for when the node is selected */ onSelect: PropTypes.func, /** * Callback function for when a parent node is expanded or collapsed */ onToggle: PropTypes.func, /** * Callback function for when any node in the tree is selected */ onTreeSelect: PropTypes.func, /** * Optional prop to allow each node to have an associated icon. * Can be a React component class */ renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), /** * Array containing all selected node IDs in the tree */ selected: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), /** * Specify the value of the TreeNode */ value: PropTypes.string }; export { TreeNode as default };