@gtivr4/a1-design-system-react 0.12.1 → 0.14.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 (62) hide show
  1. package/package.json +1 -1
  2. package/src/components/accordion/Accordion.jsx +2 -0
  3. package/src/components/banner/Banner.jsx +4 -1
  4. package/src/components/blockquote/blockquote.css +0 -2
  5. package/src/components/bottom-drawer/BottomDrawer.jsx +2 -2
  6. package/src/components/button/Button.d.ts +4 -0
  7. package/src/components/button/Button.jsx +15 -3
  8. package/src/components/button/button.css +39 -0
  9. package/src/components/calendar/calendar.css +0 -2
  10. package/src/components/card/card.css +1 -0
  11. package/src/components/checkbox-group/CheckboxGroup.jsx +1 -1
  12. package/src/components/checkbox-group/checkbox-group.css +3 -3
  13. package/src/components/choice-group/ChoiceGroup.d.ts +23 -0
  14. package/src/components/choice-group/ChoiceGroup.jsx +22 -10
  15. package/src/components/choice-group/choice-group.css +53 -7
  16. package/src/components/code/Code.d.ts +4 -0
  17. package/src/components/code/Code.jsx +44 -8
  18. package/src/components/code/code.css +29 -0
  19. package/src/components/context-menu/ContextMenu.d.ts +56 -0
  20. package/src/components/context-menu/ContextMenu.jsx +146 -0
  21. package/src/components/context-menu/context-menu.css +107 -0
  22. package/src/components/data-table/DataTable.jsx +1 -1
  23. package/src/components/definition-list/definition-list.css +15 -0
  24. package/src/components/divider/Divider.d.ts +4 -2
  25. package/src/components/divider/Divider.jsx +6 -1
  26. package/src/components/divider/divider.css +9 -5
  27. package/src/components/field/DateField.jsx +17 -2
  28. package/src/components/field/SelectField.jsx +1 -1
  29. package/src/components/field/TextField.d.ts +2 -0
  30. package/src/components/field/TextField.jsx +1 -1
  31. package/src/components/field/TextareaField.jsx +1 -1
  32. package/src/components/field/TimeField.jsx +17 -2
  33. package/src/components/field/field.css +12 -5
  34. package/src/components/field/textarea-field.css +1 -2
  35. package/src/components/fieldset/fieldset.css +2 -0
  36. package/src/components/icon-button/IconButton.d.ts +8 -0
  37. package/src/components/icon-button/IconButton.jsx +9 -4
  38. package/src/components/inline-editable/InlineEditable.d.ts +25 -0
  39. package/src/components/inline-editable/InlineEditable.jsx +77 -1
  40. package/src/components/inline-editable/inline-editable.css +44 -1
  41. package/src/components/message/Message.jsx +15 -9
  42. package/src/components/page-layout/page-layout.css +13 -0
  43. package/src/components/page-nav/page-nav.css +0 -2
  44. package/src/components/pagination/Pagination.jsx +3 -1
  45. package/src/components/radio-group/RadioGroup.jsx +1 -1
  46. package/src/components/radio-group/radio-group.css +3 -3
  47. package/src/components/section/Section.d.ts +8 -0
  48. package/src/components/section/Section.jsx +24 -0
  49. package/src/components/section/section.css +28 -0
  50. package/src/components/snackbar/Snackbar.d.ts +24 -0
  51. package/src/components/snackbar/Snackbar.jsx +11 -8
  52. package/src/components/snackbar/snackbar.css +7 -22
  53. package/src/components/stack/Stack.jsx +2 -1
  54. package/src/components/tabs/Tabs.d.ts +2 -0
  55. package/src/components/tabs/Tabs.jsx +3 -3
  56. package/src/components/tabs/tabs.css +95 -0
  57. package/src/components/top-header/TopHeader.jsx +2 -0
  58. package/src/components/tree-menu/TreeMenu.d.ts +54 -0
  59. package/src/components/tree-menu/TreeMenu.jsx +500 -0
  60. package/src/components/tree-menu/tree-menu.css +254 -0
  61. package/src/index.js +2 -0
  62. package/src/tokens.css +16 -0
@@ -0,0 +1,54 @@
1
+ import * as React from 'react';
2
+
3
+ export interface TreeItem {
4
+ /** Unique identifier for this node. */
5
+ id: string;
6
+ /** Display label. */
7
+ label: string;
8
+ /** Material Symbols icon name. */
9
+ icon?: string;
10
+ /** Renders the item as an `<a>` when provided; otherwise a `<button>`. */
11
+ href?: string;
12
+ /** Prevents interaction and applies a muted style. Default: false */
13
+ disabled?: boolean;
14
+ /** Nested child items — supports unlimited depth. */
15
+ children?: TreeItem[];
16
+ }
17
+
18
+ export interface TreeMenuProps {
19
+ /** Tree data. Supports any depth of nesting. */
20
+ items: TreeItem[];
21
+ /** ID of the currently selected item (controlled). */
22
+ selectedId?: string | null;
23
+ /** Called with the id of the item the user activates. */
24
+ onSelect?: (id: string) => void;
25
+ /** IDs of items that are expanded on initial render (uncontrolled). */
26
+ defaultExpandedIds?: string[];
27
+ /** IDs of currently expanded items (controlled). */
28
+ expandedIds?: string[];
29
+ /** Called with the new array of expanded IDs when expansion changes. */
30
+ onExpandedChange?: (ids: string[]) => void;
31
+ /** Renders "Expand all" and "Collapse all" buttons above the tree. Default: false */
32
+ showExpandControls?: boolean;
33
+ /** Called with the id of the item the user is hovering, or null when hover ends. */
34
+ onHoverChange?: (id: string | null) => void;
35
+ /** Called when the user right-clicks a tree item label. Fires with the item id and the originating mouse event. */
36
+ onItemContextMenu?: (id: string, event: React.MouseEvent) => void;
37
+ /** Enables drag-and-drop reordering and reparenting of items. Default: false */
38
+ draggable?: boolean;
39
+ /**
40
+ * Called when the user drops a dragged item onto a target.
41
+ * Only fires when `draggable` is true.
42
+ * - `position: "before"` — insert before the target.
43
+ * - `position: "after"` — insert after the target.
44
+ * - `position: "into"` — make the dragged item the last child of the target (branch nodes only).
45
+ */
46
+ onMove?: (params: { draggedId: string; targetId: string; position: 'before' | 'into' | 'after' }) => void;
47
+ /** Accessible name for the tree. Required when no visible label references the tree. */
48
+ 'aria-label'?: string;
49
+ /** ID of an element that labels the tree. */
50
+ 'aria-labelledby'?: string;
51
+ className?: string;
52
+ }
53
+
54
+ export declare function TreeMenu(props: TreeMenuProps): React.ReactElement;
@@ -0,0 +1,500 @@
1
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
2
+ import { Icon } from '../icon/Icon.jsx';
3
+ import './tree-menu.css';
4
+
5
+ // ── Context ───────────────────────────────────────────────────────────────────
6
+
7
+ const TreeCtx = createContext({
8
+ selectedId: null,
9
+ rovingId: null,
10
+ onSelect: () => {},
11
+ expandedIds: new Set(),
12
+ onToggle: () => {},
13
+ onRoving: () => {},
14
+ onHoverChange: () => {},
15
+ onItemContextMenu: () => {},
16
+ nodeRefs: { current: new Map() },
17
+ items: [],
18
+ isDraggable: false,
19
+ dragState: null,
20
+ forbiddenIds: new Set(),
21
+ onDragStart: () => {},
22
+ onDragOver: () => {},
23
+ onDragLeave: () => {},
24
+ onDrop: () => {},
25
+ onDragEnd: () => {},
26
+ });
27
+
28
+ // ── Helpers ───────────────────────────────────────────────────────────────────
29
+
30
+ function getVisibleFlat(items, expandedIds, result = []) {
31
+ for (const item of items) {
32
+ result.push(item);
33
+ if (item.children?.length && expandedIds.has(item.id)) {
34
+ getVisibleFlat(item.children, expandedIds, result);
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+
40
+ function findParent(items, targetId, parent = null) {
41
+ for (const item of items) {
42
+ if (item.id === targetId) return parent;
43
+ if (item.children?.length) {
44
+ const found = findParent(item.children, targetId, item);
45
+ if (found !== undefined) return found;
46
+ }
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ function getAllBranchIds(items, result = []) {
52
+ for (const item of items) {
53
+ if (item.children?.length) {
54
+ result.push(item.id);
55
+ getAllBranchIds(item.children, result);
56
+ }
57
+ }
58
+ return result;
59
+ }
60
+
61
+ function collectAllIds(items, result = []) {
62
+ for (const item of items) {
63
+ result.push(item.id);
64
+ if (item.children?.length) collectAllIds(item.children, result);
65
+ }
66
+ return result;
67
+ }
68
+
69
+ // Returns all descendant IDs of the item with targetId (not including itself).
70
+ function getDescendantIds(items, targetId, result = []) {
71
+ for (const item of items) {
72
+ if (item.id === targetId) {
73
+ collectAllIds(item.children ?? [], result);
74
+ return result;
75
+ }
76
+ if (item.children?.length) {
77
+ getDescendantIds(item.children, targetId, result);
78
+ }
79
+ }
80
+ return result;
81
+ }
82
+
83
+ // ── TreeItem ──────────────────────────────────────────────────────────────────
84
+
85
+ function TreeItem({ item, depth }) {
86
+ const {
87
+ selectedId, rovingId, onSelect, expandedIds, onToggle, onRoving, onHoverChange, onItemContextMenu, nodeRefs,
88
+ isDraggable, dragState, forbiddenIds,
89
+ onDragStart, onDragOver, onDragLeave, onDrop, onDragEnd,
90
+ } = useContext(TreeCtx);
91
+ const uid = useId();
92
+ const groupId = `${uid}-group`;
93
+ const onToggleRef = useRef(onToggle);
94
+ onToggleRef.current = onToggle;
95
+
96
+ const isBranch = Array.isArray(item.children);
97
+ const hasChildren = !!item.children?.length;
98
+ const isExpanded = expandedIds.has(item.id);
99
+ const isSelected = item.id === selectedId;
100
+ const isRoving = item.id === rovingId;
101
+ const isForbidden = forbiddenIds.has(item.id);
102
+ const isDragging = dragState?.draggingId === item.id;
103
+ const isOver = !isForbidden && dragState?.overId === item.id;
104
+ const dropPosition = isOver ? dragState.position : null;
105
+
106
+ const Component = item.href ? 'a' : 'button';
107
+ const extraProps = item.href
108
+ ? { href: item.disabled ? undefined : item.href, draggable: false }
109
+ : { type: 'button', disabled: item.disabled };
110
+
111
+ // Auto-expand collapsed branches when the user holds a drag over them for 600 ms.
112
+ useEffect(() => {
113
+ if (isOver && dropPosition === 'into' && hasChildren && !isExpanded) {
114
+ const timer = setTimeout(() => onToggleRef.current(item.id), 600);
115
+ return () => clearTimeout(timer);
116
+ }
117
+ }, [isOver, dropPosition, hasChildren, isExpanded, item.id]);
118
+
119
+ function handleToggle(e) {
120
+ e.stopPropagation();
121
+ onToggle(item.id);
122
+ // Return focus to the label button so roving tabindex stays coherent.
123
+ nodeRefs.current.get(item.id)?.focus();
124
+ }
125
+
126
+ function handleSelect(e) {
127
+ if (item.disabled) return;
128
+ e.stopPropagation();
129
+ onSelect(item.id);
130
+ onRoving(item.id);
131
+ }
132
+
133
+ function handleFocus() {
134
+ onRoving(item.id);
135
+ }
136
+
137
+ // ── Drag handlers ──────────────────────────────────────────────────────────
138
+
139
+ function handleDragStart(e) {
140
+ if (!isDraggable || item.disabled) return;
141
+ e.stopPropagation();
142
+ e.dataTransfer.effectAllowed = 'move';
143
+ e.dataTransfer.setData('text/plain', item.id);
144
+ onDragStart(item.id);
145
+ }
146
+
147
+ function handleDragOver(e) {
148
+ if (!dragState || isForbidden) return;
149
+ e.preventDefault();
150
+ e.stopPropagation();
151
+ e.dataTransfer.dropEffect = 'move';
152
+ const rect = e.currentTarget.getBoundingClientRect();
153
+ const ratio = (e.clientY - rect.top) / rect.height;
154
+ let position;
155
+ if (ratio < 0.3) {
156
+ position = 'before';
157
+ } else if (ratio > 0.7) {
158
+ position = 'after';
159
+ } else {
160
+ // Middle zone: reparent if the target is a branch (children array present, even empty), otherwise split evenly.
161
+ position = isBranch ? 'into' : (ratio < 0.5 ? 'before' : 'after');
162
+ }
163
+ onDragOver(item.id, position);
164
+ }
165
+
166
+ function handleDragLeave(e) {
167
+ // Only fire when the drag actually leaves this element (not just moving to a child).
168
+ if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget)) {
169
+ onDragLeave(item.id);
170
+ }
171
+ }
172
+
173
+ function handleDrop(e) {
174
+ e.preventDefault();
175
+ e.stopPropagation();
176
+ if (!isForbidden) onDrop(item.id);
177
+ }
178
+
179
+ function handleDragEnd(e) {
180
+ e.stopPropagation();
181
+ onDragEnd();
182
+ }
183
+
184
+ return (
185
+ <li
186
+ role="treeitem"
187
+ aria-selected={isSelected}
188
+ aria-expanded={hasChildren ? isExpanded : undefined}
189
+ aria-disabled={item.disabled || undefined}
190
+ >
191
+ <div
192
+ className={[
193
+ 'a1-tree-menu__row',
194
+ isSelected && 'a1-tree-menu__row--selected',
195
+ item.disabled && 'a1-tree-menu__row--disabled',
196
+ isDragging && 'a1-tree-menu__row--dragging',
197
+ isOver && dropPosition === 'before' && 'a1-tree-menu__row--drop-before',
198
+ isOver && dropPosition === 'into' && 'a1-tree-menu__row--drop-into',
199
+ isOver && dropPosition === 'after' && 'a1-tree-menu__row--drop-after',
200
+ ].filter(Boolean).join(' ')}
201
+ style={{ '--a1-tree-depth': depth }}
202
+ draggable={isDraggable && !item.disabled}
203
+ onDragStart={handleDragStart}
204
+ onDragOver={handleDragOver}
205
+ onDragLeave={handleDragLeave}
206
+ onDrop={handleDrop}
207
+ onDragEnd={handleDragEnd}
208
+ >
209
+ {/* Expand/collapse — mouse only. Keyboard uses Arrow Right/Left. */}
210
+ {hasChildren ? (
211
+ <button
212
+ type="button"
213
+ className="a1-tree-menu__toggle"
214
+ aria-label={isExpanded ? 'Collapse' : 'Expand'}
215
+ tabIndex={-1}
216
+ onMouseDown={(e) => e.preventDefault()}
217
+ onClick={handleToggle}
218
+ >
219
+ <Icon name={isExpanded ? 'indeterminate_check_box' : 'add_box'} />
220
+ </button>
221
+ ) : (
222
+ <span className="a1-tree-menu__toggle-spacer" aria-hidden="true" />
223
+ )}
224
+
225
+ {/* Select trigger */}
226
+ <Component
227
+ data-tree-id={item.id}
228
+ className={[
229
+ 'a1-tree-menu__label-btn',
230
+ item.disabled && 'a1-tree-menu__label-btn--disabled',
231
+ ].filter(Boolean).join(' ')}
232
+ tabIndex={isRoving ? 0 : -1}
233
+ ref={(el) => {
234
+ if (el) nodeRefs.current.set(item.id, el);
235
+ else nodeRefs.current.delete(item.id);
236
+ }}
237
+ onClick={handleSelect}
238
+ onFocus={handleFocus}
239
+ onMouseEnter={() => !item.disabled && onHoverChange(item.id)}
240
+ onMouseLeave={() => onHoverChange(null)}
241
+ onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onItemContextMenu(item.id, e); }}
242
+ {...extraProps}
243
+ >
244
+ {item.icon && (
245
+ <Icon name={item.icon} className="a1-tree-menu__icon" aria-hidden="true" />
246
+ )}
247
+ <span className="a1-tree-menu__label">{item.label}</span>
248
+ </Component>
249
+ </div>
250
+
251
+ {hasChildren && (
252
+ <div
253
+ className={[
254
+ 'a1-tree-menu__group-wrapper',
255
+ isExpanded && 'a1-tree-menu__group-wrapper--open',
256
+ ].filter(Boolean).join(' ')}
257
+ >
258
+ <div className="a1-tree-menu__group-inner">
259
+ <ul id={groupId} role="group" className="a1-tree-menu__group">
260
+ {item.children.map((child) => (
261
+ <TreeItem key={child.id} item={child} depth={depth + 1} />
262
+ ))}
263
+ </ul>
264
+ </div>
265
+ </div>
266
+ )}
267
+ </li>
268
+ );
269
+ }
270
+
271
+ // ── TreeMenu ──────────────────────────────────────────────────────────────────
272
+
273
+ export function TreeMenu({
274
+ items = [],
275
+ selectedId = null,
276
+ onSelect,
277
+ defaultExpandedIds = [],
278
+ expandedIds: controlledExpandedIds,
279
+ onExpandedChange,
280
+ showExpandControls = false,
281
+ onHoverChange,
282
+ onItemContextMenu,
283
+ draggable = false,
284
+ onMove,
285
+ 'aria-label': ariaLabel,
286
+ 'aria-labelledby': ariaLabelledBy,
287
+ className = '',
288
+ }) {
289
+ const isControlled = controlledExpandedIds !== undefined;
290
+
291
+ const [uncontrolledExpandedIds, setUncontrolledExpandedIds] = useState(
292
+ () => new Set(defaultExpandedIds)
293
+ );
294
+ const expandedIds = isControlled
295
+ ? new Set(controlledExpandedIds)
296
+ : uncontrolledExpandedIds;
297
+
298
+ const [rovingId, setRovingId] = useState(
299
+ () => selectedId ?? items[0]?.id ?? null
300
+ );
301
+
302
+ const [dragState, setDragState] = useState(null);
303
+
304
+ const nodeRefs = useRef(new Map());
305
+
306
+ const setExpanded = useCallback((next) => {
307
+ if (!isControlled) setUncontrolledExpandedIds(next);
308
+ onExpandedChange?.(Array.from(next));
309
+ }, [isControlled, onExpandedChange]);
310
+
311
+ const onToggle = useCallback((id) => {
312
+ const next = new Set(expandedIds);
313
+ if (next.has(id)) next.delete(id);
314
+ else next.add(id);
315
+ setExpanded(next);
316
+ }, [expandedIds, setExpanded]);
317
+
318
+ // IDs that cannot receive a drop: the dragged item itself and all its descendants.
319
+ const forbiddenIds = useMemo(() => {
320
+ if (!dragState?.draggingId) return new Set();
321
+ const ids = getDescendantIds(items, dragState.draggingId);
322
+ ids.push(dragState.draggingId);
323
+ return new Set(ids);
324
+ }, [items, dragState?.draggingId]);
325
+
326
+ // ── Drag callbacks ────────────────────────────────────────────────────────
327
+
328
+ const handleDragStart = useCallback((id) => {
329
+ setDragState({ draggingId: id, overId: null, position: 'after' });
330
+ }, []);
331
+
332
+ const handleDragOver = useCallback((id, position) => {
333
+ setDragState((prev) => {
334
+ if (!prev) return prev;
335
+ if (prev.overId === id && prev.position === position) return prev;
336
+ return { ...prev, overId: id, position };
337
+ });
338
+ }, []);
339
+
340
+ const handleDragLeave = useCallback((id) => {
341
+ setDragState((prev) => {
342
+ if (!prev || prev.overId !== id) return prev;
343
+ return { ...prev, overId: null };
344
+ });
345
+ }, []);
346
+
347
+ const handleDrop = useCallback((targetId) => {
348
+ setDragState((prev) => {
349
+ if (!prev) return null;
350
+ onMove?.({ draggedId: prev.draggingId, targetId, position: prev.position ?? 'after' });
351
+ return null;
352
+ });
353
+ }, [onMove]);
354
+
355
+ const handleDragEnd = useCallback(() => {
356
+ setDragState(null);
357
+ }, []);
358
+
359
+ // ── Expand controls ───────────────────────────────────────────────────────
360
+
361
+ function handleExpandAll() {
362
+ setExpanded(new Set(getAllBranchIds(items)));
363
+ }
364
+
365
+ function handleCollapseAll() {
366
+ setExpanded(new Set());
367
+ }
368
+
369
+ // ── Keyboard navigation ───────────────────────────────────────────────────
370
+
371
+ function focusNode(id) {
372
+ setRovingId(id);
373
+ requestAnimationFrame(() => nodeRefs.current.get(id)?.focus());
374
+ }
375
+
376
+ function handleKeyDown(e) {
377
+ const visible = getVisibleFlat(items, expandedIds);
378
+ const currentId = e.target.closest('[data-tree-id]')?.dataset?.treeId;
379
+ const currentIndex = currentId ? visible.findIndex((v) => v.id === currentId) : -1;
380
+
381
+ switch (e.key) {
382
+ case 'ArrowDown': {
383
+ e.preventDefault();
384
+ const next = visible[currentIndex + 1];
385
+ if (next) focusNode(next.id);
386
+ break;
387
+ }
388
+ case 'ArrowUp': {
389
+ e.preventDefault();
390
+ const prev = visible[currentIndex - 1];
391
+ if (prev) focusNode(prev.id);
392
+ break;
393
+ }
394
+ case 'ArrowRight': {
395
+ e.preventDefault();
396
+ if (currentIndex >= 0) {
397
+ const cur = visible[currentIndex];
398
+ if (cur.children?.length) {
399
+ if (!expandedIds.has(cur.id)) {
400
+ onToggle(cur.id);
401
+ } else {
402
+ const first = cur.children[0];
403
+ if (first) focusNode(first.id);
404
+ }
405
+ }
406
+ }
407
+ break;
408
+ }
409
+ case 'ArrowLeft': {
410
+ e.preventDefault();
411
+ if (currentIndex >= 0) {
412
+ const cur = visible[currentIndex];
413
+ if (cur.children?.length && expandedIds.has(cur.id)) {
414
+ onToggle(cur.id);
415
+ } else {
416
+ const parent = findParent(items, cur.id);
417
+ if (parent) focusNode(parent.id);
418
+ }
419
+ }
420
+ break;
421
+ }
422
+ case 'Home': {
423
+ e.preventDefault();
424
+ const first = visible[0];
425
+ if (first) focusNode(first.id);
426
+ break;
427
+ }
428
+ case 'End': {
429
+ e.preventDefault();
430
+ const last = visible[visible.length - 1];
431
+ if (last) focusNode(last.id);
432
+ break;
433
+ }
434
+ case 'Enter':
435
+ case ' ': {
436
+ e.preventDefault();
437
+ if (currentIndex >= 0) {
438
+ const cur = visible[currentIndex];
439
+ if (!cur.disabled) onSelect?.(cur.id);
440
+ }
441
+ break;
442
+ }
443
+ }
444
+ }
445
+
446
+ const tree = (
447
+ <TreeCtx.Provider
448
+ value={{
449
+ selectedId,
450
+ rovingId: rovingId ?? items[0]?.id ?? null,
451
+ onSelect: onSelect ?? (() => {}),
452
+ expandedIds,
453
+ onToggle,
454
+ onRoving: setRovingId,
455
+ onHoverChange: onHoverChange ?? (() => {}),
456
+ onItemContextMenu: onItemContextMenu ?? (() => {}),
457
+ nodeRefs,
458
+ items,
459
+ isDraggable: draggable,
460
+ dragState,
461
+ forbiddenIds,
462
+ onDragStart: handleDragStart,
463
+ onDragOver: handleDragOver,
464
+ onDragLeave: handleDragLeave,
465
+ onDrop: handleDrop,
466
+ onDragEnd: handleDragEnd,
467
+ }}
468
+ >
469
+ <ul
470
+ role="tree"
471
+ aria-label={ariaLabel}
472
+ aria-labelledby={ariaLabelledBy}
473
+ className={['a1-tree-menu', className].filter(Boolean).join(' ')}
474
+ onKeyDown={handleKeyDown}
475
+ >
476
+ {items.map((item) => (
477
+ <TreeItem key={item.id} item={item} depth={0} />
478
+ ))}
479
+ </ul>
480
+ </TreeCtx.Provider>
481
+ );
482
+
483
+ if (!showExpandControls) return tree;
484
+
485
+ return (
486
+ <div className="a1-tree-menu-root">
487
+ <div className="a1-tree-menu__controls">
488
+ <button type="button" className="a1-tree-menu__control-btn" onClick={handleExpandAll}>
489
+ <Icon name="unfold_more" />
490
+ Expand all
491
+ </button>
492
+ <button type="button" className="a1-tree-menu__control-btn" onClick={handleCollapseAll}>
493
+ <Icon name="unfold_less" />
494
+ Collapse all
495
+ </button>
496
+ </div>
497
+ {tree}
498
+ </div>
499
+ );
500
+ }