@cloud-ru/ds-tree 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +341 -0
  4. package/dist/cjs/components/Tree/Tree.d.ts +4 -0
  5. package/dist/cjs/components/Tree/Tree.js +31 -0
  6. package/dist/cjs/components/Tree/index.d.ts +1 -0
  7. package/dist/cjs/components/Tree/index.js +17 -0
  8. package/dist/cjs/components/Tree/styles.module.css +17 -0
  9. package/dist/cjs/components/Tree/utils.d.ts +4 -0
  10. package/dist/cjs/components/Tree/utils.js +15 -0
  11. package/dist/cjs/components/index.d.ts +1 -0
  12. package/dist/cjs/components/index.js +17 -0
  13. package/dist/cjs/constants.d.ts +26 -0
  14. package/dist/cjs/constants.js +29 -0
  15. package/dist/cjs/contexts/TreeContext.d.ts +30 -0
  16. package/dist/cjs/contexts/TreeContext.js +108 -0
  17. package/dist/cjs/helperComponents/ExpandableTreeNode/ExpandableTreeNode.d.ts +11 -0
  18. package/dist/cjs/helperComponents/ExpandableTreeNode/ExpandableTreeNode.js +87 -0
  19. package/dist/cjs/helperComponents/ExpandableTreeNode/index.d.ts +1 -0
  20. package/dist/cjs/helperComponents/ExpandableTreeNode/index.js +17 -0
  21. package/dist/cjs/helperComponents/ExpandableTreeNode/styles.module.css +37 -0
  22. package/dist/cjs/helperComponents/TreeItem/TreeItem.d.ts +8 -0
  23. package/dist/cjs/helperComponents/TreeItem/TreeItem.js +16 -0
  24. package/dist/cjs/helperComponents/TreeItem/index.d.ts +1 -0
  25. package/dist/cjs/helperComponents/TreeItem/index.js +17 -0
  26. package/dist/cjs/helperComponents/TreeLine/TreeLine.d.ts +10 -0
  27. package/dist/cjs/helperComponents/TreeLine/TreeLine.js +13 -0
  28. package/dist/cjs/helperComponents/TreeLine/index.d.ts +1 -0
  29. package/dist/cjs/helperComponents/TreeLine/index.js +17 -0
  30. package/dist/cjs/helperComponents/TreeLine/styles.module.css +37 -0
  31. package/dist/cjs/helperComponents/TreeNode/TreeNode.d.ts +4 -0
  32. package/dist/cjs/helperComponents/TreeNode/TreeNode.js +187 -0
  33. package/dist/cjs/helperComponents/TreeNode/components/TreeNodeActions.d.ts +15 -0
  34. package/dist/cjs/helperComponents/TreeNode/components/TreeNodeActions.js +76 -0
  35. package/dist/cjs/helperComponents/TreeNode/components/TreeNodeHref.d.ts +7 -0
  36. package/dist/cjs/helperComponents/TreeNode/components/TreeNodeHref.js +12 -0
  37. package/dist/cjs/helperComponents/TreeNode/components/index.d.ts +2 -0
  38. package/dist/cjs/helperComponents/TreeNode/components/index.js +18 -0
  39. package/dist/cjs/helperComponents/TreeNode/constants.d.ts +5 -0
  40. package/dist/cjs/helperComponents/TreeNode/constants.js +10 -0
  41. package/dist/cjs/helperComponents/TreeNode/index.d.ts +1 -0
  42. package/dist/cjs/helperComponents/TreeNode/index.js +17 -0
  43. package/dist/cjs/helperComponents/TreeNode/styles.module.css +474 -0
  44. package/dist/cjs/helperComponents/TreeNode/types.d.ts +9 -0
  45. package/dist/cjs/helperComponents/TreeNode/types.js +2 -0
  46. package/dist/cjs/helperComponents/TreeNode/utils.d.ts +3 -0
  47. package/dist/cjs/helperComponents/TreeNode/utils.js +10 -0
  48. package/dist/cjs/helperComponents/index.d.ts +4 -0
  49. package/dist/cjs/helperComponents/index.js +20 -0
  50. package/dist/cjs/helpers/checkNestedNodesSelection.d.ts +9 -0
  51. package/dist/cjs/helpers/checkNestedNodesSelection.js +15 -0
  52. package/dist/cjs/helpers/collectEmptyNestedNodesInExpanded.d.ts +9 -0
  53. package/dist/cjs/helpers/collectEmptyNestedNodesInExpanded.js +21 -0
  54. package/dist/cjs/helpers/collectHierarchicalAncestors.d.ts +3 -0
  55. package/dist/cjs/helpers/collectHierarchicalAncestors.js +18 -0
  56. package/dist/cjs/helpers/collectIds.d.ts +9 -0
  57. package/dist/cjs/helpers/collectIds.js +16 -0
  58. package/dist/cjs/helpers/extractTreeNodeTitle.d.ts +6 -0
  59. package/dist/cjs/helpers/extractTreeNodeTitle.js +9 -0
  60. package/dist/cjs/helpers/findAllChildNodeIds.d.ts +9 -0
  61. package/dist/cjs/helpers/findAllChildNodeIds.js +24 -0
  62. package/dist/cjs/helpers/findAllExpandedChildNodeIds.d.ts +8 -0
  63. package/dist/cjs/helpers/findAllExpandedChildNodeIds.js +21 -0
  64. package/dist/cjs/helpers/getSearchedTreeItems.d.ts +23 -0
  65. package/dist/cjs/helpers/getSearchedTreeItems.js +41 -0
  66. package/dist/cjs/helpers/getSearchedTreeNodeById.d.ts +19 -0
  67. package/dist/cjs/helpers/getSearchedTreeNodeById.js +35 -0
  68. package/dist/cjs/helpers/index.d.ts +13 -0
  69. package/dist/cjs/helpers/index.js +29 -0
  70. package/dist/cjs/helpers/lookupTreeForSelectedNodes.d.ts +15 -0
  71. package/dist/cjs/helpers/lookupTreeForSelectedNodes.js +21 -0
  72. package/dist/cjs/helpers/sortTreeItemsByTitle.d.ts +13 -0
  73. package/dist/cjs/helpers/sortTreeItemsByTitle.js +14 -0
  74. package/dist/cjs/helpers/traverse.d.ts +11 -0
  75. package/dist/cjs/helpers/traverse.js +25 -0
  76. package/dist/cjs/helpers/traverseWithTarget.d.ts +14 -0
  77. package/dist/cjs/helpers/traverseWithTarget.js +30 -0
  78. package/dist/cjs/helpers/updateTreeNode.d.ts +13 -0
  79. package/dist/cjs/helpers/updateTreeNode.js +34 -0
  80. package/dist/cjs/hooks/index.d.ts +2 -0
  81. package/dist/cjs/hooks/index.js +18 -0
  82. package/dist/cjs/hooks/internalHooks.d.ts +4 -0
  83. package/dist/cjs/hooks/internalHooks.js +29 -0
  84. package/dist/cjs/hooks/useSearchableTree.d.ts +52 -0
  85. package/dist/cjs/hooks/useSearchableTree.js +113 -0
  86. package/dist/cjs/hooks/useTreeMultiSelection.d.ts +33 -0
  87. package/dist/cjs/hooks/useTreeMultiSelection.js +37 -0
  88. package/dist/cjs/index.d.ts +17 -0
  89. package/dist/cjs/index.js +47 -0
  90. package/dist/cjs/package.json +3 -0
  91. package/dist/cjs/style.css +565 -0
  92. package/dist/cjs/types.d.ts +151 -0
  93. package/dist/cjs/types.js +2 -0
  94. package/dist/esm/components/Tree/Tree.d.ts +4 -0
  95. package/dist/esm/components/Tree/Tree.js +25 -0
  96. package/dist/esm/components/Tree/index.d.ts +1 -0
  97. package/dist/esm/components/Tree/index.js +1 -0
  98. package/dist/esm/components/Tree/styles.module.css +17 -0
  99. package/dist/esm/components/Tree/utils.d.ts +4 -0
  100. package/dist/esm/components/Tree/utils.js +12 -0
  101. package/dist/esm/components/index.d.ts +1 -0
  102. package/dist/esm/components/index.js +1 -0
  103. package/dist/esm/constants.d.ts +26 -0
  104. package/dist/esm/constants.js +26 -0
  105. package/dist/esm/contexts/TreeContext.d.ts +30 -0
  106. package/dist/esm/contexts/TreeContext.js +103 -0
  107. package/dist/esm/helperComponents/ExpandableTreeNode/ExpandableTreeNode.d.ts +11 -0
  108. package/dist/esm/helperComponents/ExpandableTreeNode/ExpandableTreeNode.js +81 -0
  109. package/dist/esm/helperComponents/ExpandableTreeNode/index.d.ts +1 -0
  110. package/dist/esm/helperComponents/ExpandableTreeNode/index.js +1 -0
  111. package/dist/esm/helperComponents/ExpandableTreeNode/styles.module.css +37 -0
  112. package/dist/esm/helperComponents/TreeItem/TreeItem.d.ts +8 -0
  113. package/dist/esm/helperComponents/TreeItem/TreeItem.js +13 -0
  114. package/dist/esm/helperComponents/TreeItem/index.d.ts +1 -0
  115. package/dist/esm/helperComponents/TreeItem/index.js +1 -0
  116. package/dist/esm/helperComponents/TreeLine/TreeLine.d.ts +10 -0
  117. package/dist/esm/helperComponents/TreeLine/TreeLine.js +7 -0
  118. package/dist/esm/helperComponents/TreeLine/index.d.ts +1 -0
  119. package/dist/esm/helperComponents/TreeLine/index.js +1 -0
  120. package/dist/esm/helperComponents/TreeLine/styles.module.css +37 -0
  121. package/dist/esm/helperComponents/TreeNode/TreeNode.d.ts +4 -0
  122. package/dist/esm/helperComponents/TreeNode/TreeNode.js +181 -0
  123. package/dist/esm/helperComponents/TreeNode/components/TreeNodeActions.d.ts +15 -0
  124. package/dist/esm/helperComponents/TreeNode/components/TreeNodeActions.js +70 -0
  125. package/dist/esm/helperComponents/TreeNode/components/TreeNodeHref.d.ts +7 -0
  126. package/dist/esm/helperComponents/TreeNode/components/TreeNodeHref.js +6 -0
  127. package/dist/esm/helperComponents/TreeNode/components/index.d.ts +2 -0
  128. package/dist/esm/helperComponents/TreeNode/components/index.js +2 -0
  129. package/dist/esm/helperComponents/TreeNode/constants.d.ts +5 -0
  130. package/dist/esm/helperComponents/TreeNode/constants.js +7 -0
  131. package/dist/esm/helperComponents/TreeNode/index.d.ts +1 -0
  132. package/dist/esm/helperComponents/TreeNode/index.js +1 -0
  133. package/dist/esm/helperComponents/TreeNode/styles.module.css +474 -0
  134. package/dist/esm/helperComponents/TreeNode/types.d.ts +9 -0
  135. package/dist/esm/helperComponents/TreeNode/types.js +1 -0
  136. package/dist/esm/helperComponents/TreeNode/utils.d.ts +3 -0
  137. package/dist/esm/helperComponents/TreeNode/utils.js +6 -0
  138. package/dist/esm/helperComponents/index.d.ts +4 -0
  139. package/dist/esm/helperComponents/index.js +4 -0
  140. package/dist/esm/helpers/checkNestedNodesSelection.d.ts +9 -0
  141. package/dist/esm/helpers/checkNestedNodesSelection.js +12 -0
  142. package/dist/esm/helpers/collectEmptyNestedNodesInExpanded.d.ts +9 -0
  143. package/dist/esm/helpers/collectEmptyNestedNodesInExpanded.js +18 -0
  144. package/dist/esm/helpers/collectHierarchicalAncestors.d.ts +3 -0
  145. package/dist/esm/helpers/collectHierarchicalAncestors.js +15 -0
  146. package/dist/esm/helpers/collectIds.d.ts +9 -0
  147. package/dist/esm/helpers/collectIds.js +13 -0
  148. package/dist/esm/helpers/extractTreeNodeTitle.d.ts +6 -0
  149. package/dist/esm/helpers/extractTreeNodeTitle.js +5 -0
  150. package/dist/esm/helpers/findAllChildNodeIds.d.ts +9 -0
  151. package/dist/esm/helpers/findAllChildNodeIds.js +21 -0
  152. package/dist/esm/helpers/findAllExpandedChildNodeIds.d.ts +8 -0
  153. package/dist/esm/helpers/findAllExpandedChildNodeIds.js +18 -0
  154. package/dist/esm/helpers/getSearchedTreeItems.d.ts +23 -0
  155. package/dist/esm/helpers/getSearchedTreeItems.js +37 -0
  156. package/dist/esm/helpers/getSearchedTreeNodeById.d.ts +19 -0
  157. package/dist/esm/helpers/getSearchedTreeNodeById.js +31 -0
  158. package/dist/esm/helpers/index.d.ts +13 -0
  159. package/dist/esm/helpers/index.js +13 -0
  160. package/dist/esm/helpers/lookupTreeForSelectedNodes.d.ts +15 -0
  161. package/dist/esm/helpers/lookupTreeForSelectedNodes.js +18 -0
  162. package/dist/esm/helpers/sortTreeItemsByTitle.d.ts +13 -0
  163. package/dist/esm/helpers/sortTreeItemsByTitle.js +10 -0
  164. package/dist/esm/helpers/traverse.d.ts +11 -0
  165. package/dist/esm/helpers/traverse.js +21 -0
  166. package/dist/esm/helpers/traverseWithTarget.d.ts +14 -0
  167. package/dist/esm/helpers/traverseWithTarget.js +26 -0
  168. package/dist/esm/helpers/updateTreeNode.d.ts +13 -0
  169. package/dist/esm/helpers/updateTreeNode.js +30 -0
  170. package/dist/esm/hooks/index.d.ts +2 -0
  171. package/dist/esm/hooks/index.js +2 -0
  172. package/dist/esm/hooks/internalHooks.d.ts +4 -0
  173. package/dist/esm/hooks/internalHooks.js +24 -0
  174. package/dist/esm/hooks/useSearchableTree.d.ts +52 -0
  175. package/dist/esm/hooks/useSearchableTree.js +110 -0
  176. package/dist/esm/hooks/useTreeMultiSelection.d.ts +33 -0
  177. package/dist/esm/hooks/useTreeMultiSelection.js +34 -0
  178. package/dist/esm/index.d.ts +17 -0
  179. package/dist/esm/index.js +17 -0
  180. package/dist/esm/style.css +565 -0
  181. package/dist/esm/types.d.ts +151 -0
  182. package/dist/esm/types.js +1 -0
  183. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  184. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  185. package/package.json +51 -0
  186. package/src/components/Tree/Tree.tsx +53 -0
  187. package/src/components/Tree/index.ts +1 -0
  188. package/src/components/Tree/styles.module.scss +22 -0
  189. package/src/components/Tree/utils.ts +16 -0
  190. package/src/components/index.ts +1 -0
  191. package/src/constants.ts +29 -0
  192. package/src/contexts/TreeContext.tsx +189 -0
  193. package/src/helperComponents/ExpandableTreeNode/ExpandableTreeNode.tsx +156 -0
  194. package/src/helperComponents/ExpandableTreeNode/index.ts +1 -0
  195. package/src/helperComponents/ExpandableTreeNode/styles.module.scss +55 -0
  196. package/src/helperComponents/TreeItem/TreeItem.tsx +26 -0
  197. package/src/helperComponents/TreeItem/index.ts +1 -0
  198. package/src/helperComponents/TreeLine/TreeLine.tsx +27 -0
  199. package/src/helperComponents/TreeLine/index.ts +1 -0
  200. package/src/helperComponents/TreeLine/styles.module.scss +58 -0
  201. package/src/helperComponents/TreeNode/TreeNode.tsx +389 -0
  202. package/src/helperComponents/TreeNode/components/TreeNodeActions.tsx +129 -0
  203. package/src/helperComponents/TreeNode/components/TreeNodeHref.tsx +29 -0
  204. package/src/helperComponents/TreeNode/components/index.ts +2 -0
  205. package/src/helperComponents/TreeNode/constants.ts +7 -0
  206. package/src/helperComponents/TreeNode/index.ts +1 -0
  207. package/src/helperComponents/TreeNode/styles.module.scss +300 -0
  208. package/src/helperComponents/TreeNode/types.ts +11 -0
  209. package/src/helperComponents/TreeNode/utils.ts +9 -0
  210. package/src/helperComponents/index.ts +4 -0
  211. package/src/helpers/checkNestedNodesSelection.ts +15 -0
  212. package/src/helpers/collectEmptyNestedNodesInExpanded.ts +23 -0
  213. package/src/helpers/collectHierarchicalAncestors.ts +22 -0
  214. package/src/helpers/collectIds.ts +17 -0
  215. package/src/helpers/extractTreeNodeTitle.ts +8 -0
  216. package/src/helpers/findAllChildNodeIds.ts +27 -0
  217. package/src/helpers/findAllExpandedChildNodeIds.ts +24 -0
  218. package/src/helpers/getSearchedTreeItems.ts +63 -0
  219. package/src/helpers/getSearchedTreeNodeById.ts +54 -0
  220. package/src/helpers/index.ts +13 -0
  221. package/src/helpers/lookupTreeForSelectedNodes.ts +33 -0
  222. package/src/helpers/sortTreeItemsByTitle.ts +26 -0
  223. package/src/helpers/traverse.ts +28 -0
  224. package/src/helpers/traverseWithTarget.ts +41 -0
  225. package/src/helpers/updateTreeNode.ts +46 -0
  226. package/src/hooks/index.ts +2 -0
  227. package/src/hooks/internalHooks.ts +30 -0
  228. package/src/hooks/useSearchableTree.ts +190 -0
  229. package/src/hooks/useTreeMultiSelection.ts +67 -0
  230. package/src/index.ts +17 -0
  231. package/src/types.ts +162 -0
@@ -0,0 +1,37 @@
1
+ .treeLine{
2
+ pointer-events:none;
3
+ position:relative;
4
+ flex-shrink:0;
5
+ width:var(--tree-line-width, var(--sn-density-size-icon-s, var(--sn-primitive-dimension-24, 24px)));
6
+ visibility:hidden;
7
+ }
8
+ .treeLine[data-visible]{
9
+ visibility:visible;
10
+ }
11
+ .treeLine[data-visible]::after{
12
+ content:"";
13
+ position:absolute;
14
+ top:0;
15
+ left:50%;
16
+ transform:translateX(-50%);
17
+ width:var(--sn-primitive-strokeWeight-strokeRegular, 1px);
18
+ height:100%;
19
+ background:var(--sn-theme-color-available-borderColor, var(--sn-brand-color-state-neutralBorder-forLightMode-default-borderColor, rgba(159, 166, 198, 0.3019607843)));
20
+ }
21
+ .treeLine[data-horizontal]{
22
+ box-sizing:border-box;
23
+ }
24
+ .treeLine[data-horizontal]::after{
25
+ top:50%;
26
+ left:calc(-50% - var(--tree-item-gap, 0px));
27
+ transform:translateY(-50%);
28
+ width:calc(150% + var(--tree-item-gap, 0px));
29
+ height:var(--sn-primitive-strokeWeight-strokeRegular, 1px);
30
+ }
31
+ .treeLine[data-horizontal][data-half-width]{
32
+ width:0;
33
+ }
34
+ .treeLine[data-horizontal][data-half-width]::after{
35
+ left:calc(0px - var(--tree-line-width, var(--sn-density-size-icon-s, var(--sn-primitive-dimension-24, 24px))) / 2 - var(--tree-item-gap, 0px));
36
+ width:calc(var(--tree-line-width, var(--sn-density-size-icon-s, var(--sn-primitive-dimension-24, 24px))) / 2 + var(--tree-item-gap, 0px));
37
+ }
@@ -0,0 +1,4 @@
1
+ import { TreeNodeProps } from '../../types.js';
2
+ import { TreeNodeComponentProps } from './types.js';
3
+ export type { TreeNodeProps };
4
+ export declare const TreeNode: import("react").ForwardRefExoticComponent<TreeNodeComponentProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,181 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { ChevronRightSVG, FileSVG, FolderOpenSVG, FolderSVG } from '@cloud-ru/ds-icons/interface/system';
3
+ import { Spinner } from '@cloud-ru/ds-loader';
4
+ import { Checkbox, Radio } from '@cloud-ru/ds-toggles';
5
+ import { TruncateString } from '@cloud-ru/ds-truncate-string';
6
+ import { Typography } from '@cloud-ru/ds-typography';
7
+ import { extractSupportProps } from '@cloud-ru/ds-utils';
8
+ import cn from 'classnames';
9
+ import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
10
+ import { TEST_IDS } from '../../constants.js';
11
+ import { useTreeContext } from '../../contexts/TreeContext.js';
12
+ import { checkNestedNodesSelection, findAllChildNodeIds } from '../../helpers/index.js';
13
+ import { TreeLine } from '../TreeLine/index.js';
14
+ import { TreeNodeActions, TreeNodeHref } from './components/index.js';
15
+ import { CONTROL_SIZE_BY_TREE_SIZE } from './constants.js';
16
+ import styles from './styles.module.css';
17
+ import { stopPropagationClick } from './utils.js';
18
+ export const TreeNode = forwardRef(({ id, title, icon: iconProp, expandedIcon: expandedIconProp, collapsedIcon: collapsedIconProp, disabled, onClick, nested, className, onChevronClick, onKeyDown, loading, parentNode, tabIndexAvailable, href, ...rest }, ref) => {
19
+ const { isMultiSelect, isSelectable, onNodeClick, selected, expandedNodes, onSelect, nodeActions, parentActions, setFocusPosition, resetFocusPosition, focusedNodeId, setFocusIndex, focusableNodeIds, showToggle, showLines, showIcons, size = 'm', titleMaxLines = 1, getSelectionState, } = useTreeContext();
20
+ const [isDroplistOpen, setDroplistOpen] = useState(false);
21
+ const [isDroplistTriggerFocused, setFocusDroplistTrigger] = useState(false);
22
+ const contentRef = useRef(null);
23
+ const anchorRef = useRef(null);
24
+ const isExpandable = Array.isArray(nested);
25
+ const isExpanded = isExpandable ? expandedNodes?.includes(id) : undefined;
26
+ const selectedIds = useMemo(() => {
27
+ if (Array.isArray(selected)) {
28
+ return selected;
29
+ }
30
+ if (selected) {
31
+ return [selected];
32
+ }
33
+ return [];
34
+ }, [selected]);
35
+ const descendantIds = useMemo(() => (nested?.length ? findAllChildNodeIds(nested) : []), [nested]);
36
+ const nestedNodesSelection = useMemo(() => {
37
+ if (!nested || !selected)
38
+ return undefined;
39
+ return checkNestedNodesSelection(nested, selectedIds);
40
+ }, [nested, selected, selectedIds]);
41
+ const multiSelectState = useMemo(() => {
42
+ if (!isMultiSelect) {
43
+ return { checked: false, indeterminate: false };
44
+ }
45
+ return getSelectionState({
46
+ nodeId: id,
47
+ childIds: descendantIds,
48
+ selectedIds,
49
+ });
50
+ }, [descendantIds, getSelectionState, id, isMultiSelect, selectedIds]);
51
+ const isSelected = isMultiSelect ? multiSelectState.checked : selected === id || false;
52
+ const isIndeterminate = isMultiSelect && multiSelectState.indeterminate;
53
+ const isFocused = focusedNodeId === id;
54
+ useEffect(() => {
55
+ if (contentRef.current && isFocused) {
56
+ contentRef.current.focus();
57
+ }
58
+ }, [isFocused]);
59
+ const treeNodeIcon = useMemo(() => {
60
+ if (!showIcons)
61
+ return undefined;
62
+ // Размер svg задаётся в styles.module.scss через `.treeNodeIcon svg`,
63
+ // зависит от data-size корня; size в JSX не прокидываем.
64
+ if (isExpandable) {
65
+ return isExpanded ? (expandedIconProp ?? _jsx(FolderOpenSVG, {})) : (collapsedIconProp ?? _jsx(FolderSVG, {}));
66
+ }
67
+ return iconProp ?? _jsx(FileSVG, {});
68
+ }, [showIcons, isExpandable, isExpanded, iconProp, expandedIconProp, collapsedIconProp]);
69
+ const handleClick = e => {
70
+ onNodeClick({
71
+ id,
72
+ title,
73
+ disabled,
74
+ nested,
75
+ onClick,
76
+ href,
77
+ }, e);
78
+ };
79
+ const handleAnchorClick = (e) => {
80
+ e.stopPropagation();
81
+ if (e?.metaKey || e?.ctrlKey || e?.button === 1) {
82
+ return;
83
+ }
84
+ // preventDefault только если есть кастомный onClick на ноде — иначе
85
+ // позволяем браузеру навигировать по href. Но handleClick зовём всегда,
86
+ // чтобы Tree успел прокинуть onNodeClick/onSelect.
87
+ if (onClick) {
88
+ e.preventDefault();
89
+ }
90
+ handleClick(e);
91
+ };
92
+ const handleSelect = () => {
93
+ onSelect({
94
+ id,
95
+ disabled,
96
+ nested,
97
+ }, parentNode);
98
+ };
99
+ const handleFocus = (e) => {
100
+ setFocusPosition(id);
101
+ if (!e) {
102
+ setFocusDroplistTrigger(false);
103
+ contentRef.current?.focus();
104
+ }
105
+ };
106
+ const handleBlurActions = () => {
107
+ if (isDroplistTriggerFocused && !isDroplistOpen) {
108
+ setFocusDroplistTrigger(false);
109
+ contentRef.current?.blur();
110
+ }
111
+ };
112
+ const handleKeyDown = e => {
113
+ onKeyDown?.(e);
114
+ const focusableCount = focusableNodeIds.length;
115
+ switch (e.key) {
116
+ case 'ArrowDown': {
117
+ e.preventDefault();
118
+ setFocusIndex((prev = 0) => {
119
+ if (prev >= focusableCount - 1) {
120
+ return focusableCount - 1;
121
+ }
122
+ return prev + 1;
123
+ });
124
+ return;
125
+ }
126
+ case 'ArrowUp': {
127
+ e.preventDefault();
128
+ setFocusIndex((prev = 0) => {
129
+ if (!prev) {
130
+ return 0;
131
+ }
132
+ return prev - 1;
133
+ });
134
+ return;
135
+ }
136
+ case 'ArrowRight': {
137
+ e.preventDefault();
138
+ // ArrowRight уводит фокус на droplist trigger row-actions — это нужно
139
+ // только если на строке вообще есть actions. Без них клавиша не
140
+ // должна давать визуального hover-эффекта (`data-droplist-active`).
141
+ const hasRowActions = Boolean(nested ? parentActions : nodeActions);
142
+ if (hasRowActions && (isExpanded || !isExpandable || disabled)) {
143
+ setFocusDroplistTrigger(true);
144
+ }
145
+ return;
146
+ }
147
+ case 'ArrowLeft': {
148
+ e.preventDefault();
149
+ if (isDroplistTriggerFocused) {
150
+ setFocusDroplistTrigger(false);
151
+ contentRef.current?.focus();
152
+ }
153
+ return;
154
+ }
155
+ case 'Escape': {
156
+ contentRef.current?.blur();
157
+ return;
158
+ }
159
+ case ' ':
160
+ case 'Enter': {
161
+ e.preventDefault();
162
+ handleSelect();
163
+ if (href && anchorRef.current) {
164
+ anchorRef.current.click();
165
+ }
166
+ return;
167
+ }
168
+ default:
169
+ return;
170
+ }
171
+ };
172
+ const getNodeActions = nested ? parentActions : nodeActions;
173
+ return (_jsxs("div", { role: 'presentation', className: cn(styles.treeNode, className), ...extractSupportProps(rest), "data-node-id": id, "data-size": size, ref: ref, children: [parentNode && _jsx(TreeLine, { halfWidth: true, horizontal: true, visible: showLines, "data-test-id": TEST_IDS.line }), _jsx("div", { className: styles.treeNodeExpandButtonWrapper, children: isExpandable ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: 'button', onClick: onChevronClick, "data-expanded": isExpanded || undefined, className: styles.treeNodeExpandButton, tabIndex: -1, "data-test-id": TEST_IDS.chevron, "aria-label": isExpanded ? 'Collapse' : 'Expand', children: [_jsx("span", { className: styles.treeNodeExpandButtonStateLayer, "aria-hidden": true, "data-state": 'emptyNeutralOnBackground' }), loading ? _jsx(Spinner, { size: CONTROL_SIZE_BY_TREE_SIZE[size] }) : _jsx(ChevronRightSVG, {})] }), _jsx(TreeLine, { visible: isExpanded && showLines })] })) : (_jsx("div", { className: styles.treeNodeExpandButtonSpacer, "aria-hidden": true })) }), _jsxs("div", { role: 'treeitem', "aria-expanded": isExpanded, "aria-selected": isSelectable
174
+ ? isSelected || (nestedNodesSelection?.someSelected && !isExpanded && !isMultiSelect)
175
+ : undefined, "aria-disabled": disabled, "data-multiselect": isMultiSelect || undefined, "data-droplist-active": isDroplistOpen || isDroplistTriggerFocused || undefined, onClick: handleClick, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: resetFocusPosition, tabIndex: tabIndexAvailable ? 0 : -1, className: styles.treeNodeContent, "data-test-id": TEST_IDS.item, ref: contentRef, children: [_jsx("span", { className: styles.stateLayer, "aria-hidden": true, "data-state": isSelected && !isMultiSelect ? 'activatedOnBackground' : 'emptyNeutralOnBackground' }), (isMultiSelect || showToggle) && (_jsxs("div", { className: styles.treeNodeCheckboxWrap, children: [isMultiSelect && (_jsx(Checkbox, { size: CONTROL_SIZE_BY_TREE_SIZE[size], disabled: disabled, checked: isSelected, indeterminate: isIndeterminate, onChange: handleSelect, onClick: stopPropagationClick, "data-test-id": TEST_IDS.checkbox, tabIndex: -1 })), showToggle && (_jsx(Radio, { size: CONTROL_SIZE_BY_TREE_SIZE[size], checked: isSelected, disabled: disabled, "data-test-id": TEST_IDS.radio, tabIndex: -1 }))] })), treeNodeIcon && (_jsx("div", { className: styles.treeNodeIcon, "data-test-id": TEST_IDS.icon, children: treeNodeIcon })), _jsx(Typography, { variant: 'body', size: size, as: 'div', className: styles.treeNodeTitle, children: _jsxs(TreeNodeHref, { href: href, onClick: handleAnchorClick, ref: anchorRef, children: [typeof title === 'string' && (_jsx(TruncateString, { text: title, maxLines: titleMaxLines, "data-test-id": TEST_IDS.title })), typeof title !== 'string' && title({ id, disabled, nested })] }) }), getNodeActions && (_jsx(TreeNodeActions, { getNodeActions: getNodeActions, node: {
176
+ id,
177
+ title,
178
+ disabled,
179
+ nested,
180
+ }, focusNode: handleFocus, onBlurActions: handleBlurActions, isDroplistTriggerFocused: isDroplistTriggerFocused, isDroplistOpen: isDroplistOpen, setDroplistOpen: setDroplistOpen, size: size }))] })] }));
181
+ });
@@ -0,0 +1,15 @@
1
+ import { ItemProps } from '@cloud-ru/ds-list';
2
+ import { Dispatch, SetStateAction } from 'react';
3
+ import { Size, TreeNodeProps } from '../../../types.js';
4
+ type TreeNodeActionsProps = {
5
+ isDroplistOpen: boolean;
6
+ setDroplistOpen: Dispatch<SetStateAction<boolean>>;
7
+ getNodeActions(node: Omit<TreeNodeProps, 'href'>): ItemProps[];
8
+ node: Omit<TreeNodeProps, 'href'>;
9
+ isDroplistTriggerFocused: boolean;
10
+ focusNode(): void;
11
+ onBlurActions(): void;
12
+ size: Size;
13
+ };
14
+ export declare function TreeNodeActions({ getNodeActions, isDroplistTriggerFocused, focusNode, isDroplistOpen, setDroplistOpen, onBlurActions, node, size, }: TreeNodeActionsProps): import("react/jsx-runtime").JSX.Element | null;
15
+ export {};
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Button } from '@cloud-ru/ds-button';
3
+ import { KebabSVG } from '@cloud-ru/ds-icons/interface/system';
4
+ import { Droplist } from '@cloud-ru/ds-list';
5
+ import { useEffect, useRef } from 'react';
6
+ import { TEST_IDS } from '../../../constants.js';
7
+ import styles from '../styles.module.css';
8
+ import { stopPropagationClick, stopPropagationFocus } from '../utils.js';
9
+ export function TreeNodeActions({ getNodeActions, isDroplistTriggerFocused, focusNode, isDroplistOpen, setDroplistOpen, onBlurActions, node, size, }) {
10
+ const droplistActions = getNodeActions(node);
11
+ const localRef = useRef(null);
12
+ useEffect(() => {
13
+ if (localRef.current && isDroplistTriggerFocused) {
14
+ localRef.current.focus();
15
+ }
16
+ }, [isDroplistTriggerFocused, localRef]);
17
+ const handleKeyDown = e => {
18
+ switch (e.key) {
19
+ case 'Tab': {
20
+ focusNode();
21
+ setDroplistOpen(false);
22
+ e.preventDefault();
23
+ e.stopPropagation();
24
+ return;
25
+ }
26
+ case 'ArrowLeft': {
27
+ if (isDroplistTriggerFocused) {
28
+ focusNode();
29
+ setDroplistOpen(false);
30
+ e.stopPropagation();
31
+ }
32
+ return;
33
+ }
34
+ case ' ':
35
+ case 'Enter': {
36
+ e.stopPropagation();
37
+ return;
38
+ }
39
+ case 'ArrowDown': {
40
+ if (isDroplistTriggerFocused) {
41
+ setDroplistOpen(true);
42
+ }
43
+ e.stopPropagation();
44
+ return;
45
+ }
46
+ case 'ArrowUp': {
47
+ setDroplistOpen(false);
48
+ localRef.current?.focus();
49
+ e.stopPropagation();
50
+ return;
51
+ }
52
+ default:
53
+ return;
54
+ }
55
+ };
56
+ if (!droplistActions.length) {
57
+ return null;
58
+ }
59
+ return (_jsx("div", { role: 'presentation', className: styles.treeNodeActions, "data-focused": isDroplistTriggerFocused || undefined, onClick: stopPropagationClick, onKeyDown: handleKeyDown, onFocus: stopPropagationFocus, children: _jsx(Droplist, { open: isDroplistOpen, onOpenChange: open => {
60
+ setDroplistOpen(open);
61
+ // Закрытие дроплиста (по клику в пункт, ESC, outside-click) должно
62
+ // вернуть фокус на строку и снять `isDroplistTriggerFocused` —
63
+ // иначе kebab остаётся в visible-hover-состоянии. Tab и ArrowLeft
64
+ // уже делают это сами; для пути через `closeDroplistOnItemClick`
65
+ // ресет навешиваем здесь.
66
+ if (!open) {
67
+ focusNode();
68
+ }
69
+ }, items: droplistActions, closeDroplistOnItemClick: true, placement: 'bottom-end', size: size, children: _jsx(Button, { view: 'elevated', appearance: 'neutral', size: size, icon: _jsx(KebabSVG, {}), onBlur: onBlurActions, tabIndex: -1, "data-test-id": TEST_IDS.droplistTrigger, innerRef: localRef }) }) }));
70
+ }
@@ -0,0 +1,7 @@
1
+ import { MouseEventHandler, ReactNode } from 'react';
2
+ export type TreeNodeHrefProps = {
3
+ href?: string;
4
+ children: ReactNode;
5
+ onClick?: MouseEventHandler<HTMLAnchorElement>;
6
+ };
7
+ export declare const TreeNodeHref: import("react").ForwardRefExoticComponent<TreeNodeHrefProps & import("react").RefAttributes<HTMLAnchorElement>>;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { TEST_IDS } from '../../../constants.js';
4
+ import styles from '../styles.module.css';
5
+ export const TreeNodeHref = forwardRef(({ href, children, onClick }, ref) => href ? (_jsx("a", { href: href, className: styles.treeNodeLink, onClick: onClick, "data-test-id": TEST_IDS.link, tabIndex: -2, ref: ref, children: children })) : (children));
6
+ TreeNodeHref.displayName = 'TreeNodeHref';
@@ -0,0 +1,2 @@
1
+ export * from './TreeNodeActions.js';
2
+ export * from './TreeNodeHref.js';
@@ -0,0 +1,2 @@
1
+ export * from './TreeNodeActions.js';
2
+ export * from './TreeNodeHref.js';
@@ -0,0 +1,5 @@
1
+ export declare const CONTROL_SIZE_BY_TREE_SIZE: {
2
+ readonly s: "xs";
3
+ readonly m: "s";
4
+ readonly l: "s";
5
+ };
@@ -0,0 +1,7 @@
1
+ // Size-ось контролов (Checkbox / Radio / Spinner) короче, чем у Tree:
2
+ // 's'-строка → 'xs'-контрол, 'm'/'l' → 's'.
3
+ export const CONTROL_SIZE_BY_TREE_SIZE = {
4
+ s: 'xs',
5
+ m: 's',
6
+ l: 's',
7
+ };
@@ -0,0 +1 @@
1
+ export * from './TreeNode.js';
@@ -0,0 +1 @@
1
+ export * from './TreeNode.js';