UNPKG

@mskcc/carbon-react

Version:

Carbon react components for the MSKCC DSM

311 lines (302 loc) 9.92 kB
/** * MSKCC 2021, 2024 */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var React = require('react'); var PropTypes = require('prop-types'); var iconsReact = require('@carbon/icons-react'); var cx = require('classnames'); var uniqueId = require('../../tools/uniqueId.js'); var usePrefix = require('../../internal/usePrefix.js'); var match = require('../../internal/keyboard/match.js'); var keys = require('../../internal/keyboard/keys.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes); var cx__default = /*#__PURE__*/_interopDefaultLegacy(cx); 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 } = React.useRef(rest.id || uniqueId["default"]()); const [expanded, setExpanded] = React.useState(isExpanded); const currentNode = React.useRef(null); const currentNodeLabel = React.useRef(null); const prefix = usePrefix.usePrefix(); const nodesWithProps = React__default["default"].Children.map(children, node => { if ( /*#__PURE__*/React__default["default"].isValidElement(node)) { return /*#__PURE__*/React__default["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__default["default"](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__default["default"](`${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 (match.matches(event, [keys.ArrowLeft, keys.ArrowRight, keys.Enter])) { event.stopPropagation(); } if (match.match(event, keys.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.match(event, keys.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 (match.matches(event, [keys.Enter, keys.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); } React.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["default"].createElement("li", treeNodeProps, /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--tree-node__label`, ref: currentNodeLabel }, Icon && /*#__PURE__*/React__default["default"].createElement(Icon, { className: `${prefix}--tree-node__icon` }), label)); } return ( /*#__PURE__*/ // eslint-disable-next-line jsx-a11y/role-supports-aria-props React__default["default"].createElement("li", _rollupPluginBabelHelpers["extends"]({}, treeNodeProps, { "aria-expanded": !!expanded }), /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--tree-node__label`, ref: currentNodeLabel }, /*#__PURE__*/React__default["default"].createElement("span", { className: `${prefix}--tree-parent-node__toggle`, disabled: disabled, onClick: handleToggleClick }, /*#__PURE__*/React__default["default"].createElement(iconsReact.CaretDown, { className: toggleClasses })), /*#__PURE__*/React__default["default"].createElement("span", { className: `${prefix}--tree-node__label__details` }, Icon && /*#__PURE__*/React__default["default"].createElement(Icon, { className: `${prefix}--tree-node__icon` }), label)), expanded && /*#__PURE__*/React__default["default"].createElement("ul", { role: "group", className: `${prefix}--tree-node__children` }, nodesWithProps)) ); } TreeNode.propTypes = { /** * The value of the active node in the tree */ active: PropTypes__default["default"].oneOfType([PropTypes__default["default"].string, PropTypes__default["default"].number]), /** * Specify the children of the TreeNode */ children: PropTypes__default["default"].node, /** * Specify an optional className to be applied to the TreeNode */ className: PropTypes__default["default"].string, /** * TreeNode depth to determine spacing, automatically calculated by default */ depth: PropTypes__default["default"].number, /** * Specify if the TreeNode is disabled */ disabled: PropTypes__default["default"].bool, /** * Specify if the TreeNode is expanded (only applicable to parent nodes) */ isExpanded: PropTypes__default["default"].bool, /** * Rendered label for the TreeNode */ label: PropTypes__default["default"].node, /** * Callback function for when the node receives or loses focus */ onNodeFocusEvent: PropTypes__default["default"].func, /** * Callback function for when the node is selected */ onSelect: PropTypes__default["default"].func, /** * Callback function for when a parent node is expanded or collapsed */ onToggle: PropTypes__default["default"].func, /** * Callback function for when any node in the tree is selected */ onTreeSelect: PropTypes__default["default"].func, /** * Optional prop to allow each node to have an associated icon. * Can be a React component class */ renderIcon: PropTypes__default["default"].oneOfType([PropTypes__default["default"].func, PropTypes__default["default"].object]), /** * Array containing all selected node IDs in the tree */ selected: PropTypes__default["default"].arrayOf(PropTypes__default["default"].oneOfType([PropTypes__default["default"].string, PropTypes__default["default"].number])), /** * Specify the value of the TreeNode */ value: PropTypes__default["default"].string }; exports["default"] = TreeNode;