@carbon/react
Version:
React components for the Carbon Design System
254 lines (244 loc) • 9.76 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
;
Object.defineProperty(exports, '__esModule', { value: true });
var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js');
var cx = require('classnames');
var PropTypes = require('prop-types');
var React = require('react');
var keys = require('../../internal/keyboard/keys.js');
var match = require('../../internal/keyboard/match.js');
var useControllableState = require('../../internal/useControllableState.js');
var usePrefix = require('../../internal/usePrefix.js');
var useId = require('../../internal/useId.js');
var index = require('../FeatureFlags/index.js');
var TreeNode = require('./TreeNode.js');
var TreeContext = require('./TreeContext.js');
const TreeView = ({
active: prespecifiedActive,
children,
className,
hideLabel = false,
label,
multiselect = false,
onActivate,
onSelect,
selected: preselected,
size = 'sm',
...rest
}) => {
const enableTreeviewControllable = index.useFeatureFlag('enable-treeview-controllable');
// eslint-disable-next-line react-hooks/rules-of-hooks -- https://github.com/carbon-design-system/carbon/issues/20452
const {
current: treeId
} = React.useRef(rest.id || useId.useId());
const prefix = usePrefix.usePrefix();
const treeClasses = cx(className, `${prefix}--tree`, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- https://github.com/carbon-design-system/carbon/issues/20452
// @ts-ignore - will always be false according to prop types
[`${prefix}--tree--${size}`]: size !== 'default'
});
const treeRootRef = React.useRef(null);
const treeWalker = React.useRef(null);
const controllableSelectionState = useControllableState.useControllableState({
value: preselected,
onChange: onSelect,
defaultValue: []
});
const uncontrollableSelectionState = React.useState(preselected ?? []);
const [selected, setSelected] = enableTreeviewControllable ? controllableSelectionState : uncontrollableSelectionState;
const controllableActiveState = useControllableState.useControllableState({
value: prespecifiedActive,
onChange: onActivate,
defaultValue: undefined
});
const uncontrollableActiveState = React.useState(prespecifiedActive);
const [active, setActive] = enableTreeviewControllable ? controllableActiveState : uncontrollableActiveState;
function resetNodeTabIndices() {
Array.prototype.forEach.call(treeRootRef?.current?.querySelectorAll('[tabIndex="0"]') ?? [], item => {
item.tabIndex = -1;
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- https://github.com/carbon-design-system/carbon/issues/20452
function handleTreeSelect(event, node) {
const nodeId = node.id;
if (nodeId) {
if (multiselect && (event.metaKey || event.ctrlKey)) {
if (!selected.includes(nodeId)) {
setSelected(selected.concat(nodeId));
} else {
setSelected(selected.filter(selectedId => selectedId !== nodeId));
}
if (!enableTreeviewControllable) {
onSelect?.(event, node);
}
} else {
setSelected([nodeId]);
setActive(nodeId);
if (!enableTreeviewControllable) {
onSelect?.(event, {
activeNodeId: nodeId,
...node
});
}
}
}
}
// Set the first non-disabled node to be tabbable
React.useEffect(() => {
const firstNode = treeRootRef.current?.querySelector(`.${prefix}--tree-node:not(.${prefix}--tree-node--disabled)`);
if (firstNode instanceof HTMLElement) {
firstNode.tabIndex = 0;
}
}, [children, prefix]);
function handleKeyDown(event) {
event.stopPropagation();
if (match.matches(event, [keys.ArrowUp, keys.ArrowDown, keys.Home, keys.End])) {
event.preventDefault();
}
if (!treeWalker.current) {
return;
}
treeWalker.current.currentNode = event.target;
let nextFocusNode = null;
if (match.match(event, keys.ArrowUp)) {
nextFocusNode = treeWalker.current.previousNode();
}
if (match.match(event, keys.ArrowDown)) {
nextFocusNode = treeWalker.current.nextNode();
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- https://github.com/carbon-design-system/carbon/issues/20452
// @ts-ignore - `matches` doesn't like the object syntax without missing properties
if (match.matches(event, [keys.Home, keys.End, {
code: 'KeyA'
}])) {
const nodeIds = [];
if (match.matches(event, [keys.Home, keys.End])) {
if (multiselect && event.shiftKey && event.ctrlKey && treeWalker.current.currentNode instanceof Element && !treeWalker.current.currentNode.getAttribute('aria-disabled') && !treeWalker.current.currentNode.classList.contains(`${prefix}--tree-node--hidden`)) {
nodeIds.push(treeWalker.current.currentNode.id);
}
while (match.match(event, keys.Home) ? treeWalker.current.previousNode() : treeWalker.current.nextNode()) {
nextFocusNode = treeWalker.current.currentNode;
if (multiselect && event.shiftKey && event.ctrlKey && nextFocusNode instanceof Element && !nextFocusNode.getAttribute('aria-disabled') && !nextFocusNode.classList.contains(`${prefix}--tree-node--hidden`)) {
nodeIds.push(nextFocusNode.id);
}
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- https://github.com/carbon-design-system/carbon/issues/20452
// @ts-ignore - `matches` doesn't like the object syntax without missing properties
if (match.match(event, {
code: 'KeyA'
}) && event.ctrlKey) {
treeWalker.current.currentNode = treeWalker.current.root;
while (treeWalker.current.nextNode()) {
if (treeWalker.current.currentNode instanceof Element && !treeWalker.current.currentNode.getAttribute('aria-disabled') && !treeWalker.current.currentNode.classList.contains(`${prefix}--tree-node--hidden`)) {
nodeIds.push(treeWalker.current.currentNode.id);
}
}
}
setSelected(selected.concat(nodeIds));
}
if (nextFocusNode && nextFocusNode !== event.target) {
resetNodeTabIndices();
if (nextFocusNode instanceof HTMLElement) {
nextFocusNode.tabIndex = 0;
nextFocusNode.focus();
}
}
rest?.onKeyDown?.(event);
}
React.useEffect(() => {
if (treeRootRef.current && !treeWalker.current) {
treeWalker.current = document.createTreeWalker(treeRootRef.current, NodeFilter.SHOW_ELEMENT, {
acceptNode: function (node) {
if (!(node instanceof Element)) {
return NodeFilter.FILTER_SKIP;
}
if (node.classList.contains(`${prefix}--tree-node--disabled`) || node.classList.contains(`${prefix}--tree-node--hidden`)) {
return NodeFilter.FILTER_REJECT;
}
if (node.matches(`.${prefix}--tree-node`)) {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_SKIP;
}
});
}
}, [prefix]);
const labelId = `${treeId}__label`;
const TreeLabel = () => !hideLabel ? /*#__PURE__*/React.createElement("label", {
id: labelId,
className: `${prefix}--label`
}, label) : null;
const treeContextValue = React.useMemo(() => ({
active,
multiselect,
onActivate: setActive,
onTreeSelect: handleTreeSelect,
selected,
size
}), [active, multiselect, setActive, handleTreeSelect, selected, size]);
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(TreeLabel, null), /*#__PURE__*/React.createElement(TreeContext.TreeContext.Provider, {
value: treeContextValue
}, /*#__PURE__*/React.createElement(TreeContext.DepthContext.Provider, {
value: 0
}, /*#__PURE__*/React.createElement("ul", _rollupPluginBabelHelpers.extends({}, rest, {
"aria-label": hideLabel ? label : undefined,
"aria-labelledby": !hideLabel ? labelId : undefined,
"aria-multiselectable": multiselect || undefined,
className: treeClasses,
onKeyDown: handleKeyDown,
ref: treeRootRef,
role: "tree"
}), children))));
};
TreeView.propTypes = {
/**
* Mark the active node in the tree, represented by its ID
*/
active: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* Specify the children of the TreeView
*/
children: PropTypes.node,
/**
* Specify an optional className to be applied to the TreeView
*/
className: PropTypes.string,
/**
* Specify whether or not the label should be hidden
*/
hideLabel: PropTypes.bool,
/**
* Provide the label text that will be read by a screen reader
*/
label: PropTypes.string.isRequired,
/**
* **[Experimental]** Specify the selection mode of the tree.
* If `multiselect` is `false` then only one node can be selected at a time
*/
multiselect: PropTypes.bool,
/**
* **[Experimental]** Callback function that is called when any node is activated.
* *This is only supported with the `enable-treeview-controllable` feature flag!*
*/
onActivate: PropTypes.func,
/**
* Callback function that is called when any node is selected
*/
onSelect: PropTypes.func,
/**
* Array representing all selected node IDs in the tree
*/
selected: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
/**
* Specify the size of the tree from a list of available sizes.
*/
size: PropTypes.oneOf(['xs', 'sm'])
};
TreeView.TreeNode = TreeNode.default;
exports.default = TreeView;