@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
311 lines (302 loc) • 9.92 kB
JavaScript
/**
* MSKCC 2021, 2024
*/
;
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;