@edux-design/tree-select 0.2.5 → 0.3.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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/elements/TreeSelect.jsx
2
- import React3, { useCallback as useCallback2, useMemo as useMemo2, useRef, useState } from "react";
2
+ import React3, { forwardRef, useCallback as useCallback2, useImperativeHandle, useMemo as useMemo2, useRef, useState } from "react";
3
3
  import {
4
4
  DndContext,
5
5
  DragOverlay,
@@ -30,6 +30,21 @@ import {
30
30
  TooltipTrigger
31
31
  } from "@edux-design/tooltips";
32
32
 
33
+ // src/elements/treeSelectInteraction.js
34
+ function isTreeRowControl(event) {
35
+ const control = event.target?.closest?.(
36
+ 'button, input, textarea, select, a[href], [contenteditable="true"], [role="button"], [role="checkbox"], [role="menuitem"], [tabindex]:not([tabindex="-1"]), [data-tree-interactive]'
37
+ );
38
+ return Boolean(control && control !== event.currentTarget && event.currentTarget.contains(control));
39
+ }
40
+ function hasActivationModifier(event) {
41
+ return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
42
+ }
43
+ function getTreeItemLabel(node, getItemLabel) {
44
+ if (getItemLabel) return getItemLabel(node);
45
+ return typeof node.label === "string" || typeof node.label === "number" ? String(node.label) : String(node.id);
46
+ }
47
+
33
48
  // src/elements/treeSelectConstants.js
34
49
  var HANDLE_WIDTH = 28;
35
50
  var TOGGLE_WIDTH = 28;
@@ -38,6 +53,8 @@ var INDICATOR_INSET = HANDLE_WIDTH + TOGGLE_WIDTH + 12;
38
53
 
39
54
  // src/elements/TreeRow.jsx
40
55
  import { jsx, jsxs } from "react/jsx-runtime";
56
+ var DRAG_ROW_BACKGROUND = "color-mix(in srgb, var(--color-fg-base) 4%, transparent)";
57
+ var SELECTED_ROW_BACKGROUND = "color-mix(in srgb, var(--color-primary-600) 8%, transparent)";
41
58
  var TreeRow = ({
42
59
  item,
43
60
  depth,
@@ -49,6 +66,11 @@ var TreeRow = ({
49
66
  dragAttributes,
50
67
  dragRef,
51
68
  rowRef,
69
+ rowAccessibility,
70
+ labelContext,
71
+ accessibleLabel,
72
+ rowClassName,
73
+ labelClassName,
52
74
  isDragging,
53
75
  isSelected,
54
76
  onRowClick,
@@ -81,11 +103,12 @@ var TreeRow = ({
81
103
  depth,
82
104
  parentId: item.parentId,
83
105
  hasChildren: item.hasChildren,
84
- isExpanded
106
+ isExpanded,
107
+ ...labelContext
85
108
  }) : label;
86
109
  const showToggle = item.hasChildren;
87
110
  const [additionalMenuOpen, setAdditionalMenuOpen] = React.useState(false);
88
- const rowBackground = isDragging ? "rgba(0, 0, 0, 0.04)" : isSelected ? "rgba(15, 98, 254, 0.08)" : void 0;
111
+ const rowBackground = isDragging ? DRAG_ROW_BACKGROUND : isSelected ? SELECTED_ROW_BACKGROUND : void 0;
89
112
  const rowPaddingLeft = depth * INDENT_WIDTH + 8;
90
113
  const hasAdditionalActionsMenu = Boolean(additionalActionsMenu);
91
114
  const showActions = canAddChild || canAddSibling || canDelete || hasAdditionalActionsMenu;
@@ -106,9 +129,12 @@ var TreeRow = ({
106
129
  "div",
107
130
  {
108
131
  ref: rowRef,
109
- onClick: (event) => onRowClick == null ? void 0 : onRowClick(event, item.id),
132
+ ...rowAccessibility,
133
+ onClick: (event) => onRowClick?.(event, item.id),
110
134
  "data-tree-row": item.id,
111
- className: "transition-colors hover:bg-bg-primary-subtle",
135
+ "data-selected": isSelected || void 0,
136
+ "data-current": labelContext?.isCurrent || void 0,
137
+ className: ["transition-colors hover:bg-bg-primary-subtle focus-visible:shadow-focus", labelContext?.isCurrent ? "bg-bg-primary-subtle text-fg-primary-emphasis" : "", rowClassName].filter(Boolean).join(" "),
112
138
  style: {
113
139
  display: "flex",
114
140
  alignItems: "center",
@@ -127,7 +153,7 @@ var TreeRow = ({
127
153
  ...dragAttributes,
128
154
  ...dragHandleProps,
129
155
  ref: dragRef,
130
- "aria-label": "Drag item",
156
+ "aria-label": `Move ${accessibleLabel}`,
131
157
  onClick: (event) => event.stopPropagation(),
132
158
  className: "border border-border-subtle bg-bg-base text-fg-base",
133
159
  style: {
@@ -147,7 +173,8 @@ var TreeRow = ({
147
173
  "button",
148
174
  {
149
175
  type: "button",
150
- "aria-label": toggleLabel,
176
+ "aria-label": `${toggleLabel}: ${accessibleLabel}`,
177
+ "aria-expanded": isExpanded,
151
178
  onClick: () => onToggle(item.id),
152
179
  style: {
153
180
  width: 24,
@@ -169,6 +196,8 @@ var TreeRow = ({
169
196
  /* @__PURE__ */ jsx(
170
197
  Checkbox,
171
198
  {
199
+ "aria-label": `${toggleLabel}: ${accessibleLabel}`,
200
+ "aria-expanded": isExpanded,
172
201
  checked: false,
173
202
  plus: !isExpanded,
174
203
  indeterminate: isExpanded,
@@ -180,19 +209,21 @@ var TreeRow = ({
180
209
  "input",
181
210
  {
182
211
  type: "text",
212
+ "aria-label": `Edit ${accessibleLabel}`,
213
+ onClick: (event) => event.stopPropagation(),
183
214
  value: draftLabel,
184
215
  autoFocus: true,
185
216
  "data-tree-input": item.id,
186
217
  onChange: (event) => onDraftChange(event.target.value),
187
218
  onBlur: onCommitEdit,
188
219
  onKeyDown: (event) => {
220
+ event.stopPropagation();
189
221
  if (event.key === "Enter") onCommitEdit();
190
222
  if (event.key === "Escape") onCancelEdit();
191
223
  },
192
224
  style: {
193
225
  fontSize: 14,
194
226
  border: "none",
195
- outline: "none",
196
227
  background: "transparent",
197
228
  padding: 0
198
229
  }
@@ -201,19 +232,30 @@ var TreeRow = ({
201
232
  "span",
202
233
  {
203
234
  onClick: (event) => {
204
- if (!isEditable) return;
235
+ if (!isEditable || event.defaultPrevented || isTreeRowControl(event)) return;
205
236
  event.stopPropagation();
206
- onStartEdit == null ? void 0 : onStartEdit();
237
+ onStartEdit?.();
207
238
  },
239
+ role: isEditable ? "button" : void 0,
240
+ tabIndex: isEditable ? 0 : void 0,
241
+ "aria-label": isEditable ? `Edit ${accessibleLabel}` : void 0,
242
+ onKeyDown: isEditable ? (event) => {
243
+ if (event.target !== event.currentTarget) return;
244
+ if (event.key === "Enter" || event.key === " ") {
245
+ event.preventDefault();
246
+ event.stopPropagation();
247
+ onStartEdit?.();
248
+ }
249
+ } : void 0,
208
250
  "data-tree-label": item.id,
251
+ className: [renderLabel ? "inline-flex items-center gap-6" : "", labelClassName].filter(Boolean).join(" "),
209
252
  style: {
210
253
  fontSize: 14,
254
+ display: "inline-flex",
255
+ alignItems: "center",
256
+ minHeight: isEditable ? 24 : void 0,
211
257
  cursor: isEditable ? "pointer" : "default",
212
- ...renderLabel ? {
213
- display: "inline-flex",
214
- alignItems: "center",
215
- gap: 6
216
- } : labelStyle
258
+ ...!renderLabel ? labelStyle : {}
217
259
  },
218
260
  children: labelContent
219
261
  }
@@ -233,7 +275,7 @@ var TreeRow = ({
233
275
  isBare: true,
234
276
  icon: /* @__PURE__ */ jsx(Plus, { "aria-hidden": "true" }),
235
277
  "aria-label": "Add child",
236
- onClick: () => onAddChild == null ? void 0 : onAddChild(item.id)
278
+ onClick: () => onAddChild?.(item.id)
237
279
  }
238
280
  ) }),
239
281
  /* @__PURE__ */ jsx(TooltipContent, { children: addChildTooltip ?? "Add child" })
@@ -246,7 +288,7 @@ var TreeRow = ({
246
288
  isBare: true,
247
289
  icon: /* @__PURE__ */ jsx(List, { "aria-hidden": "true" }),
248
290
  "aria-label": "Add sibling",
249
- onClick: () => onAddSibling == null ? void 0 : onAddSibling(item.id)
291
+ onClick: () => onAddSibling?.(item.id)
250
292
  }
251
293
  ) }),
252
294
  /* @__PURE__ */ jsx(TooltipContent, { children: addSiblingTooltip ?? "Add sibling" })
@@ -259,7 +301,7 @@ var TreeRow = ({
259
301
  isBare: true,
260
302
  icon: /* @__PURE__ */ jsx(Close, { "aria-hidden": "true" }),
261
303
  "aria-label": "Delete item",
262
- onClick: () => onDelete == null ? void 0 : onDelete(item.id)
304
+ onClick: () => onDelete?.(item.id)
263
305
  }
264
306
  ) }),
265
307
  /* @__PURE__ */ jsx(TooltipContent, { children: deleteTooltip ?? "Delete item" })
@@ -300,6 +342,12 @@ var TreeSortableRow = ({
300
342
  onToggle,
301
343
  onRowClick,
302
344
  rowRefs,
345
+ rowAccessibility,
346
+ labelContext,
347
+ accessibleLabel,
348
+ rowClassName,
349
+ labelClassName,
350
+ onRowSelect,
303
351
  isActive,
304
352
  isSelected,
305
353
  isGroupDragging,
@@ -347,12 +395,11 @@ var TreeSortableRow = ({
347
395
  () => ({
348
396
  ...listeners,
349
397
  onPointerDown: (event) => {
350
- var _a;
351
- (_a = listeners == null ? void 0 : listeners.onPointerDown) == null ? void 0 : _a.call(listeners, event);
352
- onRowClick == null ? void 0 : onRowClick(event, item.id);
398
+ listeners?.onPointerDown?.(event);
399
+ onRowSelect?.(event, item.id);
353
400
  }
354
401
  }),
355
- [item.id, listeners, onRowClick]
402
+ [item.id, listeners, onRowSelect]
356
403
  );
357
404
  return /* @__PURE__ */ jsx2(
358
405
  TreeRow,
@@ -366,6 +413,11 @@ var TreeSortableRow = ({
366
413
  dragAttributes: attributes,
367
414
  dragRef: setActivatorNodeRef,
368
415
  rowRef: rowRefCallback,
416
+ rowAccessibility,
417
+ labelContext,
418
+ accessibleLabel,
419
+ rowClassName,
420
+ labelClassName,
369
421
  isDragging: isDragging || isActive || isGroupDragging,
370
422
  isSelected,
371
423
  isEditable,
@@ -440,11 +492,10 @@ var collectDraggedIds = (items, rootIds) => {
440
492
  var getSelectedRootIds = (items, selectedIds) => {
441
493
  const itemById = new Map(items.map((item) => [item.id, item]));
442
494
  return items.filter((item) => selectedIds.has(item.id)).filter((item) => {
443
- var _a, _b;
444
- let parentId = (_a = itemById.get(item.id)) == null ? void 0 : _a.parentId;
495
+ let parentId = itemById.get(item.id)?.parentId;
445
496
  while (parentId) {
446
497
  if (selectedIds.has(parentId)) return false;
447
- parentId = (_b = itemById.get(parentId)) == null ? void 0 : _b.parentId;
498
+ parentId = itemById.get(parentId)?.parentId;
448
499
  }
449
500
  return true;
450
501
  }).map((item) => item.id);
@@ -744,7 +795,7 @@ var getDropIndicator = ({
744
795
 
745
796
  // src/elements/TreeSelect.jsx
746
797
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
747
- function TreeSelect({
798
+ var TreeSelect = forwardRef(function TreeSelect2({
748
799
  data,
749
800
  onDataChange,
750
801
  allowDragAndDrop = false,
@@ -764,8 +815,16 @@ function TreeSelect({
764
815
  expanded,
765
816
  onExpandedChange,
766
817
  renderAdditionalActionsMenu,
767
- renderLabel
768
- }) {
818
+ renderLabel,
819
+ onNodeActivate,
820
+ currentId,
821
+ getItemLabel,
822
+ rowClassName,
823
+ labelClassName,
824
+ "aria-label": ariaLabel = "Tree",
825
+ "aria-labelledby": ariaLabelledBy,
826
+ ...rootProps
827
+ }, forwardedRef) {
769
828
  const [uncontrolledExpandedIds, setExpandedIds] = useState(
770
829
  () => getExpandedIds(data ?? [], defaultExpanded)
771
830
  );
@@ -775,6 +834,8 @@ function TreeSelect({
775
834
  const [horizontalOffset, setHorizontalOffset] = useState(0);
776
835
  const [selectedIds, setSelectedIds] = useState(() => /* @__PURE__ */ new Set());
777
836
  const [lastSelectedId, setLastSelectedId] = useState(null);
837
+ const [focusedId, setFocusedId] = useState(null);
838
+ const [focusWithinId, setFocusWithinId] = useState(null);
778
839
  const [editingId, setEditingId] = useState(null);
779
840
  const [draftLabel, setDraftLabel] = useState("");
780
841
  const idCounterRef = useRef(0);
@@ -794,6 +855,22 @@ function TreeSelect({
794
855
  () => new Map(visibleItems.map((item) => [item.id, item])),
795
856
  [visibleItems]
796
857
  );
858
+ const getRowElement = useCallback2((id) => rowRefs.current.get(normaliseId(id)) ?? null, []);
859
+ useImperativeHandle(forwardedRef, () => ({
860
+ getRowElement,
861
+ focusItem(id, options) {
862
+ const row = getRowElement(id);
863
+ if (!row) return false;
864
+ row.focus(options);
865
+ return true;
866
+ },
867
+ scrollToItem(id, options = { block: "nearest", inline: "nearest" }) {
868
+ const row = getRowElement(id);
869
+ if (!row || typeof row.scrollIntoView !== "function") return false;
870
+ row.scrollIntoView(options);
871
+ return true;
872
+ }
873
+ }), [getRowElement]);
797
874
  const siblingOrderMap = useMemo2(
798
875
  () => buildSiblingOrderMap(treeData),
799
876
  [treeData]
@@ -804,19 +881,18 @@ function TreeSelect({
804
881
  }, [maxDepth]);
805
882
  const resolvedLevelConfig = useCallback2(
806
883
  (depth) => {
807
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
808
- const defaults = (levelConfig == null ? void 0 : levelConfig.default) ?? {};
809
- const perLevel = ((_a = levelConfig == null ? void 0 : levelConfig.levels) == null ? void 0 : _a[depth]) ?? {};
884
+ const defaults = levelConfig?.default ?? {};
885
+ const perLevel = levelConfig?.levels?.[depth] ?? {};
810
886
  const editable = perLevel.editable ?? defaults.editable ?? isEditable;
811
887
  const actions = {
812
- addChild: ((_b = perLevel.actions) == null ? void 0 : _b.addChild) ?? ((_c = defaults.actions) == null ? void 0 : _c.addChild) ?? editable,
813
- addSibling: ((_d = perLevel.actions) == null ? void 0 : _d.addSibling) ?? ((_e = defaults.actions) == null ? void 0 : _e.addSibling) ?? editable,
814
- delete: ((_f = perLevel.actions) == null ? void 0 : _f.delete) ?? ((_g = defaults.actions) == null ? void 0 : _g.delete) ?? (editable && allowDelete)
888
+ addChild: perLevel.actions?.addChild ?? defaults.actions?.addChild ?? editable,
889
+ addSibling: perLevel.actions?.addSibling ?? defaults.actions?.addSibling ?? editable,
890
+ delete: perLevel.actions?.delete ?? defaults.actions?.delete ?? (editable && allowDelete)
815
891
  };
816
892
  const tooltips = {
817
- addChild: ((_h = perLevel.tooltips) == null ? void 0 : _h.addChild) ?? ((_i = defaults.tooltips) == null ? void 0 : _i.addChild) ?? addChildTooltip,
818
- addSibling: ((_j = perLevel.tooltips) == null ? void 0 : _j.addSibling) ?? ((_k = defaults.tooltips) == null ? void 0 : _k.addSibling) ?? addSiblingTooltip,
819
- delete: ((_l = perLevel.tooltips) == null ? void 0 : _l.delete) ?? ((_m = defaults.tooltips) == null ? void 0 : _m.delete) ?? deleteTooltip
893
+ addChild: perLevel.tooltips?.addChild ?? defaults.tooltips?.addChild ?? addChildTooltip,
894
+ addSibling: perLevel.tooltips?.addSibling ?? defaults.tooltips?.addSibling ?? addSiblingTooltip,
895
+ delete: perLevel.tooltips?.delete ?? defaults.tooltips?.delete ?? deleteTooltip
820
896
  };
821
897
  return { editable, actions, tooltips };
822
898
  },
@@ -836,7 +912,7 @@ function TreeSelect({
836
912
  (updater) => {
837
913
  if (isExpandedControlled) {
838
914
  const next = updater(new Set(expandedIds));
839
- onExpandedChange == null ? void 0 : onExpandedChange(next);
915
+ onExpandedChange?.(next);
840
916
  return;
841
917
  }
842
918
  setExpandedIds((prev) => updater(new Set(prev)));
@@ -845,7 +921,7 @@ function TreeSelect({
845
921
  );
846
922
  const emitDataChange = useCallback2(
847
923
  (nextTree, meta) => {
848
- onDataChange == null ? void 0 : onDataChange(nextTree, meta);
924
+ onDataChange?.(nextTree, meta);
849
925
  },
850
926
  [onDataChange]
851
927
  );
@@ -859,17 +935,16 @@ function TreeSelect({
859
935
  return prev;
860
936
  });
861
937
  }, [updateExpandedIds]);
862
- const handleRowClick = useCallback2(
938
+ const handleRowSelection = useCallback2(
863
939
  (event, id) => {
864
- var _a, _b;
865
940
  if (!allowMultiDrag) return;
866
941
  const hasMultiKey = event.metaKey || event.ctrlKey;
867
- const parentId = ((_a = itemById.get(id)) == null ? void 0 : _a.parentId) ?? null;
942
+ const parentId = itemById.get(id)?.parentId ?? null;
868
943
  const siblingOrder = siblingOrderMap.get(parentId) ?? [];
869
944
  if (event.shiftKey) {
870
945
  const anchorIndex = siblingOrder.indexOf(lastSelectedId);
871
946
  const targetIndex = siblingOrder.indexOf(id);
872
- if (anchorIndex < 0 || targetIndex < 0 || (((_b = itemById.get(lastSelectedId ?? "")) == null ? void 0 : _b.parentId) ?? null) !== parentId) {
947
+ if (anchorIndex < 0 || targetIndex < 0 || (itemById.get(lastSelectedId ?? "")?.parentId ?? null) !== parentId) {
873
948
  setSelectedIds(/* @__PURE__ */ new Set([id]));
874
949
  setLastSelectedId(id);
875
950
  return;
@@ -891,10 +966,9 @@ function TreeSelect({
891
966
  return;
892
967
  }
893
968
  setSelectedIds((prev) => {
894
- var _a2;
895
969
  if (prev.size) {
896
970
  const firstSelected = prev.values().next().value;
897
- const selectedParent = ((_a2 = itemById.get(firstSelected)) == null ? void 0 : _a2.parentId) ?? null;
971
+ const selectedParent = itemById.get(firstSelected)?.parentId ?? null;
898
972
  if (selectedParent !== parentId) {
899
973
  return /* @__PURE__ */ new Set([id]);
900
974
  }
@@ -921,6 +995,16 @@ function TreeSelect({
921
995
  siblingOrderMap
922
996
  ]
923
997
  );
998
+ const activateItem = useCallback2((event, id, inputSource) => {
999
+ if (event.defaultPrevented || event.repeat || inputSource === "pointer" && event.button !== 0 || hasActivationModifier(event) || isTreeRowControl(event)) return;
1000
+ const item = itemById.get(id);
1001
+ if (item && !activeId) onNodeActivate?.({ ...item, event, inputSource });
1002
+ }, [activeId, itemById, onNodeActivate]);
1003
+ const handleRowClick = useCallback2((event, id) => {
1004
+ if (event.defaultPrevented || isTreeRowControl(event)) return;
1005
+ handleRowSelection(event, id);
1006
+ activateItem(event, id, "pointer");
1007
+ }, [activateItem, handleRowSelection]);
924
1008
  const draggedRootIds = useMemo2(
925
1009
  () => getDraggedRootIds({
926
1010
  activeId,
@@ -952,13 +1036,11 @@ function TreeSelect({
952
1036
  setHorizontalOffset(event.delta.x);
953
1037
  }, []);
954
1038
  const handleDragOver = useCallback2((event) => {
955
- var _a;
956
- if ((_a = event.over) == null ? void 0 : _a.id) {
1039
+ if (event.over?.id) {
957
1040
  setOverId(normaliseId(event.over.id));
958
1041
  }
959
1042
  }, []);
960
1043
  const handleDragEnd = useCallback2(() => {
961
- var _a;
962
1044
  const projection = getDropProjection({
963
1045
  items: visibleItems,
964
1046
  activeId,
@@ -974,7 +1056,7 @@ function TreeSelect({
974
1056
  return;
975
1057
  }
976
1058
  const { parentId, siblingIndex, projectedDepth } = projection;
977
- const sourceParentId = ((_a = itemById.get(draggedRootIds[0] ?? activeId)) == null ? void 0 : _a.parentId) ?? null;
1059
+ const sourceParentId = itemById.get(draggedRootIds[0] ?? activeId)?.parentId ?? null;
978
1060
  const { nextNodes, removedNodes } = removeNodes(treeData, draggedRootIds);
979
1061
  if (!removedNodes.length) {
980
1062
  setActiveId(null);
@@ -1087,8 +1169,7 @@ function TreeSelect({
1087
1169
  );
1088
1170
  const startEdit = useCallback2(
1089
1171
  (id) => {
1090
- var _a, _b, _c, _d;
1091
- const currentLabel = ((_b = (_a = itemById.get(id)) == null ? void 0 : _a.node) == null ? void 0 : _b.label) ?? ((_d = (_c = itemById.get(id)) == null ? void 0 : _c.node) == null ? void 0 : _d.id) ?? "";
1172
+ const currentLabel = itemById.get(id)?.node?.label ?? itemById.get(id)?.node?.id ?? "";
1092
1173
  setEditingId(id);
1093
1174
  setDraftLabel(String(currentLabel));
1094
1175
  },
@@ -1121,15 +1202,15 @@ function TreeSelect({
1121
1202
  if (!item) return null;
1122
1203
  if (!renderLabel) return item.node.label ?? item.id;
1123
1204
  return renderLabel({
1124
- id: item.id,
1125
- node: item.node,
1126
- depth: item.depth,
1127
- parentId: item.parentId,
1128
- hasChildren: item.hasChildren,
1129
- isExpanded: expandedIds.has(item.id)
1205
+ ...item,
1206
+ isExpanded: expandedIds.has(item.id),
1207
+ isCurrent: currentId != null && normaliseId(currentId) === item.id,
1208
+ isSelected: selectedIds.has(item.id),
1209
+ isFocused: focusWithinId === item.id,
1210
+ getRowElement: () => null
1130
1211
  });
1131
1212
  },
1132
- [expandedIds, renderLabel]
1213
+ [currentId, expandedIds, focusWithinId, renderLabel, selectedIds]
1133
1214
  );
1134
1215
  const indicator = useMemo2(() => {
1135
1216
  if (!allowDragAndDrop) return null;
@@ -1152,7 +1233,61 @@ function TreeSelect({
1152
1233
  visibleItems,
1153
1234
  maxDepth
1154
1235
  ]);
1155
- return /* @__PURE__ */ jsx3(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsx3("div", { style: { position: "relative", padding: 8 }, children: allowDragAndDrop ? /* @__PURE__ */ jsxs2(
1236
+ const rowContext = (item) => ({
1237
+ ...item,
1238
+ isExpanded: expandedIds.has(item.id),
1239
+ isCurrent: currentId != null && normaliseId(currentId) === item.id,
1240
+ isSelected: selectedIds.has(item.id),
1241
+ isFocused: focusWithinId === item.id,
1242
+ getRowElement: () => getRowElement(item.id)
1243
+ });
1244
+ const rowAccessibility = (item) => ({
1245
+ role: "treeitem",
1246
+ "aria-label": getTreeItemLabel(item.node, getItemLabel),
1247
+ "aria-level": item.depth + 1,
1248
+ "aria-expanded": item.hasChildren ? expandedIds.has(item.id) : void 0,
1249
+ "aria-selected": allowMultiDrag ? selectedIds.has(item.id) : void 0,
1250
+ "aria-current": currentId != null && normaliseId(currentId) === item.id ? "page" : void 0,
1251
+ "aria-posinset": (siblingOrderMap.get(item.parentId) || []).indexOf(item.id) + 1,
1252
+ "aria-setsize": (siblingOrderMap.get(item.parentId) || []).length,
1253
+ tabIndex: (focusedId && itemById.has(focusedId) ? focusedId : visibleItems[0]?.id) === item.id ? 0 : -1,
1254
+ onFocus: () => {
1255
+ setFocusedId(item.id);
1256
+ setFocusWithinId(item.id);
1257
+ },
1258
+ onBlur: (event) => {
1259
+ if (!event.currentTarget.contains(event.relatedTarget)) setFocusWithinId(null);
1260
+ },
1261
+ onKeyDown: (event) => {
1262
+ if (event.target !== event.currentTarget || event.defaultPrevented) return;
1263
+ if (event.repeat && (event.key === "Enter" || event.key === " ")) {
1264
+ event.preventDefault();
1265
+ return;
1266
+ }
1267
+ const index = visibleItems.findIndex((row) => row.id === item.id);
1268
+ let target;
1269
+ if (event.key === "ArrowDown") target = visibleItems[Math.min(index + 1, visibleItems.length - 1)];
1270
+ if (event.key === "ArrowUp") target = visibleItems[Math.max(index - 1, 0)];
1271
+ if (event.key === "Home") target = visibleItems[0];
1272
+ if (event.key === "End") target = visibleItems[visibleItems.length - 1];
1273
+ if (event.key === "ArrowRight" && item.hasChildren) {
1274
+ if (!expandedIds.has(item.id)) toggleExpanded(item.id);
1275
+ else target = visibleItems[index + 1];
1276
+ }
1277
+ if (event.key === "ArrowLeft") {
1278
+ if (item.hasChildren && expandedIds.has(item.id)) toggleExpanded(item.id);
1279
+ else target = itemById.get(item.parentId);
1280
+ }
1281
+ if (event.key === "F2" && resolvedLevelConfig(item.depth).editable) startEdit(item.id);
1282
+ if (event.key === "Enter" || event.key === " ") {
1283
+ handleRowSelection(event, item.id);
1284
+ activateItem(event, item.id, "keyboard");
1285
+ }
1286
+ if (["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft", "Home", "End", "F2", " ", "Enter"].includes(event.key)) event.preventDefault();
1287
+ if (target) getRowElement(target.id)?.focus();
1288
+ }
1289
+ });
1290
+ return /* @__PURE__ */ jsx3(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsx3("div", { ...rootProps, "data-data-component": "tree-select", style: { position: "relative", padding: 8, ...rootProps.style }, children: allowDragAndDrop ? /* @__PURE__ */ jsxs2(
1156
1291
  DndContext,
1157
1292
  {
1158
1293
  sensors,
@@ -1163,24 +1298,24 @@ function TreeSelect({
1163
1298
  onDragEnd: handleDragEnd,
1164
1299
  onDragCancel: handleDragCancel,
1165
1300
  children: [
1166
- /* @__PURE__ */ jsx3(SortableContext, { items: visibleItems.map((item) => item.id), children: /* @__PURE__ */ jsxs2("div", { ref: listRef, style: { position: "relative" }, children: [
1301
+ /* @__PURE__ */ jsx3(SortableContext, { items: visibleItems.map((item) => item.id), children: /* @__PURE__ */ jsxs2("div", { ref: listRef, role: "tree", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-multiselectable": allowMultiDrag || void 0, style: { position: "relative" }, children: [
1167
1302
  indicator ? /* @__PURE__ */ jsx3(
1168
1303
  "div",
1169
1304
  {
1305
+ className: "bg-bg-primary-base",
1170
1306
  style: {
1171
1307
  position: "absolute",
1172
1308
  top: indicator.top,
1173
1309
  left: indicator.left,
1174
1310
  right: 8,
1175
1311
  height: 2,
1176
- background: "#0f62fe",
1177
1312
  borderRadius: 999
1178
1313
  }
1179
1314
  }
1180
1315
  ) : null,
1181
1316
  visibleItems.map((item) => {
1182
1317
  const levelSettings = resolvedLevelConfig(item.depth);
1183
- const additionalActionsMenu = renderAdditionalActionsMenu == null ? void 0 : renderAdditionalActionsMenu({
1318
+ const additionalActionsMenu = renderAdditionalActionsMenu?.({
1184
1319
  id: item.id,
1185
1320
  node: item.node,
1186
1321
  depth: item.depth,
@@ -1195,6 +1330,12 @@ function TreeSelect({
1195
1330
  onToggle: toggleExpanded,
1196
1331
  onRowClick: handleRowClick,
1197
1332
  rowRefs,
1333
+ onRowSelect: handleRowSelection,
1334
+ rowAccessibility: rowAccessibility(item),
1335
+ labelContext: rowContext(item),
1336
+ accessibleLabel: getTreeItemLabel(item.node, getItemLabel),
1337
+ rowClassName: typeof rowClassName === "function" ? rowClassName(rowContext(item)) : rowClassName,
1338
+ labelClassName: typeof labelClassName === "function" ? labelClassName(rowContext(item)) : labelClassName,
1198
1339
  isActive: activeId === item.id,
1199
1340
  isSelected: selectedIds.has(item.id),
1200
1341
  isGroupDragging: Boolean(
@@ -1229,20 +1370,15 @@ function TreeSelect({
1229
1370
  /* @__PURE__ */ jsx3(DragOverlay, { children: activeId ? /* @__PURE__ */ jsx3(
1230
1371
  "div",
1231
1372
  {
1232
- className: "border border-border-subtle bg-bg-base text-fg-base",
1373
+ className: "border border-border-subtle bg-bg-base text-fg-base shadow-lg",
1233
1374
  style: {
1234
1375
  padding: "6px 8px",
1235
- borderRadius: 6,
1236
- boxShadow: "0 8px 24px rgba(0, 0, 0, 0.12)"
1376
+ borderRadius: 6
1237
1377
  },
1238
1378
  children: draggedRootIds.length > 1 ? `${draggedRootIds.length} items` : /* @__PURE__ */ jsx3(
1239
1379
  "span",
1240
1380
  {
1241
- style: {
1242
- display: "inline-flex",
1243
- alignItems: "center",
1244
- gap: 6
1245
- },
1381
+ className: "inline-flex items-center gap-6",
1246
1382
  children: renderLabelContent(
1247
1383
  visibleItems.find((item) => item.id === activeId)
1248
1384
  ) ?? activeId
@@ -1252,9 +1388,9 @@ function TreeSelect({
1252
1388
  ) : null })
1253
1389
  ]
1254
1390
  }
1255
- ) : /* @__PURE__ */ jsx3("div", { ref: listRef, style: { position: "relative" }, children: visibleItems.map((item) => {
1391
+ ) : /* @__PURE__ */ jsx3("div", { ref: listRef, role: "tree", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-multiselectable": allowMultiDrag || void 0, style: { position: "relative" }, children: visibleItems.map((item) => {
1256
1392
  const levelSettings = resolvedLevelConfig(item.depth);
1257
- const additionalActionsMenu = renderAdditionalActionsMenu == null ? void 0 : renderAdditionalActionsMenu({
1393
+ const additionalActionsMenu = renderAdditionalActionsMenu?.({
1258
1394
  id: item.id,
1259
1395
  node: item.node,
1260
1396
  depth: item.depth,
@@ -1269,7 +1405,15 @@ function TreeSelect({
1269
1405
  isExpanded: expandedIds.has(item.id),
1270
1406
  onToggle: toggleExpanded,
1271
1407
  onRowClick: handleRowClick,
1272
- rowRef: void 0,
1408
+ rowRef: (row) => {
1409
+ if (row) rowRefs.current.set(item.id, row);
1410
+ else rowRefs.current.delete(item.id);
1411
+ },
1412
+ rowAccessibility: rowAccessibility(item),
1413
+ labelContext: rowContext(item),
1414
+ accessibleLabel: getTreeItemLabel(item.node, getItemLabel),
1415
+ rowClassName: typeof rowClassName === "function" ? rowClassName(rowContext(item)) : rowClassName,
1416
+ labelClassName: typeof labelClassName === "function" ? labelClassName(rowContext(item)) : labelClassName,
1273
1417
  isDraggable: false,
1274
1418
  isDragging: false,
1275
1419
  isSelected: selectedIds.has(item.id),
@@ -1298,7 +1442,10 @@ function TreeSelect({
1298
1442
  item.id
1299
1443
  );
1300
1444
  }) }) }) });
1301
- }
1445
+ });
1446
+
1447
+ // src/index.ts
1448
+ var TreeSelect3 = TreeSelect;
1302
1449
  export {
1303
- TreeSelect
1450
+ TreeSelect3 as TreeSelect
1304
1451
  };
@@ -0,0 +1,52 @@
1
+ # DS15 interaction inventory
2
+
3
+ Inspected 29 October 2026 against Payload's installed public TreeSelect 0.2.5
4
+ and the local Edux source. Payload files were read only.
5
+
6
+ | Host | Interaction and local ownership | Replacement boundary |
7
+ | --- | --- | --- |
8
+ | Manuscripts `ManuScriptNavigationTree` | Delegated row click resolves domain metadata and aborts previous asynchronous editor navigation; filtered trees disable reordering. `onDataChange` consumes exact drag metadata. | `onNodeActivate` ID resolves existing metadata. Keep editor readiness, abort controllers and reorder transactions local. |
9
+ | Curricula `CurriculaBuilderPanel` | Delegated click opens node editor. Labels are not inline editable at any configured level; add/delete and multi-drag remain active. | `onNodeActivate`; keep mutations, expansion and selection of domain editor local. |
10
+ | Book Setup `PageBuilderBookSetupNavigationPanel` | Filtered content block tree delegates IDs to section/view changes. Rich labels include icons. | `onNodeActivate`, `currentId`, accessible name override and public row/label classes. Keep filtering and view/section routing local. |
11
+ | Rights `PermissionsWorkspaceSourceNavigation` | Rich labels contain focusable role-button and status tooltip; selected label ref locates private ancestor for nearest scroll. | Row activation/current state, `getItemLabel`, imperative `scrollToItem`; remove nested navigation button. Keep source permissions/status/search local. |
12
+ | Rights `PermissionsWorkspaceAssetNavigation` | Rich label role-button duplicates keyboard navigation; selected label ref scrapes ancestor for scroll. | Row activation/current state, `getItemLabel`, `scrollToItem`; remove nested navigation button. Keep asset mapping/search local. |
13
+
14
+ Also inspected `pages/PageBuilderBookSetupEdit`: its debug summary and section
15
+ handler scrape row IDs from DOM events, but the handler already accepts a string
16
+ ID. Passing the activation ID at the navigation-panel boundary removes the event
17
+ lookup without moving debug logging or section routing into Edux. No host source
18
+ is modified by this package task.
19
+
20
+ ## Compatibility baseline
21
+
22
+ The registry 0.2.5 archive matches the source interaction baseline. Existing
23
+ tracked source token work (`be4a9b2`) replaces registry raw state colours/drop
24
+ indicator/shadow classes with semantic equivalents; DS15 retains that source
25
+ baseline and introduces no tokens or further geometry changes. Payload's
26
+ installed 0.2.5 runtime is locally patched and has
27
+ roving row focus, tree/treeitem roles, level/position/set-size attributes,
28
+ `aria-selected` for multi-drag, labelled edit/toggle/move controls and keyboard
29
+ arrow/Home/End/F2 handling. Source and registry lack these. The extension adds supported equivalents while
30
+ preserving registry geometry, data operations and drag metadata.
31
+
32
+ The installed runtime also uses `--theme-radius` and `--theme-hover-surface`
33
+ fallbacks and `data-selected`; these are local patch hooks, not registry APIs.
34
+ The extension retains source semantic token styling and registry geometry; it
35
+ does not adopt the local theme variable overrides. Public current/selection
36
+ context and row/label classes replace host styling selectors.
37
+
38
+ ## Interaction decisions
39
+
40
+ - Current item is controlled by `currentId`. Activation never changes it implicitly;
41
+ hosts can wait for navigation success. `aria-current="page"` expresses current
42
+ location; `aria-selected` continues to mean drag selection.
43
+ - Plain row pointer clicks and row Enter/Space activate once. Ctrl/Meta/Shift/Alt
44
+ suppress activation; Ctrl/Meta/Shift retain existing multi-selection behaviour.
45
+ - `defaultPrevented`, editable inputs, row controls and interactive custom label
46
+ descendants suppress activation. Pointer selection from a drag handle never
47
+ invokes navigation. Keyboard events from descendants stay with that control.
48
+ - Visible row access is explicit: get element, focus and scroll. Collapsed,
49
+ filtered and removed targets return null/false. No method implicitly expands
50
+ or changes current item, selection, routing or domain data.
51
+ - Rich labels need an accessible name: use `getItemLabel(node)`; fallback is a
52
+ string/number label, otherwise the stable normalised ID.