@carbon/react
Version:
React components for the Carbon Design System
180 lines (178 loc) • 8.21 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const require_runtime = require("../../_virtual/_rolldown/runtime.js");
const require_usePrefix = require("../../internal/usePrefix.js");
const require_keys = require("../../internal/keyboard/keys.js");
const require_match = require("../../internal/keyboard/match.js");
const require_useId = require("../../internal/useId.js");
const require_index = require("../FeatureFlags/index.js");
const require_useControllableState = require("../../internal/useControllableState.js");
const require_TreeContext = require("./TreeContext.js");
const require_TreeNode = require("./TreeNode.js");
let classnames = require("classnames");
classnames = require_runtime.__toESM(classnames);
let react = require("react");
react = require_runtime.__toESM(react);
let prop_types = require("prop-types");
prop_types = require_runtime.__toESM(prop_types);
let react_jsx_runtime = require("react/jsx-runtime");
//#region src/components/TreeView/TreeView.tsx
/**
* Copyright IBM Corp. 2016, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const TreeView = ({ active: prespecifiedActive, children, className, hideLabel = false, label, multiselect = false, onActivate, onSelect, selected: preselected, size = "sm", ...rest }) => {
const enableTreeviewControllable = require_index.useFeatureFlag("enable-treeview-controllable");
const generatedId = require_useId.useId();
const { current: treeId } = (0, react.useRef)(rest.id ?? generatedId);
const prefix = require_usePrefix.usePrefix();
const treeClasses = (0, classnames.default)(className, `${prefix}--tree`, { [`${prefix}--tree--${size}`]: size !== "default" });
const treeRootRef = (0, react.useRef)(null);
const treeWalker = (0, react.useRef)(null);
const controllableSelectionState = require_useControllableState.useControllableState({
value: preselected,
onChange: onSelect,
defaultValue: []
});
const uncontrollableSelectionState = (0, react.useState)(preselected ?? []);
const [selected, setSelected] = enableTreeviewControllable ? controllableSelectionState : uncontrollableSelectionState;
const controllableActiveState = require_useControllableState.useControllableState({
value: prespecifiedActive,
onChange: onActivate,
defaultValue: void 0
});
const uncontrollableActiveState = (0, react.useState)(prespecifiedActive);
const [active, setActive] = enableTreeviewControllable ? controllableActiveState : uncontrollableActiveState;
function resetNodeTabIndices() {
Array.prototype.forEach.call(treeRootRef?.current?.querySelectorAll("[tabIndex=\"0\"]") ?? [], (item) => {
item.tabIndex = -1;
});
}
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
});
}
}
(0, 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 (require_match.matches(event, [
require_keys.ArrowUp,
require_keys.ArrowDown,
require_keys.Home,
require_keys.End
])) event.preventDefault();
if (!treeWalker.current) return;
treeWalker.current.currentNode = event.target;
let nextFocusNode = null;
if (require_match.match(event, require_keys.ArrowUp)) nextFocusNode = treeWalker.current.previousNode();
if (require_match.match(event, require_keys.ArrowDown)) nextFocusNode = treeWalker.current.nextNode();
if (require_match.matches(event, [
require_keys.Home,
require_keys.End,
{ code: "KeyA" }
])) {
const nodeIds = [];
if (require_match.matches(event, [require_keys.Home, require_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 (require_match.match(event, require_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);
}
}
if (require_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);
}
(0, 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__ */ (0, react_jsx_runtime.jsx)("label", {
id: labelId,
className: `${prefix}--label`,
children: label
}) : null;
const treeContextValue = (0, react.useMemo)(() => ({
active,
multiselect,
onActivate: setActive,
onTreeSelect: handleTreeSelect,
selected,
size
}), [
active,
multiselect,
setActive,
handleTreeSelect,
selected,
size
]);
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeLabel, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_TreeContext.TreeContext.Provider, {
value: treeContextValue,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_TreeContext.DepthContext.Provider, {
value: 0,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
...rest,
"aria-label": hideLabel ? label : void 0,
"aria-labelledby": !hideLabel ? labelId : void 0,
"aria-multiselectable": multiselect || void 0,
className: treeClasses,
onKeyDown: handleKeyDown,
ref: treeRootRef,
role: "tree",
children
})
})
})] });
};
TreeView.propTypes = {
active: prop_types.default.oneOfType([prop_types.default.string, prop_types.default.number]),
children: prop_types.default.node,
className: prop_types.default.string,
hideLabel: prop_types.default.bool,
label: prop_types.default.string.isRequired,
multiselect: prop_types.default.bool,
onActivate: prop_types.default.func,
onSelect: prop_types.default.func,
selected: prop_types.default.arrayOf(prop_types.default.oneOfType([prop_types.default.string, prop_types.default.number])),
size: prop_types.default.oneOf(["xs", "sm"])
};
TreeView.TreeNode = require_TreeNode.default;
//#endregion
exports.default = TreeView;