@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
@@ -1,5 +1,14 @@
1
1
  import cn from 'classnames';
2
- import { FocusEvent, KeyboardEventHandler, MouseEventHandler, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ FocusEvent,
4
+ forwardRef,
5
+ KeyboardEventHandler,
6
+ MouseEventHandler,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
3
12
 
4
13
  import { ButtonFunction } from '@snack-uikit/button';
5
14
  import { ChevronRightSVG, FileSVG, FolderOpenSVG, FolderSVG } from '@snack-uikit/icons';
@@ -27,276 +36,288 @@ type TreeNodeComponentProps = TreeNodeProps & {
27
36
  tabIndexAvailable?: boolean;
28
37
  };
29
38
 
30
- export function TreeNode({
31
- id,
32
- title,
33
- icon = <FileSVG size={24} />,
34
- expandedIcon = <FolderOpenSVG size={24} />,
35
- collapsedIcon = <FolderSVG size={24} />,
36
- disabled,
37
- onClick,
38
- nested,
39
- className,
40
- onChevronClick,
41
- onKeyDown,
42
- isLoading,
43
- parentNode,
44
- tabIndexAvailable,
45
- ...rest
46
- }: TreeNodeComponentProps) {
47
- const {
48
- isMultiSelect,
49
- isSelectable,
50
- onNodeClick,
51
- selected,
52
- expandedNodes,
53
- onSelect,
54
- nodeActions,
55
- parentActions,
56
- setFocusPosition,
57
- resetFocusPosition,
58
- focusedNodeId,
59
- setFocusIndex,
60
- focusableNodeIds,
61
- showToggle,
62
- showLines,
63
- showIcons,
64
- } = useTreeContext();
65
-
66
- const [isDroplistOpen, setDroplistOpen] = useState(false);
67
- const [isDroplistTriggerFocused, setFocusDroplistTrigger] = useState(false);
68
-
69
- const ref = useRef<HTMLDivElement | null>(null);
70
-
71
- const isExpandable = Array.isArray(nested);
72
- const isExpanded = isExpandable ? expandedNodes?.includes(id) : undefined;
73
-
74
- const nestedNodesSelection = useMemo(() => {
75
- if (!nested || !selected) return undefined;
76
-
77
- return checkNestedNodesSelection(nested, Array.isArray(selected) ? selected : [selected]);
78
- }, [nested, selected]);
79
-
80
- const isSelected =
81
- (Array.isArray(selected) ? selected.includes(id) || nestedNodesSelection?.allSelected : selected === id) || false;
82
-
83
- const isFocused = focusedNodeId === id;
84
-
85
- useEffect(() => {
86
- if (ref.current && isFocused) {
87
- ref.current.focus();
88
- }
89
- }, [isFocused]);
90
-
91
- const treeNodeIcon = useMemo(() => {
92
- if (!showIcons) return undefined;
93
-
94
- if (isExpandable) {
95
- return isExpanded ? expandedIcon : collapsedIcon;
96
- }
97
-
98
- return icon;
99
- }, [showIcons, isExpandable, icon, isExpanded, expandedIcon, collapsedIcon]);
100
-
101
- const handleClick: TreeNodeProps['onClick'] = e => {
102
- onNodeClick(
103
- {
104
- id,
105
- title,
106
- disabled,
107
- nested,
108
- onClick,
109
- },
110
- e,
111
- );
112
- };
113
-
114
- const handleSelect = () => {
115
- onSelect(
116
- {
117
- id,
118
- disabled,
119
- nested,
120
- },
39
+ export const TreeNode = forwardRef<HTMLDivElement, TreeNodeComponentProps>(
40
+ (
41
+ {
42
+ id,
43
+ title,
44
+ icon = <FileSVG size={24} />,
45
+ expandedIcon = <FolderOpenSVG size={24} />,
46
+ collapsedIcon = <FolderSVG size={24} />,
47
+ disabled,
48
+ onClick,
49
+ nested,
50
+ className,
51
+ onChevronClick,
52
+ onKeyDown,
53
+ isLoading,
121
54
  parentNode,
122
- );
123
- };
55
+ tabIndexAvailable,
56
+ ...rest
57
+ },
58
+ ref,
59
+ ) => {
60
+ const {
61
+ isMultiSelect,
62
+ isSelectable,
63
+ onNodeClick,
64
+ selected,
65
+ expandedNodes,
66
+ onSelect,
67
+ nodeActions,
68
+ parentActions,
69
+ setFocusPosition,
70
+ resetFocusPosition,
71
+ focusedNodeId,
72
+ setFocusIndex,
73
+ focusableNodeIds,
74
+ showToggle,
75
+ showLines,
76
+ showIcons,
77
+ } = useTreeContext();
78
+
79
+ const [isDroplistOpen, setDroplistOpen] = useState(false);
80
+ const [isDroplistTriggerFocused, setFocusDroplistTrigger] = useState(false);
81
+
82
+ const contentRef = useRef<HTMLDivElement | null>(null);
83
+
84
+ const isExpandable = Array.isArray(nested);
85
+ const isExpanded = isExpandable ? expandedNodes?.includes(id) : undefined;
86
+
87
+ const nestedNodesSelection = useMemo(() => {
88
+ if (!nested || !selected) return undefined;
89
+
90
+ return checkNestedNodesSelection(nested, Array.isArray(selected) ? selected : [selected]);
91
+ }, [nested, selected]);
92
+
93
+ const isSelected =
94
+ (Array.isArray(selected) ? selected.includes(id) || nestedNodesSelection?.allSelected : selected === id) || false;
95
+
96
+ const isFocused = focusedNodeId === id;
97
+
98
+ useEffect(() => {
99
+ if (contentRef.current && isFocused) {
100
+ contentRef.current.focus();
101
+ }
102
+ }, [isFocused]);
124
103
 
125
- const handleFocus = (e?: FocusEvent) => {
126
- setFocusPosition(id);
104
+ const treeNodeIcon = useMemo(() => {
105
+ if (!showIcons) return undefined;
127
106
 
128
- if (!e) {
129
- setFocusDroplistTrigger(false);
130
- ref.current?.focus();
131
- }
132
- };
107
+ if (isExpandable) {
108
+ return isExpanded ? expandedIcon : collapsedIcon;
109
+ }
133
110
 
134
- const handleBlurActions = () => {
135
- if (isDroplistTriggerFocused && !isDroplistOpen) {
136
- setFocusDroplistTrigger(false);
111
+ return icon;
112
+ }, [showIcons, isExpandable, icon, isExpanded, expandedIcon, collapsedIcon]);
113
+
114
+ const handleClick: TreeNodeProps['onClick'] = e => {
115
+ onNodeClick(
116
+ {
117
+ id,
118
+ title,
119
+ disabled,
120
+ nested,
121
+ onClick,
122
+ },
123
+ e,
124
+ );
125
+ };
126
+
127
+ const handleSelect = () => {
128
+ onSelect(
129
+ {
130
+ id,
131
+ disabled,
132
+ nested,
133
+ },
134
+ parentNode,
135
+ );
136
+ };
137
+
138
+ const handleFocus = (e?: FocusEvent) => {
139
+ setFocusPosition(id);
140
+
141
+ if (!e) {
142
+ setFocusDroplistTrigger(false);
143
+ contentRef.current?.focus();
144
+ }
145
+ };
137
146
 
138
- ref.current?.blur();
139
- }
140
- };
147
+ const handleBlurActions = () => {
148
+ if (isDroplistTriggerFocused && !isDroplistOpen) {
149
+ setFocusDroplistTrigger(false);
141
150
 
142
- const handleKeyDown: KeyboardEventHandler<HTMLElement> = e => {
143
- onKeyDown?.(e);
151
+ contentRef.current?.blur();
152
+ }
153
+ };
144
154
 
145
- const focusableCount = focusableNodeIds.length;
155
+ const handleKeyDown: KeyboardEventHandler<HTMLElement> = e => {
156
+ onKeyDown?.(e);
146
157
 
147
- switch (e.key) {
148
- case 'ArrowDown': {
149
- e.preventDefault();
158
+ const focusableCount = focusableNodeIds.length;
150
159
 
151
- setFocusIndex((prev = 0) => {
152
- if (prev >= focusableCount - 1) {
153
- return focusableCount - 1;
154
- }
155
- return prev + 1;
156
- });
160
+ switch (e.key) {
161
+ case 'ArrowDown': {
162
+ e.preventDefault();
157
163
 
158
- return;
159
- }
160
- case 'ArrowUp': {
161
- e.preventDefault();
164
+ setFocusIndex((prev = 0) => {
165
+ if (prev >= focusableCount - 1) {
166
+ return focusableCount - 1;
167
+ }
168
+ return prev + 1;
169
+ });
162
170
 
163
- setFocusIndex((prev = 0) => {
164
- if (!prev) {
165
- return 0;
166
- }
167
- return prev - 1;
168
- });
171
+ return;
172
+ }
173
+ case 'ArrowUp': {
174
+ e.preventDefault();
169
175
 
170
- return;
171
- }
172
- case 'ArrowRight': {
173
- e.preventDefault();
176
+ setFocusIndex((prev = 0) => {
177
+ if (!prev) {
178
+ return 0;
179
+ }
180
+ return prev - 1;
181
+ });
174
182
 
175
- if (isExpanded || !isExpandable || disabled) {
176
- setFocusDroplistTrigger(true);
183
+ return;
177
184
  }
185
+ case 'ArrowRight': {
186
+ e.preventDefault();
178
187
 
179
- return;
180
- }
181
- case 'ArrowLeft': {
182
- e.preventDefault();
188
+ if (isExpanded || !isExpandable || disabled) {
189
+ setFocusDroplistTrigger(true);
190
+ }
183
191
 
184
- if (isDroplistTriggerFocused) {
185
- setFocusDroplistTrigger(false);
186
- ref.current?.focus();
192
+ return;
187
193
  }
194
+ case 'ArrowLeft': {
195
+ e.preventDefault();
188
196
 
189
- return;
190
- }
191
- case 'Escape': {
192
- ref.current?.blur();
193
- return;
194
- }
195
- case ' ':
196
- case 'Enter': {
197
- e.preventDefault();
198
- handleSelect();
199
- return;
197
+ if (isDroplistTriggerFocused) {
198
+ setFocusDroplistTrigger(false);
199
+ contentRef.current?.focus();
200
+ }
201
+
202
+ return;
203
+ }
204
+ case 'Escape': {
205
+ contentRef.current?.blur();
206
+ return;
207
+ }
208
+ case ' ':
209
+ case 'Enter': {
210
+ e.preventDefault();
211
+ handleSelect();
212
+ return;
213
+ }
214
+ default:
215
+ return;
200
216
  }
201
- default:
202
- return;
203
- }
204
- };
205
-
206
- const getNodeActions = nested ? parentActions : nodeActions;
207
-
208
- return (
209
- <div
210
- role='presentation'
211
- className={cn(styles.treeNode, className)}
212
- {...extractSupportProps(rest)}
213
- data-node-id={id}
214
- >
215
- {parentNode && (
216
- <TreeLine halfWidth={Boolean(nested)} horizontal visible={showLines} data-test-id={TEST_IDS.line} />
217
- )}
218
-
219
- {!parentNode && !nested && <TreeLine visible={false} />}
220
-
221
- {isExpandable && (
222
- <ButtonFunction
223
- size='xs'
224
- icon={<ChevronRightSVG />}
225
- loading={isLoading}
226
- onClick={onChevronClick}
227
- data-expanded={isExpanded || undefined}
228
- className={styles.treeNodeExpandButton}
229
- tabIndex={-1}
230
- data-test-id={TEST_IDS.chevron}
231
- />
232
- )}
217
+ };
233
218
 
219
+ const getNodeActions = nested ? parentActions : nodeActions;
220
+
221
+ return (
234
222
  <div
235
- role='treeitem'
236
- aria-expanded={isExpanded}
237
- aria-selected={
238
- isSelectable ? isSelected || (nestedNodesSelection?.someSelected && !isExpanded && !isMultiSelect) : undefined
239
- }
240
- aria-disabled={disabled}
241
- data-multiselect={isMultiSelect || undefined}
242
- data-droplist-active={isDroplistOpen || isDroplistTriggerFocused || undefined}
243
- onClick={handleClick}
244
- onKeyDown={handleKeyDown}
245
- onFocus={handleFocus}
246
- onBlur={resetFocusPosition}
247
- tabIndex={tabIndexAvailable ? 0 : -1}
248
- className={styles.treeNodeContent}
249
- data-test-id={TEST_IDS.item}
223
+ role='presentation'
224
+ className={cn(styles.treeNode, className)}
225
+ {...extractSupportProps(rest)}
226
+ data-node-id={id}
250
227
  ref={ref}
251
228
  >
252
- {(isMultiSelect || showToggle) && (
253
- <div className={styles.treeNodeCheckboxWrap}>
254
- {isMultiSelect && (
255
- <Checkbox
256
- size='s'
257
- disabled={disabled}
258
- checked={isSelected}
259
- indeterminate={!isSelected && nestedNodesSelection?.someSelected}
260
- onChange={handleSelect}
261
- onClick={stopPropagationClick}
262
- data-test-id={TEST_IDS.checkbox}
263
- tabIndex={-1}
264
- />
265
- )}
266
- {showToggle && (
267
- <Radio size='s' checked={isSelected} disabled={disabled} data-test-id={TEST_IDS.radio} tabIndex={-1} />
268
- )}
269
- </div>
229
+ {parentNode && (
230
+ <TreeLine halfWidth={Boolean(nested)} horizontal visible={showLines} data-test-id={TEST_IDS.line} />
270
231
  )}
271
232
 
272
- {treeNodeIcon && (
273
- <div className={styles.treeNodeIcon} data-test-id={TEST_IDS.icon}>
274
- {treeNodeIcon}
233
+ {!parentNode && !nested && <TreeLine visible={false} />}
234
+
235
+ {isExpandable && (
236
+ <div className={styles.treeNodeExpandButtonWrapper}>
237
+ <ButtonFunction
238
+ size='xs'
239
+ icon={<ChevronRightSVG />}
240
+ loading={isLoading}
241
+ onClick={onChevronClick}
242
+ data-expanded={isExpanded || undefined}
243
+ className={styles.treeNodeExpandButton}
244
+ tabIndex={-1}
245
+ data-test-id={TEST_IDS.chevron}
246
+ />
247
+
248
+ <TreeLine visible={isExpanded && showLines} height={'100%'} />
275
249
  </div>
276
250
  )}
277
251
 
278
- <Typography.SansBodyM tag='div' className={styles.treeNodeTitle}>
279
- {typeof title === 'string' && <TruncateString text={title} data-test-id={TEST_IDS.title} />}
280
- {typeof title !== 'string' && title({ id, disabled, nested } as TreeNodeProps)}
281
- </Typography.SansBodyM>
282
-
283
- {getNodeActions && (
284
- <TreeNodeActions
285
- getNodeActions={getNodeActions}
286
- node={{
287
- id,
288
- title,
289
- disabled,
290
- nested,
291
- }}
292
- focusNode={handleFocus}
293
- onBlurActions={handleBlurActions}
294
- isDroplistTriggerFocused={isDroplistTriggerFocused}
295
- isDroplistOpen={isDroplistOpen}
296
- setDroplistOpen={setDroplistOpen}
297
- />
298
- )}
252
+ <div
253
+ role='treeitem'
254
+ aria-expanded={isExpanded}
255
+ aria-selected={
256
+ isSelectable
257
+ ? isSelected || (nestedNodesSelection?.someSelected && !isExpanded && !isMultiSelect)
258
+ : undefined
259
+ }
260
+ aria-disabled={disabled}
261
+ data-multiselect={isMultiSelect || undefined}
262
+ data-droplist-active={isDroplistOpen || isDroplistTriggerFocused || undefined}
263
+ onClick={handleClick}
264
+ onKeyDown={handleKeyDown}
265
+ onFocus={handleFocus}
266
+ onBlur={resetFocusPosition}
267
+ tabIndex={tabIndexAvailable ? 0 : -1}
268
+ className={styles.treeNodeContent}
269
+ data-test-id={TEST_IDS.item}
270
+ ref={contentRef}
271
+ >
272
+ {(isMultiSelect || showToggle) && (
273
+ <div className={styles.treeNodeCheckboxWrap}>
274
+ {isMultiSelect && (
275
+ <Checkbox
276
+ size='s'
277
+ disabled={disabled}
278
+ checked={isSelected}
279
+ indeterminate={!isSelected && nestedNodesSelection?.someSelected}
280
+ onChange={handleSelect}
281
+ onClick={stopPropagationClick}
282
+ data-test-id={TEST_IDS.checkbox}
283
+ tabIndex={-1}
284
+ />
285
+ )}
286
+ {showToggle && (
287
+ <Radio size='s' checked={isSelected} disabled={disabled} data-test-id={TEST_IDS.radio} tabIndex={-1} />
288
+ )}
289
+ </div>
290
+ )}
291
+
292
+ {treeNodeIcon && (
293
+ <div className={styles.treeNodeIcon} data-test-id={TEST_IDS.icon}>
294
+ {treeNodeIcon}
295
+ </div>
296
+ )}
297
+
298
+ <Typography.SansBodyM tag='div' className={styles.treeNodeTitle}>
299
+ {typeof title === 'string' && <TruncateString text={title} data-test-id={TEST_IDS.title} />}
300
+ {typeof title !== 'string' && title({ id, disabled, nested } as TreeNodeProps)}
301
+ </Typography.SansBodyM>
302
+
303
+ {getNodeActions && (
304
+ <TreeNodeActions
305
+ getNodeActions={getNodeActions}
306
+ node={{
307
+ id,
308
+ title,
309
+ disabled,
310
+ nested,
311
+ }}
312
+ focusNode={handleFocus}
313
+ onBlurActions={handleBlurActions}
314
+ isDroplistTriggerFocused={isDroplistTriggerFocused}
315
+ isDroplistOpen={isDroplistOpen}
316
+ setDroplistOpen={setDroplistOpen}
317
+ />
318
+ )}
319
+ </div>
299
320
  </div>
300
- </div>
301
- );
302
- }
321
+ );
322
+ },
323
+ );
@@ -3,7 +3,7 @@
3
3
 
4
4
  .treeNode {
5
5
  display: flex;
6
- align-items: center;
6
+ align-items: flex-start;
7
7
  box-sizing: border-box;
8
8
  width: 100%;
9
9
  }
@@ -25,9 +25,15 @@
25
25
  }
26
26
 
27
27
  .treeNodeTitle {
28
+ @include tree.composite-var(tree.$tree-item-content-wrap);
29
+
30
+ display: flex;
28
31
  flex-grow: 1;
32
+ align-items: center;
33
+
29
34
  box-sizing: border-box;
30
- min-width: tree.$size-tree-item-checkbox-wrap;
35
+ min-width: tree.$size-tree-item-wrap;
36
+
31
37
  color: tree.$sys-neutral-text-main;
32
38
  }
33
39
 
@@ -48,12 +54,10 @@
48
54
  position: relative;
49
55
 
50
56
  display: flex;
51
- align-items: center;
57
+ align-items: flex-start;
52
58
 
53
59
  box-sizing: border-box;
54
60
  width: 100%;
55
- height: auto;
56
- min-height: tree.simple-var(tree.$tree-item-content-layout, 'height');
57
61
 
58
62
  &::before {
59
63
  pointer-events: none;
@@ -119,7 +123,7 @@
119
123
  }
120
124
 
121
125
  .treeNodeCheckboxWrap {
122
- @include tree.composite-var(tree.$tree-item-checkbox-wrap);
126
+ @include tree.composite-var(tree.$tree-item-toggle-wrap);
123
127
 
124
128
  display: flex;
125
129
  flex-shrink: 0;
@@ -130,6 +134,8 @@
130
134
  }
131
135
 
132
136
  .treeNodeExpandButton {
137
+ @include tree.composite-var(tree.$tree-item-expand-wrap);
138
+
133
139
  box-sizing: border-box;
134
140
 
135
141
  svg {
@@ -142,3 +148,12 @@
142
148
  }
143
149
  }
144
150
  }
151
+
152
+ .treeNodeExpandButtonWrapper {
153
+ overflow: hidden;
154
+ display: flex;
155
+ flex-direction: column;
156
+ align-self: stretch;
157
+
158
+ min-width: tree.$size-button-xs;
159
+ }