@sproutsocial/seeds-react-tree 0.3.1

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.
package/dist/index.js ADDED
@@ -0,0 +1,926 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Tree: () => Tree,
34
+ TreeCombobox: () => TreeCombobox,
35
+ TreeItem: () => TreeItem
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/Tree.tsx
40
+ var React = __toESM(require("react"));
41
+
42
+ // src/Common/treeContext.ts
43
+ var import_react = require("react");
44
+ var TreeContext = (0, import_react.createContext)(null);
45
+ function useTreeContext() {
46
+ const ctx = (0, import_react.useContext)(TreeContext);
47
+ if (!ctx) {
48
+ throw new Error("TreeItem must be rendered inside a <Tree>.");
49
+ }
50
+ return ctx;
51
+ }
52
+ var TreeItemContext = (0, import_react.createContext)({
53
+ level: 1
54
+ });
55
+
56
+ // src/Common/useTreeState.ts
57
+ var import_react2 = require("react");
58
+ function useTreeState(opts) {
59
+ const {
60
+ selectionMode,
61
+ selectableNodes,
62
+ defaultExpanded,
63
+ expanded: expandedProp,
64
+ onExpandedChange,
65
+ defaultSelected,
66
+ selected: selectedProp,
67
+ onSelectionChange
68
+ } = opts;
69
+ const [uncontrolledExpanded, setUncontrolledExpanded] = (0, import_react2.useState)(
70
+ () => new Set(defaultExpanded ?? [])
71
+ );
72
+ const [uncontrolledSelected, setUncontrolledSelected] = (0, import_react2.useState)(
73
+ () => new Set(defaultSelected ?? [])
74
+ );
75
+ const expanded = (0, import_react2.useMemo)(
76
+ () => expandedProp ? new Set(expandedProp) : uncontrolledExpanded,
77
+ [expandedProp, uncontrolledExpanded]
78
+ );
79
+ const selected = (0, import_react2.useMemo)(
80
+ () => selectedProp ? new Set(selectedProp) : uncontrolledSelected,
81
+ [selectedProp, uncontrolledSelected]
82
+ );
83
+ const toggleExpanded = (0, import_react2.useCallback)(
84
+ (id, next) => {
85
+ const current = expandedProp ? new Set(expandedProp) : uncontrolledExpanded;
86
+ const willOpen = next ?? !current.has(id);
87
+ const updated = new Set(current);
88
+ if (willOpen) {
89
+ updated.add(id);
90
+ } else {
91
+ updated.delete(id);
92
+ }
93
+ if (!expandedProp) {
94
+ setUncontrolledExpanded(updated);
95
+ }
96
+ onExpandedChange?.(Array.from(updated));
97
+ },
98
+ [expandedProp, uncontrolledExpanded, onExpandedChange]
99
+ );
100
+ const toggleSelected = (0, import_react2.useCallback)(
101
+ (id, hasChildren) => {
102
+ if (selectionMode === "none") return;
103
+ if (selectableNodes === "leaves" && hasChildren) return;
104
+ const current = selectedProp ? new Set(selectedProp) : uncontrolledSelected;
105
+ const updated = /* @__PURE__ */ new Set();
106
+ if (selectionMode === "single") {
107
+ if (!current.has(id)) {
108
+ updated.add(id);
109
+ }
110
+ } else {
111
+ current.forEach((v) => updated.add(v));
112
+ if (updated.has(id)) {
113
+ updated.delete(id);
114
+ } else {
115
+ updated.add(id);
116
+ }
117
+ }
118
+ if (!selectedProp) {
119
+ setUncontrolledSelected(updated);
120
+ }
121
+ onSelectionChange?.(Array.from(updated));
122
+ },
123
+ [
124
+ selectionMode,
125
+ selectableNodes,
126
+ selectedProp,
127
+ uncontrolledSelected,
128
+ onSelectionChange
129
+ ]
130
+ );
131
+ return {
132
+ expanded,
133
+ toggleExpanded,
134
+ selected,
135
+ toggleSelected
136
+ };
137
+ }
138
+
139
+ // src/TreeStyles.tsx
140
+ var import_styled_components = __toESM(require("styled-components"));
141
+ var TreeRoot = import_styled_components.default.ul`
142
+ list-style: none;
143
+ margin: 0;
144
+ padding: 0;
145
+ font-family: ${({ theme }) => theme.fontFamily};
146
+ ${({ theme }) => theme.typography[300]}
147
+ color: ${({ theme }) => theme.colors.text.body};
148
+ `;
149
+ var TreeItemRow = import_styled_components.default.div`
150
+ display: flex;
151
+ align-items: center;
152
+ gap: ${({ theme }) => theme.space[300]};
153
+ padding: ${({ theme }) => theme.space[300]};
154
+ padding-left: ${({ theme, $level }) => `calc(${theme.space[300]} + ${$level - 1} * ${theme.space[500]})`};
155
+ border-radius: ${({ theme }) => theme.radii[400]};
156
+ cursor: pointer;
157
+ user-select: none;
158
+ background: transparent;
159
+ transition: background-color ${({ theme }) => theme.duration.fast}
160
+ ${({ theme }) => theme.easing.ease_in};
161
+
162
+ &:hover {
163
+ background: ${({ theme }) => theme.colors.listItem.background.hover};
164
+ }
165
+
166
+ ${({ $selected, theme }) => $selected && import_styled_components.css`
167
+ background: ${theme.colors.listItem.background.hover};
168
+ font-weight: ${theme.fontWeights.semibold};
169
+ `}
170
+
171
+ ${({ $disabled }) => $disabled && import_styled_components.css`
172
+ opacity: 0.4;
173
+ cursor: not-allowed;
174
+ pointer-events: none;
175
+ `}
176
+ `;
177
+ var TreeItemEl = import_styled_components.default.li`
178
+ list-style: none;
179
+ outline: none;
180
+
181
+ /*
182
+ * Tree rows stack tightly, so the standard outset Seeds focusRing bleeds
183
+ * into adjacent rows. Use an inset outline so the ring is drawn just inside
184
+ * the row's edge and never overlaps siblings or children.
185
+ *
186
+ * The same ring is drawn when [data-treeitem-active] is set so combobox
187
+ * hosts that drive the tree via aria-activedescendant (no real DOM focus)
188
+ * still get a visible "active" indicator on the row.
189
+ */
190
+ &:focus-visible > ${TreeItemRow}, &[data-treeitem-active] > ${TreeItemRow} {
191
+ outline: 2px solid
192
+ ${({ theme }) => theme.colors.button.primary.background.base};
193
+ outline-offset: -2px;
194
+ }
195
+ `;
196
+ var TreeItemGroup = import_styled_components.default.ul`
197
+ list-style: none;
198
+ margin: 0;
199
+ padding: 0;
200
+ `;
201
+ var TreeItemLabel = import_styled_components.default.span`
202
+ flex: 1;
203
+ min-width: 0;
204
+ overflow: hidden;
205
+ text-overflow: ellipsis;
206
+ white-space: nowrap;
207
+ `;
208
+ var TreeItemIcon = import_styled_components.default.span`
209
+ display: inline-flex;
210
+ align-items: center;
211
+ flex-shrink: 0;
212
+ `;
213
+ var TreeItemChevron = import_styled_components.default.button.attrs({
214
+ type: "button",
215
+ tabIndex: -1
216
+ })`
217
+ display: inline-flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ flex-shrink: 0;
221
+ width: ${({ theme }) => theme.space[500]};
222
+ height: ${({ theme }) => theme.space[500]};
223
+ padding: 0;
224
+ border: none;
225
+ background: transparent;
226
+ color: ${({ theme }) => theme.colors.icon.base};
227
+ cursor: pointer;
228
+ border-radius: ${({ theme }) => theme.radii[300]};
229
+ transition: transform ${({ theme }) => theme.duration.fast}
230
+ ${({ theme }) => theme.easing.ease_in};
231
+ transform: ${({ $expanded }) => $expanded ? "rotate(0deg)" : "rotate(-90deg)"};
232
+
233
+ &:hover {
234
+ background: ${({ theme }) => theme.colors.listItem.background.hover};
235
+ }
236
+ `;
237
+
238
+ // src/Tree.tsx
239
+ var import_jsx_runtime = require("react/jsx-runtime");
240
+ var Tree = React.forwardRef(function Tree2(props, forwardedRef) {
241
+ const {
242
+ children,
243
+ selectionMode = "none",
244
+ selectableNodes = "all",
245
+ defaultExpanded,
246
+ expanded: expandedProp,
247
+ onExpandedChange,
248
+ defaultSelected,
249
+ selected: selectedProp,
250
+ onSelectionChange,
251
+ renderSelectionIndicator,
252
+ defaultFocusedId,
253
+ focusedId: focusedIdProp,
254
+ onFocusedIdChange,
255
+ manageDomFocus = true,
256
+ className,
257
+ id
258
+ } = props;
259
+ const ariaLabel = props["aria-label"];
260
+ const ariaLabelledBy = props["aria-labelledby"];
261
+ const innerRef = React.useRef(null);
262
+ React.useImperativeHandle(forwardedRef, () => innerRef.current, []);
263
+ const { expanded, toggleExpanded, selected, toggleSelected } = useTreeState({
264
+ selectionMode,
265
+ selectableNodes,
266
+ defaultExpanded,
267
+ expanded: expandedProp,
268
+ onExpandedChange,
269
+ defaultSelected,
270
+ selected: selectedProp,
271
+ onSelectionChange
272
+ });
273
+ const isFocusedControlled = focusedIdProp !== void 0;
274
+ const [internalFocusedId, setInternalFocusedId] = React.useState(defaultFocusedId ?? null);
275
+ const focusedId = isFocusedControlled ? focusedIdProp ?? null : internalFocusedId;
276
+ const setFocusedId = React.useCallback(
277
+ (nextId) => {
278
+ if (!isFocusedControlled) setInternalFocusedId(nextId);
279
+ onFocusedIdChange?.(nextId);
280
+ },
281
+ [isFocusedControlled, onFocusedIdChange]
282
+ );
283
+ const [hasFocused, setHasFocused] = React.useState(false);
284
+ React.useLayoutEffect(() => {
285
+ const root = innerRef.current;
286
+ if (!root || focusedId === null) return;
287
+ const stillExists = root.querySelector(
288
+ `[role="treeitem"][data-treeitem-id="${CSS.escape(focusedId)}"]`
289
+ );
290
+ if (stillExists) return;
291
+ const firstVisible = root.querySelector('[role="treeitem"]');
292
+ setFocusedId(firstVisible?.dataset.treeitemId ?? null);
293
+ });
294
+ const ctxValue = React.useMemo(
295
+ () => ({
296
+ focusedId,
297
+ setFocusedId,
298
+ expanded,
299
+ toggleExpanded,
300
+ selected,
301
+ toggleSelected,
302
+ selectionMode,
303
+ selectableNodes,
304
+ renderSelectionIndicator,
305
+ rootRef: innerRef,
306
+ hasFocused,
307
+ setHasFocused,
308
+ manageDomFocus
309
+ }),
310
+ [
311
+ focusedId,
312
+ setFocusedId,
313
+ expanded,
314
+ toggleExpanded,
315
+ selected,
316
+ toggleSelected,
317
+ selectionMode,
318
+ selectableNodes,
319
+ renderSelectionIndicator,
320
+ hasFocused,
321
+ manageDomFocus
322
+ ]
323
+ );
324
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeContext.Provider, { value: ctxValue, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeItemContext.Provider, { value: { level: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
325
+ TreeRoot,
326
+ {
327
+ ref: innerRef,
328
+ role: "tree",
329
+ id,
330
+ className,
331
+ "aria-label": ariaLabel,
332
+ "aria-labelledby": ariaLabelledBy,
333
+ "aria-multiselectable": selectionMode === "multiple" ? true : void 0,
334
+ children
335
+ }
336
+ ) }) });
337
+ });
338
+
339
+ // src/TreeItem.tsx
340
+ var React2 = __toESM(require("react"));
341
+ var import_seeds_react_icon = require("@sproutsocial/seeds-react-icon");
342
+
343
+ // src/Common/useTreeKeyboard.ts
344
+ var import_react3 = require("react");
345
+
346
+ // src/Common/treeNavigation.ts
347
+ function getVisibleTreeItems(root) {
348
+ if (!root) return [];
349
+ const all = Array.from(
350
+ root.querySelectorAll('[role="treeitem"]')
351
+ );
352
+ return all.filter((el) => {
353
+ if (el.getAttribute("aria-disabled") === "true") return false;
354
+ let parent = el.parentElement;
355
+ while (parent && parent !== root) {
356
+ if (parent.getAttribute("role") === "group" && parent.hasAttribute("hidden")) {
357
+ return false;
358
+ }
359
+ parent = parent.parentElement;
360
+ }
361
+ return true;
362
+ });
363
+ }
364
+ function getTreeItemId(el) {
365
+ return el.dataset.treeitemId ?? null;
366
+ }
367
+ function treeItemDomId(itemId) {
368
+ return `${itemId}__item`;
369
+ }
370
+ function computeTreeNavigation(root, currentEl, key, meta) {
371
+ const visible = getVisibleTreeItems(root);
372
+ if (visible.length === 0) return { preventDefault: false };
373
+ const idOf = (el) => {
374
+ if (!el) return void 0;
375
+ return getTreeItemId(el) ?? void 0;
376
+ };
377
+ if (!currentEl || !meta) {
378
+ if (key === "ArrowDown" || key === "Home") {
379
+ return { preventDefault: true, nextFocusedId: idOf(visible[0]) };
380
+ }
381
+ if (key === "ArrowUp" || key === "End") {
382
+ return {
383
+ preventDefault: true,
384
+ nextFocusedId: idOf(visible[visible.length - 1])
385
+ };
386
+ }
387
+ return { preventDefault: false };
388
+ }
389
+ const currentIndex = visible.indexOf(currentEl);
390
+ if (currentIndex === -1) return { preventDefault: false };
391
+ const clamp = (i) => Math.max(0, Math.min(visible.length - 1, i));
392
+ switch (key) {
393
+ case "ArrowDown":
394
+ return {
395
+ preventDefault: true,
396
+ nextFocusedId: idOf(visible[clamp(currentIndex + 1)])
397
+ };
398
+ case "ArrowUp":
399
+ return {
400
+ preventDefault: true,
401
+ nextFocusedId: idOf(visible[clamp(currentIndex - 1)])
402
+ };
403
+ case "ArrowRight": {
404
+ if (meta.hasChildren && !meta.isExpanded) {
405
+ return {
406
+ preventDefault: true,
407
+ expandToggle: { id: meta.id, next: true }
408
+ };
409
+ }
410
+ if (meta.hasChildren && meta.isExpanded) {
411
+ return {
412
+ preventDefault: true,
413
+ nextFocusedId: idOf(visible[clamp(currentIndex + 1)])
414
+ };
415
+ }
416
+ return { preventDefault: true };
417
+ }
418
+ case "ArrowLeft": {
419
+ if (meta.hasChildren && meta.isExpanded) {
420
+ return {
421
+ preventDefault: true,
422
+ expandToggle: { id: meta.id, next: false }
423
+ };
424
+ }
425
+ let parent = currentEl.parentElement;
426
+ while (parent && parent !== root) {
427
+ if (parent.getAttribute("role") === "group") {
428
+ const parentItem = parent.parentElement;
429
+ if (parentItem?.getAttribute("role") === "treeitem") {
430
+ return {
431
+ preventDefault: true,
432
+ nextFocusedId: idOf(parentItem)
433
+ };
434
+ }
435
+ }
436
+ parent = parent.parentElement;
437
+ }
438
+ return { preventDefault: true };
439
+ }
440
+ case "Home":
441
+ return { preventDefault: true, nextFocusedId: idOf(visible[0]) };
442
+ case "End":
443
+ return {
444
+ preventDefault: true,
445
+ nextFocusedId: idOf(visible[visible.length - 1])
446
+ };
447
+ case "Enter":
448
+ case " ":
449
+ return {
450
+ preventDefault: true,
451
+ selectToggle: { id: meta.id, hasChildren: meta.hasChildren }
452
+ };
453
+ case "*": {
454
+ const siblings = Array.from(
455
+ currentEl.parentElement?.children ?? []
456
+ ).filter(
457
+ (el) => el instanceof HTMLElement && el.getAttribute("role") === "treeitem"
458
+ );
459
+ const toExpand = siblings.filter((s) => s.getAttribute("aria-expanded") === "false").map((s) => getTreeItemId(s)).filter((id) => id !== null);
460
+ return { preventDefault: true, expandSiblings: toExpand };
461
+ }
462
+ }
463
+ return { preventDefault: false };
464
+ }
465
+
466
+ // src/Common/useTreeKeyboard.ts
467
+ var TYPEAHEAD_TIMEOUT_MS = 500;
468
+ function focusItem(el) {
469
+ if (!el) return;
470
+ el.focus();
471
+ }
472
+ function useTreeKeyboard(ctx) {
473
+ const typeaheadBufferRef = (0, import_react3.useRef)("");
474
+ const typeaheadTimerRef = (0, import_react3.useRef)(null);
475
+ const handleTypeahead = (0, import_react3.useCallback)(
476
+ (char, current, visible) => {
477
+ if (typeaheadTimerRef.current) clearTimeout(typeaheadTimerRef.current);
478
+ typeaheadBufferRef.current = (typeaheadBufferRef.current + char).toLowerCase();
479
+ typeaheadTimerRef.current = setTimeout(() => {
480
+ typeaheadBufferRef.current = "";
481
+ }, TYPEAHEAD_TIMEOUT_MS);
482
+ const buffer = typeaheadBufferRef.current;
483
+ const currentIndex = visible.indexOf(current);
484
+ const ordered = [
485
+ ...visible.slice(currentIndex + 1),
486
+ ...visible.slice(0, currentIndex + 1)
487
+ ];
488
+ const match = ordered.find(
489
+ (el) => (el.textContent ?? "").trim().toLowerCase().startsWith(buffer)
490
+ );
491
+ if (match) {
492
+ const id = getTreeItemId(match);
493
+ if (id) ctx.setFocusedId(id);
494
+ focusItem(match);
495
+ }
496
+ },
497
+ [ctx]
498
+ );
499
+ return (0, import_react3.useCallback)(
500
+ (e, itemMeta) => {
501
+ if (itemMeta.isDisabled) return;
502
+ if (e.target !== e.currentTarget) return;
503
+ const root = ctx.rootRef.current;
504
+ const current = e.currentTarget;
505
+ const result = computeTreeNavigation(root, current, e.key, {
506
+ id: itemMeta.id,
507
+ hasChildren: itemMeta.hasChildren,
508
+ isExpanded: itemMeta.isExpanded,
509
+ level: itemMeta.level
510
+ });
511
+ if (result.preventDefault) e.preventDefault();
512
+ if (result.expandToggle) {
513
+ ctx.toggleExpanded(result.expandToggle.id, result.expandToggle.next);
514
+ }
515
+ if (result.selectToggle) {
516
+ ctx.toggleSelected(
517
+ result.selectToggle.id,
518
+ result.selectToggle.hasChildren
519
+ );
520
+ }
521
+ if (result.expandSiblings) {
522
+ result.expandSiblings.forEach((id) => ctx.toggleExpanded(id, true));
523
+ }
524
+ if (result.nextFocusedId) {
525
+ ctx.setFocusedId(result.nextFocusedId);
526
+ if (ctx.manageDomFocus) {
527
+ const next = root?.querySelector(
528
+ `[data-treeitem-id="${CSS.escape(result.nextFocusedId)}"]`
529
+ );
530
+ focusItem(next ?? void 0);
531
+ }
532
+ }
533
+ if (!result.preventDefault && ctx.manageDomFocus && e.key.length === 1 && /\S/.test(e.key) && !e.ctrlKey && !e.metaKey && !e.altKey) {
534
+ handleTypeahead(e.key, current, getVisibleTreeItems(root));
535
+ }
536
+ },
537
+ [ctx, handleTypeahead]
538
+ );
539
+ }
540
+
541
+ // src/TreeItem.tsx
542
+ var import_jsx_runtime2 = require("react/jsx-runtime");
543
+ function hasTreeItemChildren(children) {
544
+ let found = false;
545
+ React2.Children.forEach(children, (child) => {
546
+ if (found) return;
547
+ if (!React2.isValidElement(child)) return;
548
+ const childType = child.type;
549
+ if (childType === React2.Fragment) {
550
+ found = hasTreeItemChildren(
551
+ child.props.children
552
+ );
553
+ return;
554
+ }
555
+ found = true;
556
+ });
557
+ return found;
558
+ }
559
+ function TreeItem(props) {
560
+ const { id, label, icon, disabled = false, children } = props;
561
+ const ctx = useTreeContext();
562
+ const { level } = React2.useContext(TreeItemContext);
563
+ const hasChildren = React2.useMemo(
564
+ () => hasTreeItemChildren(children),
565
+ [children]
566
+ );
567
+ const isExpanded = ctx.expanded.has(id);
568
+ const isSelected = ctx.selected.has(id);
569
+ const selectionMode = ctx.selectionMode;
570
+ const itemRef = React2.useRef(null);
571
+ const isTabbable = ctx.manageDomFocus && ctx.focusedId === id;
572
+ React2.useEffect(() => {
573
+ if (!ctx.manageDomFocus) return;
574
+ if (ctx.focusedId !== null) return;
575
+ const root = ctx.rootRef.current;
576
+ if (!root) return;
577
+ const first = root.querySelector('[role="treeitem"]');
578
+ if (first === itemRef.current) {
579
+ ctx.setFocusedId(id);
580
+ }
581
+ }, []);
582
+ const handleKeyDown = useTreeKeyboard(ctx);
583
+ const handleRowClick = (e) => {
584
+ if (disabled) return;
585
+ const target = e.target;
586
+ if (target.closest("[data-tree-chevron]")) return;
587
+ ctx.setFocusedId(id);
588
+ if (ctx.manageDomFocus) itemRef.current?.focus();
589
+ if (hasChildren && selectionMode === "none") {
590
+ ctx.toggleExpanded(id);
591
+ return;
592
+ }
593
+ if (hasChildren && ctx.selectableNodes === "leaves") {
594
+ ctx.toggleExpanded(id);
595
+ return;
596
+ }
597
+ if (selectionMode !== "none") {
598
+ ctx.toggleSelected(id, hasChildren);
599
+ }
600
+ };
601
+ const handleChevronClick = (e) => {
602
+ e.stopPropagation();
603
+ if (disabled) return;
604
+ ctx.setFocusedId(id);
605
+ if (ctx.manageDomFocus) itemRef.current?.focus();
606
+ ctx.toggleExpanded(id);
607
+ };
608
+ const ariaSelected = selectionMode === "single" && ctx.selectableNodes !== "leaves" ? isSelected : void 0;
609
+ const ariaChecked = selectionMode === "multiple" ? isSelected : selectionMode === "single" && ctx.selectableNodes === "leaves" ? isSelected : void 0;
610
+ const groupId = `${id}__group`;
611
+ const labelId = `${id}__label`;
612
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
613
+ TreeItemEl,
614
+ {
615
+ ref: itemRef,
616
+ id: treeItemDomId(id),
617
+ role: "treeitem",
618
+ "data-treeitem-id": id,
619
+ "data-treeitem-active": !ctx.manageDomFocus && ctx.focusedId === id ? true : void 0,
620
+ "aria-level": level,
621
+ "aria-expanded": hasChildren ? isExpanded : void 0,
622
+ "aria-selected": ariaSelected,
623
+ "aria-checked": ariaChecked,
624
+ "aria-disabled": disabled || void 0,
625
+ "aria-labelledby": labelId,
626
+ "aria-owns": hasChildren && isExpanded ? groupId : void 0,
627
+ tabIndex: isTabbable ? 0 : -1,
628
+ onKeyDown: (e) => {
629
+ if (!ctx.manageDomFocus) return;
630
+ handleKeyDown(e, {
631
+ id,
632
+ hasChildren,
633
+ isExpanded,
634
+ isDisabled: disabled,
635
+ level
636
+ });
637
+ },
638
+ onClick: (e) => {
639
+ const target = e.target;
640
+ const nearestTreeitem = target.closest('[role="treeitem"]');
641
+ if (nearestTreeitem !== e.currentTarget) return;
642
+ handleRowClick(e);
643
+ },
644
+ onMouseDown: (e) => {
645
+ if (!ctx.manageDomFocus) e.preventDefault();
646
+ },
647
+ onFocus: () => {
648
+ if (!ctx.manageDomFocus) return;
649
+ if (!ctx.hasFocused) ctx.setHasFocused(true);
650
+ if (ctx.focusedId !== id) ctx.setFocusedId(id);
651
+ },
652
+ children: [
653
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
654
+ TreeItemRow,
655
+ {
656
+ "data-treeitem-row": true,
657
+ $level: level,
658
+ $selected: isSelected,
659
+ $disabled: disabled,
660
+ children: [
661
+ ctx.renderSelectionIndicator && !(ctx.selectableNodes === "leaves" && hasChildren) ? ctx.renderSelectionIndicator({
662
+ selected: isSelected,
663
+ disabled,
664
+ selectionMode
665
+ }) : null,
666
+ icon != null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TreeItemIcon, { "aria-hidden": true, children: icon }) : null,
667
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TreeItemLabel, { id: labelId, children: label }),
668
+ hasChildren ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
669
+ TreeItemChevron,
670
+ {
671
+ "data-tree-chevron": true,
672
+ "aria-hidden": true,
673
+ $expanded: isExpanded,
674
+ onClick: handleChevronClick,
675
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_seeds_react_icon.Icon, { name: "chevron-down-outline", size: "small" })
676
+ }
677
+ ) : null
678
+ ]
679
+ }
680
+ ),
681
+ hasChildren ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TreeItemGroup, { role: "group", id: groupId, hidden: !isExpanded, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TreeItemContext.Provider, { value: { level: level + 1 }, children }) }) : null
682
+ ]
683
+ }
684
+ );
685
+ }
686
+ TreeItem.displayName = "TreeItem";
687
+
688
+ // src/TreeCombobox.tsx
689
+ var React3 = __toESM(require("react"));
690
+ var import_styled_components2 = __toESM(require("styled-components"));
691
+ var import_seeds_react_icon2 = require("@sproutsocial/seeds-react-icon");
692
+ var import_seeds_react_mixins = require("@sproutsocial/seeds-react-mixins");
693
+
694
+ // src/Common/filterTree.ts
695
+ function filterTree(items, query) {
696
+ const normalized = query.trim().toLowerCase();
697
+ if (!normalized) {
698
+ return { items: [...items], forceExpanded: [] };
699
+ }
700
+ const forceExpanded = /* @__PURE__ */ new Set();
701
+ const walk = (node) => {
702
+ const matches = node.label.toLowerCase().includes(normalized);
703
+ const filteredChildren = (node.children ?? []).map(walk).filter((c) => c !== null);
704
+ if (filteredChildren.length > 0 && node.children) {
705
+ forceExpanded.add(node.id);
706
+ }
707
+ if (matches || filteredChildren.length > 0) {
708
+ return {
709
+ ...node,
710
+ children: node.children ? filteredChildren : void 0
711
+ };
712
+ }
713
+ return null;
714
+ };
715
+ const filtered = items.map(walk).filter((n) => n !== null);
716
+ return { items: filtered, forceExpanded: Array.from(forceExpanded) };
717
+ }
718
+
719
+ // src/TreeCombobox.tsx
720
+ var import_jsx_runtime3 = require("react/jsx-runtime");
721
+ var Root = import_styled_components2.default.div`
722
+ display: flex;
723
+ flex-direction: column;
724
+ gap: ${({ theme }) => theme.space[300]};
725
+ `;
726
+ var InputGroup = import_styled_components2.default.div`
727
+ display: flex;
728
+ align-items: center;
729
+ gap: ${({ theme }) => theme.space[200]};
730
+ padding: ${({ theme }) => theme.space[200]} ${({ theme }) => theme.space[300]};
731
+ border-radius: ${({ theme }) => theme.radii[500]};
732
+ border: 1px solid ${({ theme }) => theme.colors.form.border.base};
733
+ background: ${({ theme }) => theme.colors.form.background.base};
734
+ color: ${({ theme }) => theme.colors.icon.base};
735
+
736
+ &:focus-within {
737
+ ${import_seeds_react_mixins.focusRing}
738
+ }
739
+ `;
740
+ var ComboboxInput = import_styled_components2.default.input`
741
+ flex: 1;
742
+ border: none;
743
+ background: transparent;
744
+ outline: none;
745
+ font-family: ${({ theme }) => theme.fontFamily};
746
+ ${({ theme }) => theme.typography[300]}
747
+ color: ${({ theme }) => theme.colors.text.body};
748
+
749
+ &::placeholder {
750
+ color: ${({ theme }) => theme.colors.text.subtext};
751
+ }
752
+ `;
753
+ var EmptyState = import_styled_components2.default.div`
754
+ padding: ${({ theme }) => theme.space[400]};
755
+ text-align: center;
756
+ color: ${({ theme }) => theme.colors.text.subtext};
757
+ ${({ theme }) => theme.typography[300]}
758
+ `;
759
+ var NAV_KEYS = /* @__PURE__ */ new Set([
760
+ "ArrowDown",
761
+ "ArrowUp",
762
+ "ArrowLeft",
763
+ "ArrowRight",
764
+ "Home",
765
+ "End",
766
+ "Enter"
767
+ ]);
768
+ function TreeCombobox(props) {
769
+ const {
770
+ items,
771
+ placeholder = "Search...",
772
+ emptyText = "No results found.",
773
+ query: queryProp,
774
+ defaultQuery = "",
775
+ onQueryChange,
776
+ selectionMode = "none",
777
+ selectableNodes = "all",
778
+ defaultExpanded,
779
+ expanded: expandedProp,
780
+ onExpandedChange,
781
+ defaultSelected,
782
+ selected: selectedProp,
783
+ onSelectionChange,
784
+ renderSelectionIndicator,
785
+ id,
786
+ className
787
+ } = props;
788
+ const ariaLabel = props["aria-label"];
789
+ const ariaLabelledBy = props["aria-labelledby"];
790
+ const [uncontrolledQuery, setUncontrolledQuery] = React3.useState(defaultQuery);
791
+ const query = queryProp ?? uncontrolledQuery;
792
+ const setQuery = (next) => {
793
+ if (queryProp === void 0) setUncontrolledQuery(next);
794
+ onQueryChange?.(next);
795
+ };
796
+ const [userExpanded, setUserExpanded] = React3.useState(() => [
797
+ ...defaultExpanded ?? expandedProp ?? []
798
+ ]);
799
+ const { items: visibleItems, forceExpanded } = React3.useMemo(
800
+ () => filterTree([...items], query),
801
+ [items, query]
802
+ );
803
+ const isFiltering = query.trim().length > 0;
804
+ const effectiveExpanded = isFiltering ? Array.from(/* @__PURE__ */ new Set([...userExpanded, ...forceExpanded])) : expandedProp ?? userExpanded;
805
+ const handleExpandedChange = (next) => {
806
+ if (!isFiltering) {
807
+ setUserExpanded(next);
808
+ }
809
+ onExpandedChange?.(next);
810
+ };
811
+ const [focusedId, setFocusedId] = React3.useState(null);
812
+ const treeRef = React3.useRef(null);
813
+ const reactId = React3.useId();
814
+ const treeDomId = id ? `${id}-tree` : `${reactId}-tree`;
815
+ const findTreeItemEl = (itemId) => treeRef.current?.querySelector(
816
+ `[role="treeitem"][data-treeitem-id="${CSS.escape(itemId)}"]`
817
+ ) ?? null;
818
+ const handleInputKeyDown = (e) => {
819
+ if (e.key === "Escape") {
820
+ if (query.length > 0) {
821
+ e.preventDefault();
822
+ setQuery("");
823
+ setFocusedId(null);
824
+ }
825
+ return;
826
+ }
827
+ if (!NAV_KEYS.has(e.key)) return;
828
+ const currentEl = focusedId ? findTreeItemEl(focusedId) : null;
829
+ const meta = currentEl && focusedId ? {
830
+ id: focusedId,
831
+ hasChildren: currentEl.getAttribute("aria-expanded") !== null,
832
+ isExpanded: currentEl.getAttribute("aria-expanded") === "true",
833
+ level: parseInt(currentEl.getAttribute("aria-level") ?? "1", 10)
834
+ } : null;
835
+ if (e.key === "Enter") {
836
+ if (!currentEl) return;
837
+ e.preventDefault();
838
+ const row = currentEl.querySelector("[data-treeitem-row]");
839
+ row?.click();
840
+ return;
841
+ }
842
+ const result = computeTreeNavigation(
843
+ treeRef.current,
844
+ currentEl,
845
+ e.key,
846
+ meta
847
+ );
848
+ if (result.preventDefault) e.preventDefault();
849
+ if (result.expandToggle) {
850
+ const current = new Set(effectiveExpanded);
851
+ const willOpen = result.expandToggle.next ?? !current.has(result.expandToggle.id);
852
+ if (willOpen) current.add(result.expandToggle.id);
853
+ else current.delete(result.expandToggle.id);
854
+ handleExpandedChange(Array.from(current));
855
+ }
856
+ if (result.nextFocusedId) {
857
+ setFocusedId(result.nextFocusedId);
858
+ }
859
+ };
860
+ const showEmpty = visibleItems.length === 0;
861
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Root, { className, children: [
862
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(InputGroup, { children: [
863
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_seeds_react_icon2.Icon, { name: "magnifying-glass-outline", size: "small", "aria-hidden": true }),
864
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
865
+ ComboboxInput,
866
+ {
867
+ type: "text",
868
+ role: "combobox",
869
+ id,
870
+ "aria-label": ariaLabel,
871
+ "aria-labelledby": ariaLabelledBy,
872
+ "aria-expanded": !showEmpty,
873
+ "aria-controls": treeDomId,
874
+ "aria-haspopup": "tree",
875
+ "aria-autocomplete": "list",
876
+ "aria-activedescendant": focusedId ? treeItemDomId(focusedId) : void 0,
877
+ placeholder,
878
+ value: query,
879
+ onChange: (e) => setQuery(e.target.value),
880
+ onKeyDown: handleInputKeyDown
881
+ }
882
+ )
883
+ ] }),
884
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
885
+ Tree,
886
+ {
887
+ ref: treeRef,
888
+ "aria-label": ariaLabel,
889
+ "aria-labelledby": ariaLabelledBy,
890
+ id: treeDomId,
891
+ manageDomFocus: false,
892
+ selectionMode,
893
+ selectableNodes,
894
+ expanded: effectiveExpanded,
895
+ onExpandedChange: handleExpandedChange,
896
+ selected: selectedProp,
897
+ defaultSelected,
898
+ onSelectionChange,
899
+ renderSelectionIndicator,
900
+ focusedId,
901
+ onFocusedIdChange: setFocusedId,
902
+ children: visibleItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TreeNode, { item }, item.id))
903
+ }
904
+ ),
905
+ showEmpty ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EmptyState, { role: "status", "aria-live": "polite", children: emptyText }) : null
906
+ ] });
907
+ }
908
+ function TreeNode({ item }) {
909
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
910
+ TreeItem,
911
+ {
912
+ id: item.id,
913
+ label: item.label,
914
+ icon: item.icon,
915
+ disabled: item.disabled,
916
+ children: item.children?.map((child) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TreeNode, { item: child }, child.id))
917
+ }
918
+ );
919
+ }
920
+ // Annotate the CommonJS export names for ESM import in node:
921
+ 0 && (module.exports = {
922
+ Tree,
923
+ TreeCombobox,
924
+ TreeItem
925
+ });
926
+ //# sourceMappingURL=index.js.map