@snack-uikit/tree 0.8.20-preview-913f1101.0 → 0.8.20

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 (31) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/cjs/helperComponents/ExpandableTreeNode/ExpandableTreeNode.d.ts +1 -1
  3. package/dist/cjs/helperComponents/ExpandableTreeNode/ExpandableTreeNode.js +28 -11
  4. package/dist/cjs/helperComponents/ExpandableTreeNode/styles.module.css +4 -0
  5. package/dist/cjs/helperComponents/TreeItem/TreeItem.d.ts +1 -1
  6. package/dist/cjs/helperComponents/TreeItem/TreeItem.js +8 -5
  7. package/dist/cjs/helperComponents/TreeLine/TreeLine.d.ts +4 -3
  8. package/dist/cjs/helperComponents/TreeLine/TreeLine.js +10 -4
  9. package/dist/cjs/helperComponents/TreeLine/styles.module.css +3 -3
  10. package/dist/cjs/helperComponents/TreeNode/TreeNode.d.ts +1 -1
  11. package/dist/cjs/helperComponents/TreeNode/TreeNode.js +27 -20
  12. package/dist/cjs/helperComponents/TreeNode/styles.module.css +20 -8
  13. package/dist/esm/helperComponents/ExpandableTreeNode/ExpandableTreeNode.d.ts +1 -1
  14. package/dist/esm/helperComponents/ExpandableTreeNode/ExpandableTreeNode.js +22 -11
  15. package/dist/esm/helperComponents/ExpandableTreeNode/styles.module.css +4 -0
  16. package/dist/esm/helperComponents/TreeItem/TreeItem.d.ts +1 -1
  17. package/dist/esm/helperComponents/TreeItem/TreeItem.js +5 -4
  18. package/dist/esm/helperComponents/TreeLine/TreeLine.d.ts +4 -3
  19. package/dist/esm/helperComponents/TreeLine/TreeLine.js +3 -2
  20. package/dist/esm/helperComponents/TreeLine/styles.module.css +3 -3
  21. package/dist/esm/helperComponents/TreeNode/TreeNode.d.ts +1 -1
  22. package/dist/esm/helperComponents/TreeNode/TreeNode.js +13 -11
  23. package/dist/esm/helperComponents/TreeNode/styles.module.css +20 -8
  24. package/package.json +6 -6
  25. package/src/helperComponents/ExpandableTreeNode/ExpandableTreeNode.tsx +120 -97
  26. package/src/helperComponents/ExpandableTreeNode/styles.module.scss +6 -0
  27. package/src/helperComponents/TreeItem/TreeItem.tsx +8 -4
  28. package/src/helperComponents/TreeLine/TreeLine.tsx +8 -5
  29. package/src/helperComponents/TreeLine/styles.module.scss +9 -13
  30. package/src/helperComponents/TreeNode/TreeNode.tsx +266 -245
  31. package/src/helperComponents/TreeNode/styles.module.scss +21 -6
@@ -11,7 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import cn from 'classnames';
14
- import { useEffect, useMemo, useRef, useState } from 'react';
14
+ import { forwardRef, useEffect, useMemo, useRef, useState, } from 'react';
15
15
  import { ButtonFunction } from '@snack-uikit/button';
16
16
  import { ChevronRightSVG, FileSVG, FolderOpenSVG, FolderSVG } from '@snack-uikit/icons';
17
17
  import { Checkbox, Radio } from '@snack-uikit/toggles';
@@ -25,12 +25,12 @@ import { TreeLine } from '../TreeLine';
25
25
  import { TreeNodeActions } from './components';
26
26
  import styles from './styles.module.css';
27
27
  import { stopPropagationClick } from './utils';
28
- export function TreeNode(_a) {
28
+ export const TreeNode = forwardRef((_a, ref) => {
29
29
  var { id, title, icon = _jsx(FileSVG, { size: 24 }), expandedIcon = _jsx(FolderOpenSVG, { size: 24 }), collapsedIcon = _jsx(FolderSVG, { size: 24 }), disabled, onClick, nested, className, onChevronClick, onKeyDown, isLoading, parentNode, tabIndexAvailable } = _a, rest = __rest(_a, ["id", "title", "icon", "expandedIcon", "collapsedIcon", "disabled", "onClick", "nested", "className", "onChevronClick", "onKeyDown", "isLoading", "parentNode", "tabIndexAvailable"]);
30
30
  const { isMultiSelect, isSelectable, onNodeClick, selected, expandedNodes, onSelect, nodeActions, parentActions, setFocusPosition, resetFocusPosition, focusedNodeId, setFocusIndex, focusableNodeIds, showToggle, showLines, showIcons, } = useTreeContext();
31
31
  const [isDroplistOpen, setDroplistOpen] = useState(false);
32
32
  const [isDroplistTriggerFocused, setFocusDroplistTrigger] = useState(false);
33
- const ref = useRef(null);
33
+ const contentRef = useRef(null);
34
34
  const isExpandable = Array.isArray(nested);
35
35
  const isExpanded = isExpandable ? expandedNodes === null || expandedNodes === void 0 ? void 0 : expandedNodes.includes(id) : undefined;
36
36
  const nestedNodesSelection = useMemo(() => {
@@ -41,8 +41,8 @@ export function TreeNode(_a) {
41
41
  const isSelected = (Array.isArray(selected) ? selected.includes(id) || (nestedNodesSelection === null || nestedNodesSelection === void 0 ? void 0 : nestedNodesSelection.allSelected) : selected === id) || false;
42
42
  const isFocused = focusedNodeId === id;
43
43
  useEffect(() => {
44
- if (ref.current && isFocused) {
45
- ref.current.focus();
44
+ if (contentRef.current && isFocused) {
45
+ contentRef.current.focus();
46
46
  }
47
47
  }, [isFocused]);
48
48
  const treeNodeIcon = useMemo(() => {
@@ -74,14 +74,14 @@ export function TreeNode(_a) {
74
74
  setFocusPosition(id);
75
75
  if (!e) {
76
76
  setFocusDroplistTrigger(false);
77
- (_a = ref.current) === null || _a === void 0 ? void 0 : _a.focus();
77
+ (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.focus();
78
78
  }
79
79
  };
80
80
  const handleBlurActions = () => {
81
81
  var _a;
82
82
  if (isDroplistTriggerFocused && !isDroplistOpen) {
83
83
  setFocusDroplistTrigger(false);
84
- (_a = ref.current) === null || _a === void 0 ? void 0 : _a.blur();
84
+ (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.blur();
85
85
  }
86
86
  };
87
87
  const handleKeyDown = e => {
@@ -120,12 +120,12 @@ export function TreeNode(_a) {
120
120
  e.preventDefault();
121
121
  if (isDroplistTriggerFocused) {
122
122
  setFocusDroplistTrigger(false);
123
- (_a = ref.current) === null || _a === void 0 ? void 0 : _a.focus();
123
+ (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.focus();
124
124
  }
125
125
  return;
126
126
  }
127
127
  case 'Escape': {
128
- (_b = ref.current) === null || _b === void 0 ? void 0 : _b.blur();
128
+ (_b = contentRef.current) === null || _b === void 0 ? void 0 : _b.blur();
129
129
  return;
130
130
  }
131
131
  case ' ':
@@ -139,10 +139,12 @@ export function TreeNode(_a) {
139
139
  }
140
140
  };
141
141
  const getNodeActions = nested ? parentActions : nodeActions;
142
- return (_jsxs("div", Object.assign({ role: 'presentation', className: cn(styles.treeNode, className) }, extractSupportProps(rest), { "data-node-id": id, children: [parentNode && (_jsx(TreeLine, { halfWidth: Boolean(nested), horizontal: true, visible: showLines, "data-test-id": TEST_IDS.line })), !parentNode && !nested && _jsx(TreeLine, { visible: false }), isExpandable && (_jsx(ButtonFunction, { size: 'xs', icon: _jsx(ChevronRightSVG, {}), loading: isLoading, onClick: onChevronClick, "data-expanded": isExpanded || undefined, className: styles.treeNodeExpandButton, tabIndex: -1, "data-test-id": TEST_IDS.chevron })), _jsxs("div", { role: 'treeitem', "aria-expanded": isExpanded, "aria-selected": isSelectable ? isSelected || ((nestedNodesSelection === null || nestedNodesSelection === void 0 ? void 0 : nestedNodesSelection.someSelected) && !isExpanded && !isMultiSelect) : 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: ref, children: [(isMultiSelect || showToggle) && (_jsxs("div", { className: styles.treeNodeCheckboxWrap, children: [isMultiSelect && (_jsx(Checkbox, { size: 's', disabled: disabled, checked: isSelected, indeterminate: !isSelected && (nestedNodesSelection === null || nestedNodesSelection === void 0 ? void 0 : nestedNodesSelection.someSelected), onChange: handleSelect, onClick: stopPropagationClick, "data-test-id": TEST_IDS.checkbox, tabIndex: -1 })), showToggle && (_jsx(Radio, { size: 's', 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 })), _jsxs(Typography.SansBodyM, { tag: 'div', className: styles.treeNodeTitle, children: [typeof title === 'string' && _jsx(TruncateString, { text: title, "data-test-id": TEST_IDS.title }), typeof title !== 'string' && title({ id, disabled, nested })] }), getNodeActions && (_jsx(TreeNodeActions, { getNodeActions: getNodeActions, node: {
142
+ return (_jsxs("div", Object.assign({ role: 'presentation', className: cn(styles.treeNode, className) }, extractSupportProps(rest), { "data-node-id": id, ref: ref, children: [parentNode && (_jsx(TreeLine, { halfWidth: Boolean(nested), horizontal: true, visible: showLines, "data-test-id": TEST_IDS.line })), !parentNode && !nested && _jsx(TreeLine, { visible: false }), isExpandable && (_jsxs("div", { className: styles.treeNodeExpandButtonWrapper, children: [_jsx(ButtonFunction, { size: 'xs', icon: _jsx(ChevronRightSVG, {}), loading: isLoading, onClick: onChevronClick, "data-expanded": isExpanded || undefined, className: styles.treeNodeExpandButton, tabIndex: -1, "data-test-id": TEST_IDS.chevron }), _jsx(TreeLine, { visible: isExpanded && showLines, height: '100%' })] })), _jsxs("div", { role: 'treeitem', "aria-expanded": isExpanded, "aria-selected": isSelectable
143
+ ? isSelected || ((nestedNodesSelection === null || nestedNodesSelection === void 0 ? void 0 : nestedNodesSelection.someSelected) && !isExpanded && !isMultiSelect)
144
+ : 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: [(isMultiSelect || showToggle) && (_jsxs("div", { className: styles.treeNodeCheckboxWrap, children: [isMultiSelect && (_jsx(Checkbox, { size: 's', disabled: disabled, checked: isSelected, indeterminate: !isSelected && (nestedNodesSelection === null || nestedNodesSelection === void 0 ? void 0 : nestedNodesSelection.someSelected), onChange: handleSelect, onClick: stopPropagationClick, "data-test-id": TEST_IDS.checkbox, tabIndex: -1 })), showToggle && (_jsx(Radio, { size: 's', 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 })), _jsxs(Typography.SansBodyM, { tag: 'div', className: styles.treeNodeTitle, children: [typeof title === 'string' && _jsx(TruncateString, { text: title, "data-test-id": TEST_IDS.title }), typeof title !== 'string' && title({ id, disabled, nested })] }), getNodeActions && (_jsx(TreeNodeActions, { getNodeActions: getNodeActions, node: {
143
145
  id,
144
146
  title,
145
147
  disabled,
146
148
  nested,
147
149
  }, focusNode: handleFocus, onBlurActions: handleBlurActions, isDroplistTriggerFocused: isDroplistTriggerFocused, isDroplistOpen: isDroplistOpen, setDroplistOpen: setDroplistOpen }))] })] })));
148
- }
150
+ });
@@ -1,6 +1,6 @@
1
1
  .treeNode{
2
2
  display:flex;
3
- align-items:center;
3
+ align-items:flex-start;
4
4
  box-sizing:border-box;
5
5
  width:100%;
6
6
  }
@@ -19,9 +19,12 @@
19
19
  }
20
20
 
21
21
  .treeNodeTitle{
22
+ min-height:var(--size-tree-item-wrap, 24px);
23
+ display:flex;
22
24
  flex-grow:1;
25
+ align-items:center;
23
26
  box-sizing:border-box;
24
- min-width:var(--size-tree-item-checkbox-wrap, 24px);
27
+ min-width:var(--size-tree-item-wrap, 24px);
25
28
  color:var(--sys-neutral-text-main, #41424e);
26
29
  }
27
30
 
@@ -34,19 +37,19 @@
34
37
  }
35
38
 
36
39
  .treeNodeContent{
37
- height:var(--size-tree-item-height, 32px);
38
40
  gap:var(--space-tree-item-content-layout-gap, 4px);
41
+ padding-top:var(--space-tree-item-content-layout-padding, 4px);
39
42
  padding-right:var(--space-tree-item-content-layout-padding, 4px);
43
+ padding-bottom:var(--space-tree-item-content-layout-padding, 4px);
40
44
  padding-left:var(--space-tree-item-content-layout-padding, 4px);
41
45
  border-radius:var(--radius-tree-item-content-layout, 8px);
46
+ min-height:var(--size-tree-item-height, 32px);
42
47
  cursor:pointer;
43
48
  position:relative;
44
49
  display:flex;
45
- align-items:center;
50
+ align-items:flex-start;
46
51
  box-sizing:border-box;
47
52
  width:100%;
48
- height:auto;
49
- min-height:var(--size-tree-item-height, 32px);
50
53
  }
51
54
  .treeNodeContent::before{
52
55
  pointer-events:none;
@@ -95,8 +98,8 @@
95
98
  }
96
99
 
97
100
  .treeNodeCheckboxWrap{
98
- width:var(--size-tree-item-checkbox-wrap, 24px);
99
- height:var(--size-tree-item-checkbox-wrap, 24px);
101
+ width:var(--size-tree-item-wrap, 24px);
102
+ height:var(--size-tree-item-wrap, 24px);
100
103
  display:flex;
101
104
  flex-shrink:0;
102
105
  align-items:center;
@@ -105,6 +108,7 @@
105
108
  }
106
109
 
107
110
  .treeNodeExpandButton{
111
+ min-height:var(--size-tree-item-height, 32px);
108
112
  box-sizing:border-box;
109
113
  }
110
114
  .treeNodeExpandButton svg{
@@ -112,4 +116,12 @@
112
116
  }
113
117
  .treeNodeExpandButton[data-expanded] svg{
114
118
  transform:rotate(90deg);
119
+ }
120
+
121
+ .treeNodeExpandButtonWrapper{
122
+ overflow:hidden;
123
+ display:flex;
124
+ flex-direction:column;
125
+ align-self:stretch;
126
+ min-width:var(--size-button-xs, 24px);
115
127
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "Tree",
7
- "version": "0.8.20-preview-913f1101.0",
7
+ "version": "0.8.20",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -37,15 +37,15 @@
37
37
  "scripts": {},
38
38
  "dependencies": {
39
39
  "@snack-uikit/button": "0.19.6",
40
- "@snack-uikit/icons": "0.24.3-preview-913f1101.0",
41
- "@snack-uikit/list": "0.23.2-preview-913f1101.0",
42
- "@snack-uikit/toggles": "0.13.6-preview-913f1101.0",
43
- "@snack-uikit/truncate-string": "0.6.7-preview-913f1101.0",
40
+ "@snack-uikit/icons": "0.24.2",
41
+ "@snack-uikit/list": "0.23.1",
42
+ "@snack-uikit/toggles": "0.13.5",
43
+ "@snack-uikit/truncate-string": "0.6.6",
44
44
  "@snack-uikit/typography": "0.8.4",
45
45
  "@snack-uikit/utils": "3.7.0",
46
46
  "classnames": "2.5.1",
47
47
  "react-transition-state": "2.1.1",
48
48
  "uncontrollable": "8.0.4"
49
49
  },
50
- "gitHead": "81bb5a94ada1d8222e6d7bd5b89ef3a7f71fb229"
50
+ "gitHead": "bad2d5ed6e54954b528c1ea9dae0b8f86e4c8147"
51
51
  }
@@ -1,4 +1,4 @@
1
- import { KeyboardEventHandler, useEffect, useMemo, useState } from 'react';
1
+ import { forwardRef, KeyboardEventHandler, useEffect, useRef, useState } from 'react';
2
2
  import useTransition from 'react-transition-state';
3
3
 
4
4
  import { TEST_IDS, TRANSITION_TIMING } from '../../constants';
@@ -15,116 +15,139 @@ type ExpandableTreeNodeProps = {
15
15
  tabIndexAvailable?: boolean;
16
16
  };
17
17
 
18
- export function ExpandableTreeNode({
19
- node: { 'data-test-id': dataTestId, ...node },
20
- parentNode,
21
- tabIndexAvailable,
22
- }: ExpandableTreeNodeProps) {
23
- const { expandedNodes, onExpand, onDataLoad, showLines } = useTreeContext();
18
+ export const ExpandableTreeNode = forwardRef<HTMLDivElement, ExpandableTreeNodeProps>(
19
+ ({ node: { 'data-test-id': dataTestId, ...node }, parentNode, tabIndexAvailable }, ref) => {
20
+ const { expandedNodes, onExpand, onDataLoad, showLines } = useTreeContext();
24
21
 
25
- const isExpandable = Boolean(node.nested);
26
- const isExpanded = expandedNodes?.includes(node.id) || false;
22
+ const isExpandable = Boolean(node.nested);
23
+ const isExpanded = expandedNodes?.includes(node.id) || false;
27
24
 
28
- const [isLoading, setLoading] = useState(false);
25
+ const [isLoading, setLoading] = useState(false);
29
26
 
30
- const [state, toggle] = useTransition({
31
- timeout: TRANSITION_TIMING.accordionFolding,
32
- initialEntered: isExpanded || undefined,
33
- enter: isExpanded,
34
- });
27
+ const [state, toggle] = useTransition({
28
+ timeout: TRANSITION_TIMING.accordionFolding,
29
+ initialEntered: isExpanded || undefined,
30
+ enter: isExpanded,
31
+ });
35
32
 
36
- const showContent = node.nested && node.nested.length > 0 && state.status !== 'exited';
33
+ const showContent = node.nested && node.nested.length > 0 && state.status !== 'exited';
37
34
 
38
- const isLineHalfHeight = useMemo(() => {
39
- if (!node.nested?.length) return false;
35
+ const childrenRefs = useRef<(HTMLDivElement | null)[]>([]);
36
+ const [lastItemHeight, setLastItemHeight] = useState(0);
40
37
 
41
- return !node.nested.at(-1)?.nested;
42
- }, [node.nested]);
38
+ useEffect(() => {
39
+ const lastItem = childrenRefs.current?.at(-1);
43
40
 
44
- useEffect(() => {
45
- if (node.nested?.length && isExpanded && state.status === 'exited') {
46
- toggle(true);
47
- }
48
- }, [isExpanded, node.nested, state.status]);
41
+ if (lastItem) {
42
+ const observer = new ResizeObserver(entities =>
43
+ entities.forEach(entity => {
44
+ if (entity.target === lastItem) {
45
+ const [{ blockSize: newHeight }] = entity.contentBoxSize;
46
+ setLastItemHeight(newHeight);
47
+ }
48
+ }),
49
+ );
49
50
 
50
- const toggleExpand = async () => {
51
- if (node.nested && !node.nested.length && !isExpanded && onDataLoad) {
52
- setLoading(true);
51
+ observer.observe(lastItem);
53
52
 
54
- await onDataLoad(node).finally(() => {
55
- setLoading(false);
56
- });
57
- }
53
+ setLastItemHeight(lastItem.scrollHeight ?? 0);
58
54
 
59
- onExpand(node);
55
+ return () => observer.disconnect();
56
+ }
57
+ }, [state.status]);
60
58
 
61
- toggle();
62
- };
59
+ useEffect(() => {
60
+ if (node.nested?.length && isExpanded && state.status === 'exited') {
61
+ toggle(true);
62
+ }
63
+ }, [isExpanded, node.nested, state.status]);
63
64
 
64
- const handleKeyDown: KeyboardEventHandler<HTMLElement> = e => {
65
- if (!isExpandable) return;
65
+ const toggleExpand = async () => {
66
+ if (node.nested && !node.nested.length && !isExpanded && onDataLoad) {
67
+ setLoading(true);
66
68
 
67
- switch (e.key) {
68
- case 'ArrowRight': {
69
- if (!isExpanded) {
70
- toggleExpand();
71
- }
72
- return;
69
+ await onDataLoad(node).finally(() => {
70
+ setLoading(false);
71
+ });
73
72
  }
74
- case 'ArrowLeft': {
75
- if (isExpanded) {
76
- toggleExpand();
73
+
74
+ onExpand(node);
75
+
76
+ toggle();
77
+ };
78
+
79
+ const handleKeyDown: KeyboardEventHandler<HTMLElement> = e => {
80
+ if (!isExpandable) return;
81
+
82
+ switch (e.key) {
83
+ case 'ArrowRight': {
84
+ if (!isExpanded) {
85
+ toggleExpand();
86
+ }
87
+ return;
88
+ }
89
+ case 'ArrowLeft': {
90
+ if (isExpanded) {
91
+ toggleExpand();
92
+ }
93
+ return;
77
94
  }
78
- return;
95
+ default:
96
+ return;
79
97
  }
80
- default:
81
- return;
82
- }
83
- };
84
-
85
- return (
86
- <div
87
- className={styles.expandableTreeNode}
88
- data-test-id={dataTestId}
89
- data-expandable={Boolean(node.nested) || undefined}
90
- data-disabled={node.disabled || undefined}
91
- >
92
- <TreeNode
93
- {...node}
94
- isLoading={isLoading}
95
- parentNode={parentNode}
96
- onChevronClick={toggleExpand}
97
- onKeyDown={handleKeyDown}
98
- tabIndexAvailable={tabIndexAvailable}
99
- />
100
-
101
- {node.nested && (
102
- <div
103
- className={styles.expandableWrap}
104
- data-expanded={isExpanded || undefined}
105
- data-test-id={TEST_IDS.expandable}
106
- >
107
- {showContent && (
108
- <div className={styles.expandableContent} data-test-id={TEST_IDS.expandableContent}>
109
- <TreeLine visible={showLines} halfHeight={isLineHalfHeight} data-test-id={TEST_IDS.line} />
110
-
111
- <div className={styles.expandableNested}>
112
- {node.nested.map(nestedNode => {
113
- const parent: ParentNode = { id: node.id, nested: node.nested, parentNode };
114
-
115
- return (
116
- <TreeItem
117
- key={nestedNode.id}
118
- node={{ ...nestedNode, disabled: node.disabled || nestedNode.disabled }}
119
- parentNode={parent}
120
- />
121
- );
122
- })}
98
+ };
99
+
100
+ return (
101
+ <div
102
+ className={styles.expandableTreeNode}
103
+ data-test-id={dataTestId}
104
+ data-expandable={Boolean(node.nested) || undefined}
105
+ data-disabled={node.disabled || undefined}
106
+ ref={ref}
107
+ >
108
+ <TreeNode
109
+ {...node}
110
+ isLoading={isLoading}
111
+ parentNode={parentNode}
112
+ onChevronClick={toggleExpand}
113
+ onKeyDown={handleKeyDown}
114
+ tabIndexAvailable={tabIndexAvailable}
115
+ />
116
+
117
+ {node.nested && (
118
+ <div
119
+ className={styles.expandableWrap}
120
+ data-expanded={isExpanded || undefined}
121
+ data-test-id={TEST_IDS.expandable}
122
+ >
123
+ {showContent && (
124
+ <div className={styles.expandableContent} data-test-id={TEST_IDS.expandableContent}>
125
+ <TreeLine
126
+ visible={showLines}
127
+ data-test-id={TEST_IDS.line}
128
+ height={`calc(100% - ${lastItemHeight}px)`}
129
+ className={styles.treeLine}
130
+ />
131
+
132
+ <div className={styles.expandableNested}>
133
+ {node.nested.map((nestedNode, index) => {
134
+ const parent: ParentNode = { id: node.id, nested: node.nested, parentNode };
135
+
136
+ return (
137
+ <TreeItem
138
+ ref={el => (childrenRefs.current[index] = el)}
139
+ key={nestedNode.id}
140
+ node={{ ...nestedNode, disabled: node.disabled || nestedNode.disabled }}
141
+ parentNode={parent}
142
+ />
143
+ );
144
+ })}
145
+ </div>
123
146
  </div>
124
- </div>
125
- )}
126
- </div>
127
- )}
128
- </div>
129
- );
130
- }
147
+ )}
148
+ </div>
149
+ )}
150
+ </div>
151
+ );
152
+ },
153
+ );
@@ -1,3 +1,5 @@
1
+ @use '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-tree' as tree;
2
+
1
3
  .expandableTreeNode {
2
4
  position: relative;
3
5
  width: 100%;
@@ -39,3 +41,7 @@
39
41
  width: 100%;
40
42
  min-width: 0;
41
43
  }
44
+
45
+ .treeLine {
46
+ padding-bottom: calc(#{tree.$size-tree-item-line-height} / 2);
47
+ }
@@ -1,3 +1,5 @@
1
+ import { forwardRef } from 'react';
2
+
1
3
  import { TEST_IDS } from '../../constants';
2
4
  import { ParentNode, TreeNodeProps } from '../../types';
3
5
  import { ExpandableTreeNode } from '../ExpandableTreeNode';
@@ -9,14 +11,16 @@ type TreeItemProps = {
9
11
  tabIndexAvailable?: boolean;
10
12
  };
11
13
 
12
- export function TreeItem({ node, tabIndexAvailable, parentNode }: TreeItemProps) {
14
+ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(({ node, tabIndexAvailable, parentNode }, ref) => {
13
15
  const dataTestId = node['data-test-id'] || TEST_IDS.node;
14
16
 
15
17
  const nodeProps: TreeNodeProps = { ...node, 'data-test-id': dataTestId };
16
18
 
17
19
  if (node.nested) {
18
- return <ExpandableTreeNode node={nodeProps} tabIndexAvailable={tabIndexAvailable} parentNode={parentNode} />;
20
+ return (
21
+ <ExpandableTreeNode node={nodeProps} tabIndexAvailable={tabIndexAvailable} parentNode={parentNode} ref={ref} />
22
+ );
19
23
  }
20
24
 
21
- return <TreeNode {...nodeProps} tabIndexAvailable={tabIndexAvailable} parentNode={parentNode} />;
22
- }
25
+ return <TreeNode {...nodeProps} tabIndexAvailable={tabIndexAvailable} parentNode={parentNode} ref={ref} />;
26
+ });
@@ -1,20 +1,23 @@
1
+ import cn from 'classnames';
2
+ import { CSSProperties } from 'react';
3
+
1
4
  import { TEST_IDS } from '../../constants';
2
5
  import styles from './styles.module.scss';
3
6
 
4
7
  export type TreeLineProps = {
8
+ className?: string;
5
9
  halfWidth?: boolean;
6
- halfHeight?: boolean;
7
10
  visible?: boolean;
8
11
  horizontal?: boolean;
9
- };
12
+ } & Pick<CSSProperties, 'height' | 'width'>;
10
13
 
11
- export function TreeLine({ halfWidth, halfHeight, horizontal, visible }: TreeLineProps) {
14
+ export function TreeLine({ className, halfWidth, horizontal, visible, height, width }: TreeLineProps) {
12
15
  return (
13
16
  <div
14
- className={styles.treeLine}
17
+ className={cn(styles.treeLine, className)}
18
+ style={{ height, width }}
15
19
  data-horizontal={horizontal || undefined}
16
20
  data-half-width={halfWidth || undefined}
17
- data-half-height={halfHeight || undefined}
18
21
  data-visible={visible || undefined}
19
22
  data-test-id={TEST_IDS.line}
20
23
  />
@@ -1,4 +1,4 @@
1
- @use '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-element';
1
+ @use '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-tree' as tree;
2
2
 
3
3
  .treeLine {
4
4
  pointer-events: none;
@@ -7,7 +7,7 @@
7
7
 
8
8
  flex-shrink: 0;
9
9
 
10
- width: styles-tokens-element.$size-tree-item-line-width;
10
+ width: tree.$size-tree-item-line-width;
11
11
 
12
12
  visibility: hidden;
13
13
 
@@ -23,25 +23,21 @@
23
23
  transform: translateX(-50%);
24
24
 
25
25
  width: 1px;
26
- height: calc(100% - (styles-tokens-element.$size-tree-item-height / 2));
26
+ height: 100%;
27
27
 
28
- background: styles-tokens-element.$sys-neutral-decor-default;
29
- }
30
- }
31
-
32
- &[data-half-height] {
33
- &::after {
34
- height: calc(100% - (styles-tokens-element.$size-tree-item-height / 2));
28
+ background: tree.$sys-neutral-decor-default;
35
29
  }
36
30
  }
37
31
 
38
32
  &[data-horizontal] {
33
+ @include tree.composite-var(tree.$tree-item-expand-wrap);
34
+
39
35
  &::after {
40
36
  top: 50%;
41
37
  left: -50%;
42
38
  transform: translateY(-50%);
43
39
 
44
- width: calc(150% - styles-tokens-element.$space-tree-item-child-line-padding);
40
+ width: calc(150% - #{tree.$space-tree-item-child-line-padding});
45
41
  height: 1px;
46
42
  }
47
43
 
@@ -49,8 +45,8 @@
49
45
  width: 0;
50
46
 
51
47
  &::after {
52
- left: calc(0px - styles-tokens-element.$size-tree-item-line-width / 2);
53
- width: calc(styles-tokens-element.$size-tree-item-line-width / 2);
48
+ left: calc(0px - #{tree.$size-tree-item-line-width} / 2);
49
+ width: calc(#{tree.$size-tree-item-line-width} / 2);
54
50
  }
55
51
  }
56
52
  }