@headless-tree/core 1.2.0 → 1.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.
Files changed (183) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/index.d.mts +577 -0
  3. package/dist/index.d.ts +577 -0
  4. package/dist/index.js +2321 -0
  5. package/dist/index.mjs +2276 -0
  6. package/package.json +18 -10
  7. package/src/core/create-tree.ts +26 -2
  8. package/src/features/async-data-loader/feature.ts +9 -4
  9. package/src/features/async-data-loader/types.ts +2 -0
  10. package/src/features/checkboxes/checkboxes.spec.ts +149 -0
  11. package/src/features/checkboxes/feature.ts +134 -0
  12. package/src/features/checkboxes/types.ts +29 -0
  13. package/src/features/drag-and-drop/drag-and-drop.spec.ts +11 -2
  14. package/src/features/drag-and-drop/feature.ts +88 -17
  15. package/src/features/drag-and-drop/types.ts +22 -0
  16. package/src/features/keyboard-drag-and-drop/feature.ts +8 -1
  17. package/src/features/keyboard-drag-and-drop/keyboard-drag-and-drop.spec.ts +34 -3
  18. package/src/features/sync-data-loader/feature.ts +5 -1
  19. package/src/features/tree/feature.ts +9 -3
  20. package/src/features/tree/tree.spec.ts +14 -4
  21. package/src/features/tree/types.ts +3 -2
  22. package/src/index.ts +2 -0
  23. package/src/test-utils/test-tree-do.ts +2 -0
  24. package/src/test-utils/test-tree.ts +1 -0
  25. package/src/types/core.ts +2 -0
  26. package/tsconfig.json +1 -4
  27. package/vitest.config.ts +3 -1
  28. package/lib/cjs/core/build-proxified-instance.d.ts +0 -2
  29. package/lib/cjs/core/build-proxified-instance.js +0 -58
  30. package/lib/cjs/core/build-static-instance.d.ts +0 -2
  31. package/lib/cjs/core/build-static-instance.js +0 -26
  32. package/lib/cjs/core/create-tree.d.ts +0 -2
  33. package/lib/cjs/core/create-tree.js +0 -182
  34. package/lib/cjs/features/async-data-loader/feature.d.ts +0 -2
  35. package/lib/cjs/features/async-data-loader/feature.js +0 -135
  36. package/lib/cjs/features/async-data-loader/types.d.ts +0 -47
  37. package/lib/cjs/features/async-data-loader/types.js +0 -2
  38. package/lib/cjs/features/drag-and-drop/feature.d.ts +0 -2
  39. package/lib/cjs/features/drag-and-drop/feature.js +0 -179
  40. package/lib/cjs/features/drag-and-drop/types.d.ts +0 -66
  41. package/lib/cjs/features/drag-and-drop/types.js +0 -9
  42. package/lib/cjs/features/drag-and-drop/utils.d.ts +0 -27
  43. package/lib/cjs/features/drag-and-drop/utils.js +0 -182
  44. package/lib/cjs/features/expand-all/feature.d.ts +0 -2
  45. package/lib/cjs/features/expand-all/feature.js +0 -70
  46. package/lib/cjs/features/expand-all/types.d.ts +0 -19
  47. package/lib/cjs/features/expand-all/types.js +0 -2
  48. package/lib/cjs/features/hotkeys-core/feature.d.ts +0 -2
  49. package/lib/cjs/features/hotkeys-core/feature.js +0 -107
  50. package/lib/cjs/features/hotkeys-core/types.d.ts +0 -27
  51. package/lib/cjs/features/hotkeys-core/types.js +0 -2
  52. package/lib/cjs/features/keyboard-drag-and-drop/feature.d.ts +0 -2
  53. package/lib/cjs/features/keyboard-drag-and-drop/feature.js +0 -206
  54. package/lib/cjs/features/keyboard-drag-and-drop/types.d.ts +0 -27
  55. package/lib/cjs/features/keyboard-drag-and-drop/types.js +0 -11
  56. package/lib/cjs/features/main/types.d.ts +0 -45
  57. package/lib/cjs/features/main/types.js +0 -2
  58. package/lib/cjs/features/prop-memoization/feature.d.ts +0 -2
  59. package/lib/cjs/features/prop-memoization/feature.js +0 -70
  60. package/lib/cjs/features/prop-memoization/types.d.ts +0 -15
  61. package/lib/cjs/features/prop-memoization/types.js +0 -2
  62. package/lib/cjs/features/renaming/feature.d.ts +0 -2
  63. package/lib/cjs/features/renaming/feature.js +0 -86
  64. package/lib/cjs/features/renaming/types.d.ts +0 -27
  65. package/lib/cjs/features/renaming/types.js +0 -2
  66. package/lib/cjs/features/search/feature.d.ts +0 -2
  67. package/lib/cjs/features/search/feature.js +0 -119
  68. package/lib/cjs/features/search/types.d.ts +0 -32
  69. package/lib/cjs/features/search/types.js +0 -2
  70. package/lib/cjs/features/selection/feature.d.ts +0 -2
  71. package/lib/cjs/features/selection/feature.js +0 -132
  72. package/lib/cjs/features/selection/types.d.ts +0 -21
  73. package/lib/cjs/features/selection/types.js +0 -2
  74. package/lib/cjs/features/sync-data-loader/feature.d.ts +0 -2
  75. package/lib/cjs/features/sync-data-loader/feature.js +0 -49
  76. package/lib/cjs/features/sync-data-loader/types.d.ts +0 -28
  77. package/lib/cjs/features/sync-data-loader/types.js +0 -2
  78. package/lib/cjs/features/tree/feature.d.ts +0 -2
  79. package/lib/cjs/features/tree/feature.js +0 -240
  80. package/lib/cjs/features/tree/types.d.ts +0 -62
  81. package/lib/cjs/features/tree/types.js +0 -2
  82. package/lib/cjs/index.d.ts +0 -31
  83. package/lib/cjs/index.js +0 -49
  84. package/lib/cjs/mddocs-entry.d.ts +0 -121
  85. package/lib/cjs/mddocs-entry.js +0 -17
  86. package/lib/cjs/test-utils/test-tree-do.d.ts +0 -23
  87. package/lib/cjs/test-utils/test-tree-do.js +0 -112
  88. package/lib/cjs/test-utils/test-tree-expect.d.ts +0 -17
  89. package/lib/cjs/test-utils/test-tree-expect.js +0 -66
  90. package/lib/cjs/test-utils/test-tree.d.ts +0 -48
  91. package/lib/cjs/test-utils/test-tree.js +0 -207
  92. package/lib/cjs/types/core.d.ts +0 -83
  93. package/lib/cjs/types/core.js +0 -2
  94. package/lib/cjs/types/deep-merge.d.ts +0 -13
  95. package/lib/cjs/types/deep-merge.js +0 -2
  96. package/lib/cjs/utilities/create-on-drop-handler.d.ts +0 -3
  97. package/lib/cjs/utilities/create-on-drop-handler.js +0 -20
  98. package/lib/cjs/utilities/errors.d.ts +0 -2
  99. package/lib/cjs/utilities/errors.js +0 -9
  100. package/lib/cjs/utilities/insert-items-at-target.d.ts +0 -3
  101. package/lib/cjs/utilities/insert-items-at-target.js +0 -40
  102. package/lib/cjs/utilities/remove-items-from-parents.d.ts +0 -2
  103. package/lib/cjs/utilities/remove-items-from-parents.js +0 -32
  104. package/lib/cjs/utils.d.ts +0 -6
  105. package/lib/cjs/utils.js +0 -53
  106. package/lib/esm/core/build-proxified-instance.d.ts +0 -2
  107. package/lib/esm/core/build-proxified-instance.js +0 -54
  108. package/lib/esm/core/build-static-instance.d.ts +0 -2
  109. package/lib/esm/core/build-static-instance.js +0 -22
  110. package/lib/esm/core/create-tree.d.ts +0 -2
  111. package/lib/esm/core/create-tree.js +0 -178
  112. package/lib/esm/features/async-data-loader/feature.d.ts +0 -2
  113. package/lib/esm/features/async-data-loader/feature.js +0 -132
  114. package/lib/esm/features/async-data-loader/types.d.ts +0 -47
  115. package/lib/esm/features/async-data-loader/types.js +0 -1
  116. package/lib/esm/features/drag-and-drop/feature.d.ts +0 -2
  117. package/lib/esm/features/drag-and-drop/feature.js +0 -176
  118. package/lib/esm/features/drag-and-drop/types.d.ts +0 -66
  119. package/lib/esm/features/drag-and-drop/types.js +0 -6
  120. package/lib/esm/features/drag-and-drop/utils.d.ts +0 -27
  121. package/lib/esm/features/drag-and-drop/utils.js +0 -172
  122. package/lib/esm/features/expand-all/feature.d.ts +0 -2
  123. package/lib/esm/features/expand-all/feature.js +0 -67
  124. package/lib/esm/features/expand-all/types.d.ts +0 -19
  125. package/lib/esm/features/expand-all/types.js +0 -1
  126. package/lib/esm/features/hotkeys-core/feature.d.ts +0 -2
  127. package/lib/esm/features/hotkeys-core/feature.js +0 -104
  128. package/lib/esm/features/hotkeys-core/types.d.ts +0 -27
  129. package/lib/esm/features/hotkeys-core/types.js +0 -1
  130. package/lib/esm/features/keyboard-drag-and-drop/feature.d.ts +0 -2
  131. package/lib/esm/features/keyboard-drag-and-drop/feature.js +0 -203
  132. package/lib/esm/features/keyboard-drag-and-drop/types.d.ts +0 -27
  133. package/lib/esm/features/keyboard-drag-and-drop/types.js +0 -8
  134. package/lib/esm/features/main/types.d.ts +0 -45
  135. package/lib/esm/features/main/types.js +0 -1
  136. package/lib/esm/features/prop-memoization/feature.d.ts +0 -2
  137. package/lib/esm/features/prop-memoization/feature.js +0 -67
  138. package/lib/esm/features/prop-memoization/types.d.ts +0 -15
  139. package/lib/esm/features/prop-memoization/types.js +0 -1
  140. package/lib/esm/features/renaming/feature.d.ts +0 -2
  141. package/lib/esm/features/renaming/feature.js +0 -83
  142. package/lib/esm/features/renaming/types.d.ts +0 -27
  143. package/lib/esm/features/renaming/types.js +0 -1
  144. package/lib/esm/features/search/feature.d.ts +0 -2
  145. package/lib/esm/features/search/feature.js +0 -116
  146. package/lib/esm/features/search/types.d.ts +0 -32
  147. package/lib/esm/features/search/types.js +0 -1
  148. package/lib/esm/features/selection/feature.d.ts +0 -2
  149. package/lib/esm/features/selection/feature.js +0 -129
  150. package/lib/esm/features/selection/types.d.ts +0 -21
  151. package/lib/esm/features/selection/types.js +0 -1
  152. package/lib/esm/features/sync-data-loader/feature.d.ts +0 -2
  153. package/lib/esm/features/sync-data-loader/feature.js +0 -46
  154. package/lib/esm/features/sync-data-loader/types.d.ts +0 -28
  155. package/lib/esm/features/sync-data-loader/types.js +0 -1
  156. package/lib/esm/features/tree/feature.d.ts +0 -2
  157. package/lib/esm/features/tree/feature.js +0 -237
  158. package/lib/esm/features/tree/types.d.ts +0 -62
  159. package/lib/esm/features/tree/types.js +0 -1
  160. package/lib/esm/index.d.ts +0 -31
  161. package/lib/esm/index.js +0 -30
  162. package/lib/esm/mddocs-entry.d.ts +0 -121
  163. package/lib/esm/mddocs-entry.js +0 -1
  164. package/lib/esm/test-utils/test-tree-do.d.ts +0 -23
  165. package/lib/esm/test-utils/test-tree-do.js +0 -108
  166. package/lib/esm/test-utils/test-tree-expect.d.ts +0 -17
  167. package/lib/esm/test-utils/test-tree-expect.js +0 -62
  168. package/lib/esm/test-utils/test-tree.d.ts +0 -48
  169. package/lib/esm/test-utils/test-tree.js +0 -203
  170. package/lib/esm/types/core.d.ts +0 -83
  171. package/lib/esm/types/core.js +0 -1
  172. package/lib/esm/types/deep-merge.d.ts +0 -13
  173. package/lib/esm/types/deep-merge.js +0 -1
  174. package/lib/esm/utilities/create-on-drop-handler.d.ts +0 -3
  175. package/lib/esm/utilities/create-on-drop-handler.js +0 -16
  176. package/lib/esm/utilities/errors.d.ts +0 -2
  177. package/lib/esm/utilities/errors.js +0 -4
  178. package/lib/esm/utilities/insert-items-at-target.d.ts +0 -3
  179. package/lib/esm/utilities/insert-items-at-target.js +0 -36
  180. package/lib/esm/utilities/remove-items-from-parents.d.ts +0 -2
  181. package/lib/esm/utilities/remove-items-from-parents.js +0 -28
  182. package/lib/esm/utils.d.ts +0 -6
  183. package/lib/esm/utils.js +0 -46
@@ -1,5 +1,5 @@
1
1
  import { FeatureImplementation } from "../../types/core";
2
- import { DndDataRef, DragLineData } from "./types";
2
+ import { DndDataRef, DragLineData, DragTarget } from "./types";
3
3
  import {
4
4
  canDrop,
5
5
  getDragCode,
@@ -8,13 +8,18 @@ import {
8
8
  } from "./utils";
9
9
  import { makeStateUpdater } from "../../utils";
10
10
 
11
+ const defaultCanDropForeignDragObject = () => false;
11
12
  export const dragAndDropFeature: FeatureImplementation = {
12
13
  key: "drag-and-drop",
13
14
  deps: ["selection"],
14
15
 
15
16
  getDefaultConfig: (defaultConfig, tree) => ({
16
17
  canDrop: (_, target) => target.item.isFolder(),
17
- canDropForeignDragObject: () => false,
18
+ canDropForeignDragObject: defaultCanDropForeignDragObject,
19
+ canDragForeignDragObjectOver:
20
+ defaultConfig.canDropForeignDragObject !== defaultCanDropForeignDragObject
21
+ ? (dataTransfer) => dataTransfer.effectAllowed !== "none"
22
+ : () => false,
18
23
  setDndState: makeStateUpdater("dnd", tree),
19
24
  canReorder: true,
20
25
  ...defaultConfig,
@@ -24,6 +29,19 @@ export const dragAndDropFeature: FeatureImplementation = {
24
29
  dnd: "setDndState",
25
30
  },
26
31
 
32
+ onTreeMount: (tree) => {
33
+ const listener = () => {
34
+ tree.applySubStateUpdate("dnd", null);
35
+ };
36
+ tree.getDataRef<DndDataRef>().current.windowDragEndListener = listener;
37
+ window.addEventListener("dragend", listener);
38
+ },
39
+ onTreeUnmount: (tree) => {
40
+ const { windowDragEndListener } = tree.getDataRef<DndDataRef>().current;
41
+ if (!windowDragEndListener) return;
42
+ window.removeEventListener("dragend", windowDragEndListener);
43
+ },
44
+
27
45
  treeInstance: {
28
46
  getDragTarget: ({ tree }) => {
29
47
  return tree.getState().dnd?.dragTarget ?? null;
@@ -83,10 +101,37 @@ export const dragAndDropFeature: FeatureImplementation = {
83
101
  : { display: "none" };
84
102
  },
85
103
 
86
- getContainerProps: ({ prev }, treeLabel) => {
104
+ getContainerProps: ({ prev, tree }, treeLabel) => {
87
105
  const prevProps = prev?.(treeLabel);
88
106
  return {
89
107
  ...prevProps,
108
+
109
+ onDragOver: (e: DragEvent) => {
110
+ e.preventDefault();
111
+ },
112
+
113
+ onDrop: async (e: DragEvent) => {
114
+ // TODO merge implementation with itemInstance.onDrop
115
+ const dataRef = tree.getDataRef<DndDataRef>();
116
+ const target: DragTarget<any> = { item: tree.getRootItem() };
117
+
118
+ if (!canDrop(e.dataTransfer, target, tree)) {
119
+ return;
120
+ }
121
+
122
+ e.preventDefault();
123
+ const config = tree.getConfig();
124
+ const draggedItems = tree.getState().dnd?.draggedItems;
125
+
126
+ dataRef.current.lastDragCode = undefined;
127
+
128
+ if (draggedItems) {
129
+ await config.onDrop?.(draggedItems, target);
130
+ } else if (e.dataTransfer) {
131
+ await config.onDropForeignDragObject?.(e.dataTransfer, target);
132
+ }
133
+ },
134
+
90
135
  style: {
91
136
  ...prevProps?.style,
92
137
  position: "relative",
@@ -101,6 +146,8 @@ export const dragAndDropFeature: FeatureImplementation = {
101
146
 
102
147
  draggable: true,
103
148
 
149
+ onDragEnter: (e: DragEvent) => e.preventDefault(),
150
+
104
151
  onDragStart: (e: DragEvent) => {
105
152
  const selectedItems = tree.getSelectedItems();
106
153
  const items = selectedItems.includes(item) ? selectedItems : [item];
@@ -115,9 +162,18 @@ export const dragAndDropFeature: FeatureImplementation = {
115
162
  return;
116
163
  }
117
164
 
118
- if (config.createForeignDragObject) {
119
- const { format, data } = config.createForeignDragObject(items);
120
- e.dataTransfer?.setData(format, data);
165
+ if (config.setDragImage) {
166
+ const { imgElement, xOffset, yOffset } = config.setDragImage(items);
167
+ e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
168
+ }
169
+
170
+ if (config.createForeignDragObject && e.dataTransfer) {
171
+ const { format, data, dropEffect, effectAllowed } =
172
+ config.createForeignDragObject(items);
173
+ e.dataTransfer.setData(format, data);
174
+
175
+ if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
176
+ if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
121
177
  }
122
178
 
123
179
  tree.applySubStateUpdate("dnd", {
@@ -127,6 +183,7 @@ export const dragAndDropFeature: FeatureImplementation = {
127
183
  },
128
184
 
129
185
  onDragOver: (e: DragEvent) => {
186
+ e.stopPropagation(); // don't bubble up to container dragover
130
187
  const dataRef = tree.getDataRef<DndDataRef>();
131
188
  const nextDragCode = getDragCode(e, item, tree);
132
189
  if (nextDragCode === dataRef.current.lastDragCode) {
@@ -136,6 +193,7 @@ export const dragAndDropFeature: FeatureImplementation = {
136
193
  return;
137
194
  }
138
195
  dataRef.current.lastDragCode = nextDragCode;
196
+ dataRef.current.lastDragEnter = Date.now();
139
197
 
140
198
  const target = getDragTarget(e, item, tree);
141
199
 
@@ -144,7 +202,7 @@ export const dragAndDropFeature: FeatureImplementation = {
144
202
  (!e.dataTransfer ||
145
203
  !tree
146
204
  .getConfig()
147
- .canDropForeignDragObject?.(e.dataTransfer, target))
205
+ .canDragForeignDragObjectOver?.(e.dataTransfer, target))
148
206
  ) {
149
207
  dataRef.current.lastAllowDrop = false;
150
208
  return;
@@ -165,27 +223,41 @@ export const dragAndDropFeature: FeatureImplementation = {
165
223
  },
166
224
 
167
225
  onDragLeave: () => {
168
- const dataRef = tree.getDataRef<DndDataRef>();
169
- dataRef.current.lastDragCode = "no-drag";
170
- tree.applySubStateUpdate("dnd", (state) => ({
171
- ...state,
172
- draggingOverItem: undefined,
173
- dragTarget: undefined,
174
- }));
226
+ setTimeout(() => {
227
+ const dataRef = tree.getDataRef<DndDataRef>();
228
+ if ((dataRef.current.lastDragEnter ?? 0) + 100 >= Date.now()) return;
229
+ dataRef.current.lastDragCode = "no-drag";
230
+ tree.applySubStateUpdate("dnd", (state) => ({
231
+ ...state,
232
+ draggingOverItem: undefined,
233
+ dragTarget: undefined,
234
+ }));
235
+ }, 100);
175
236
  },
176
237
 
177
238
  onDragEnd: (e: DragEvent) => {
239
+ const { onCompleteForeignDrop, canDragForeignDragObjectOver } =
240
+ tree.getConfig();
178
241
  const draggedItems = tree.getState().dnd?.draggedItems;
179
- tree.applySubStateUpdate("dnd", null);
180
242
 
181
243
  if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
182
244
  return;
183
245
  }
184
246
 
185
- tree.getConfig().onCompleteForeignDrop?.(draggedItems);
247
+ const target = getDragTarget(e, item, tree);
248
+ if (
249
+ canDragForeignDragObjectOver &&
250
+ e.dataTransfer &&
251
+ !canDragForeignDragObjectOver(e.dataTransfer, target)
252
+ ) {
253
+ return;
254
+ }
255
+
256
+ onCompleteForeignDrop?.(draggedItems);
186
257
  },
187
258
 
188
259
  onDrop: async (e: DragEvent) => {
260
+ e.stopPropagation();
189
261
  const dataRef = tree.getDataRef<DndDataRef>();
190
262
  const target = getDragTarget(e, item, tree);
191
263
 
@@ -198,7 +270,6 @@ export const dragAndDropFeature: FeatureImplementation = {
198
270
  const draggedItems = tree.getState().dnd?.draggedItems;
199
271
 
200
272
  dataRef.current.lastDragCode = undefined;
201
- tree.applySubStateUpdate("dnd", null);
202
273
 
203
274
  if (draggedItems) {
204
275
  await config.onDrop?.(draggedItems, target);
@@ -3,6 +3,8 @@ import { ItemInstance, SetStateFn } from "../../types/core";
3
3
  export interface DndDataRef {
4
4
  lastDragCode?: string;
5
5
  lastAllowDrop?: boolean;
6
+ lastDragEnter?: number;
7
+ windowDragEndListener?: () => void;
6
8
  }
7
9
 
8
10
  export interface DndState<T> {
@@ -57,11 +59,31 @@ export type DragAndDropFeatureDef<T> = {
57
59
  createForeignDragObject?: (items: ItemInstance<T>[]) => {
58
60
  format: string;
59
61
  data: any;
62
+ dropEffect?: DataTransfer["dropEffect"];
63
+ effectAllowed?: DataTransfer["effectAllowed"];
60
64
  };
65
+ setDragImage?: (items: ItemInstance<T>[]) => {
66
+ imgElement: Element;
67
+ xOffset?: number;
68
+ yOffset?: number;
69
+ };
70
+
71
+ /** Checks if a foreign drag object can be dropped on a target, validating that an actual drop can commence based on
72
+ * the data in the DataTransfer object. */
61
73
  canDropForeignDragObject?: (
62
74
  dataTransfer: DataTransfer,
63
75
  target: DragTarget<T>,
64
76
  ) => boolean;
77
+
78
+ /** Checks if a droppable visualization should be displayed when dragging a foreign object over a target. Since this
79
+ * is executed on a dragover event, `dataTransfer.getData()` is not available, so `dataTransfer.effectAllowed` or
80
+ * `dataTransfer.types` should be used instead. Before actually completing the drag, @{link canDropForeignDragObject}
81
+ * will be called by HT before applying the drop. */
82
+ canDragForeignDragObjectOver?: (
83
+ dataTransfer: DataTransfer,
84
+ target: DragTarget<T>,
85
+ ) => boolean;
86
+
65
87
  onDrop?: (
66
88
  items: ItemInstance<T>[],
67
89
  target: DragTarget<T>,
@@ -191,7 +191,14 @@ export const keyboardDragAndDropFeature: FeatureImplementation = {
191
191
  preventDefault: true,
192
192
  isEnabled: (tree) => !tree.getState().dnd,
193
193
  handler: (_, tree) => {
194
- tree.startKeyboardDrag(tree.getSelectedItems());
194
+ const selectedItems = tree.getSelectedItems();
195
+ const focusedItem = tree.getFocusedItem();
196
+
197
+ tree.startKeyboardDrag(
198
+ selectedItems.includes(focusedItem)
199
+ ? selectedItems
200
+ : selectedItems.concat(focusedItem),
201
+ );
195
202
  },
196
203
  },
197
204
  dragUp: {
@@ -49,6 +49,37 @@ describe("core-feature/keyboard-drag-and-drop", () => {
49
49
  tree.expect.substate("assistiveDndState", AssistiveDndState.Started);
50
50
  });
51
51
 
52
+ it("starts dragging only focused item", () => {
53
+ tree.item("x3").setFocused();
54
+ tree.do.hotkey("startDrag");
55
+ tree.expect.substate("dnd", {
56
+ draggedItems: [tree.item("x3")],
57
+ dragTarget: {
58
+ childIndex: 3,
59
+ dragLineIndex: 19,
60
+ dragLineLevel: 0,
61
+ insertionIndex: 2,
62
+ item: tree.item("x"),
63
+ },
64
+ });
65
+ });
66
+
67
+ it("starts dragging both selected and focused item", () => {
68
+ tree.do.selectMultiple("x111", "x112");
69
+ tree.item("x3").setFocused();
70
+ tree.do.hotkey("startDrag");
71
+ tree.expect.substate("dnd", {
72
+ draggedItems: [tree.item("x111"), tree.item("x112"), tree.item("x3")],
73
+ dragTarget: {
74
+ childIndex: 3,
75
+ dragLineIndex: 19,
76
+ dragLineLevel: 0,
77
+ insertionIndex: 2,
78
+ item: tree.item("x"),
79
+ },
80
+ });
81
+ });
82
+
52
83
  it("moves down 1", () => {
53
84
  tree.do.selectMultiple("x111", "x112");
54
85
  tree.do.hotkey("startDrag");
@@ -355,13 +386,13 @@ describe("core-feature/keyboard-drag-and-drop", () => {
355
386
 
356
387
  it("doesnt go below end of tree", () => {
357
388
  const lastState = {
358
- draggedItems: [tree.item("x111")],
389
+ draggedItems: [tree.item("x111"), tree.item("x3")],
359
390
  dragTarget: {
360
391
  item: tree.item("x"),
361
392
  childIndex: 4,
362
393
  dragLineIndex: 20,
363
394
  dragLineLevel: 0,
364
- insertionIndex: 4,
395
+ insertionIndex: 3,
365
396
  },
366
397
  };
367
398
 
@@ -378,7 +409,7 @@ describe("core-feature/keyboard-drag-and-drop", () => {
378
409
 
379
410
  it("doesnt go above top of tree", () => {
380
411
  const firstState = {
381
- draggedItems: [tree.item("x111")],
412
+ draggedItems: [tree.item("x111"), tree.item("x1")],
382
413
  dragTarget: {
383
414
  item: tree.item("x"),
384
415
  childIndex: 0,
@@ -2,9 +2,13 @@ import { FeatureImplementation } from "../../types/core";
2
2
  import { makeStateUpdater } from "../../utils";
3
3
  import { throwError } from "../../utilities/errors";
4
4
 
5
+ const undefErrorMessage = "sync dataLoader returned undefined";
5
6
  const promiseErrorMessage = "sync dataLoader returned promise";
6
7
  const unpromise = <T>(data: T | Promise<T>): T => {
7
- if (!data || (typeof data === "object" && "then" in data)) {
8
+ if (!data) {
9
+ throw throwError(undefErrorMessage);
10
+ }
11
+ if (typeof data === "object" && "then" in data) {
8
12
  throw throwError(promiseErrorMessage);
9
13
  }
10
14
  return data;
@@ -76,12 +76,18 @@ export const treeFeature: FeatureImplementation<any> = {
76
76
  },
77
77
 
78
78
  getFocusedItem: ({ tree }) => {
79
+ const focusedItemId = tree.getState().focusedItem;
79
80
  return (
80
- tree.getItemInstance(tree.getState().focusedItem ?? "") ??
81
+ (focusedItemId !== null ? tree.getItemInstance(focusedItemId) : null) ??
81
82
  tree.getItems()[0]
82
83
  );
83
84
  },
84
85
 
86
+ getRootItem: ({ tree }) => {
87
+ const { rootItemId } = tree.getConfig();
88
+ return tree.getItemInstance(rootItemId);
89
+ },
90
+
85
91
  focusNextItem: ({ tree }) => {
86
92
  const focused = tree.getFocusedItem().getItemMeta();
87
93
  if (!focused) return;
@@ -134,10 +140,10 @@ export const treeFeature: FeatureImplementation<any> = {
134
140
  ref: item.registerElement,
135
141
  role: "treeitem",
136
142
  "aria-setsize": itemMeta.setSize,
137
- "aria-posinset": itemMeta.posInSet,
143
+ "aria-posinset": itemMeta.posInSet + 1,
138
144
  "aria-selected": "false",
139
145
  "aria-label": item.getItemName(),
140
- "aria-level": itemMeta.level,
146
+ "aria-level": itemMeta.level + 1,
141
147
  tabIndex: item.isFocused() ? 0 : -1,
142
148
  onClick: (e: MouseEvent) => {
143
149
  item.setFocused();
@@ -235,8 +235,8 @@ describe("core-feature/selections", () => {
235
235
  it("generates item props for random item", () => {
236
236
  expect(tree.instance.getItemInstance("x2").getProps()).toEqual({
237
237
  "aria-label": "x2",
238
- "aria-level": 0,
239
- "aria-posinset": 1,
238
+ "aria-level": 1,
239
+ "aria-posinset": 2,
240
240
  "aria-selected": "false",
241
241
  "aria-setsize": 4,
242
242
  onClick: expect.any(Function),
@@ -249,8 +249,8 @@ describe("core-feature/selections", () => {
249
249
  it("generates item props for focused", () => {
250
250
  expect(tree.instance.getItemInstance("x1").getProps()).toEqual({
251
251
  "aria-label": "x1",
252
- "aria-level": 0,
253
- "aria-posinset": 0,
252
+ "aria-level": 1,
253
+ "aria-posinset": 1,
254
254
  "aria-selected": "false",
255
255
  "aria-setsize": 4,
256
256
  onClick: expect.any(Function),
@@ -472,4 +472,14 @@ describe("core-feature/selections", () => {
472
472
  });
473
473
  });
474
474
  });
475
+
476
+ describe("empty rootItemId", () => {
477
+ factory.with({ rootItemId: "" }).forSuits((tree) => {
478
+ describe("focused item", () => {
479
+ it("returns correct initial focused item", () => {
480
+ expect(tree.instance.getFocusedItem().getId()).toBe("1");
481
+ });
482
+ });
483
+ });
484
+ });
475
485
  });
@@ -20,7 +20,7 @@ export type TreeFeatureDef<T> = {
20
20
  focusedItem: string | null;
21
21
  };
22
22
  config: {
23
- isItemFolder: (item: ItemInstance<T>) => boolean;
23
+ isItemFolder: (item: ItemInstance<T>) => boolean; // TODO:breaking use item data as payload
24
24
  getItemName: (item: ItemInstance<T>) => string;
25
25
 
26
26
  onPrimaryAction?: (item: ItemInstance<T>) => void;
@@ -33,7 +33,8 @@ export type TreeFeatureDef<T> = {
33
33
  /** @internal */
34
34
  getItemsMeta: () => ItemMeta[];
35
35
 
36
- getFocusedItem: () => ItemInstance<any>;
36
+ getFocusedItem: () => ItemInstance<T>;
37
+ getRootItem: () => ItemInstance<T>;
37
38
  focusNextItem: () => void;
38
39
  focusPreviousItem: () => void;
39
40
  updateDomFocus: () => void;
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export { MainFeatureDef, InstanceBuilder } from "./features/main/types";
6
6
  export * from "./features/drag-and-drop/types";
7
7
  export * from "./features/keyboard-drag-and-drop/types";
8
8
  export * from "./features/selection/types";
9
+ export * from "./features/checkboxes/types";
9
10
  export * from "./features/async-data-loader/types";
10
11
  export * from "./features/sync-data-loader/types";
11
12
  export * from "./features/hotkeys-core/types";
@@ -15,6 +16,7 @@ export * from "./features/expand-all/types";
15
16
  export * from "./features/prop-memoization/types";
16
17
 
17
18
  export * from "./features/selection/feature";
19
+ export * from "./features/checkboxes/feature";
18
20
  export * from "./features/hotkeys-core/feature";
19
21
  export * from "./features/async-data-loader/feature";
20
22
  export * from "./features/sync-data-loader/feature";
@@ -105,12 +105,14 @@ export class TestTreeDo<T> {
105
105
  dragEnd(itemId: string, event?: DragEvent) {
106
106
  const e = event ?? TestTree.dragEvent();
107
107
  this.itemProps(itemId).onDragEnd(e);
108
+ window.dispatchEvent(new CustomEvent("dragend"));
108
109
  return e;
109
110
  }
110
111
 
111
112
  async drop(itemId: string, event?: DragEvent) {
112
113
  const e = event ?? TestTree.dragEvent();
113
114
  await this.itemProps(itemId).onDrop(e);
115
+ window.dispatchEvent(new CustomEvent("dragend"));
114
116
  return e;
115
117
  }
116
118
 
@@ -137,6 +137,7 @@ export class TestTree<T = string> {
137
137
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
138
138
  this.instance;
139
139
  await this.resolveAsyncVisibleItems();
140
+ this.instance.registerElement({ getBoundingClientRect: () => null } as any);
140
141
  return this;
141
142
  }
142
143
 
package/src/types/core.ts CHANGED
@@ -13,6 +13,7 @@ import { RenamingFeatureDef } from "../features/renaming/types";
13
13
  import { ExpandAllFeatureDef } from "../features/expand-all/types";
14
14
  import { PropMemoizationFeatureDef } from "../features/prop-memoization/types";
15
15
  import { KeyboardDragAndDropFeatureDef } from "../features/keyboard-drag-and-drop/types";
16
+ import { CheckboxesFeatureDef } from "../features/checkboxes/types";
16
17
 
17
18
  export type Updater<T> = T | ((old: T) => T);
18
19
  export type SetStateFn<T> = (updaterOrValue: Updater<T>) => void;
@@ -53,6 +54,7 @@ export type RegisteredFeatures<T> =
53
54
  | MainFeatureDef<T>
54
55
  | TreeFeatureDef<T>
55
56
  | SelectionFeatureDef<T>
57
+ | CheckboxesFeatureDef<T>
56
58
  | DragAndDropFeatureDef<T>
57
59
  | KeyboardDragAndDropFeatureDef<T>
58
60
  | HotkeysCoreFeatureDef<T>
package/tsconfig.json CHANGED
@@ -1,8 +1,5 @@
1
1
  {
2
2
  "extends": "../../tsconfig.json",
3
3
  "include": ["./src/**/*"],
4
- "exclude": ["./src/**/*.spec.tsx", "./src/**/*.spec.ts", "./src/**/*.stories.tsx"],
5
- "compilerOptions": {
6
- "outDir": "lib/esm"
7
- }
4
+ "exclude": ["./src/**/*.spec.tsx", "./src/**/*.spec.ts", "./src/**/*.stories.tsx"]
8
5
  }
package/vitest.config.ts CHANGED
@@ -2,5 +2,7 @@
2
2
  import { defineConfig } from "vitest/config";
3
3
 
4
4
  export default defineConfig({
5
- test: {},
5
+ test: {
6
+ environment: "jsdom",
7
+ },
6
8
  });
@@ -1,2 +0,0 @@
1
- import { InstanceBuilder } from "../features/main/types";
2
- export declare const buildProxiedInstance: InstanceBuilder;
@@ -1,58 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildProxiedInstance = void 0;
4
- const errors_1 = require("../utilities/errors");
5
- const noop = () => { };
6
- const findPrevInstanceMethod = (features, instanceType, methodKey, featureSearchIndex) => {
7
- var _a;
8
- for (let i = featureSearchIndex; i >= 0; i--) {
9
- const feature = features[i];
10
- const itemInstanceMethod = (_a = feature[instanceType]) === null || _a === void 0 ? void 0 : _a[methodKey];
11
- if (itemInstanceMethod) {
12
- return i;
13
- }
14
- }
15
- return null;
16
- };
17
- const invokeInstanceMethod = (features, instanceType, opts, methodKey, featureIndex, args) => {
18
- var _a;
19
- const prevIndex = findPrevInstanceMethod(features, instanceType, methodKey, featureIndex - 1);
20
- const itemInstanceMethod = (_a = features[featureIndex][instanceType]) === null || _a === void 0 ? void 0 : _a[methodKey];
21
- return itemInstanceMethod(Object.assign(Object.assign({}, opts), { prev: prevIndex !== null
22
- ? (...newArgs) => invokeInstanceMethod(features, instanceType, opts, methodKey, prevIndex, newArgs)
23
- : null }), ...args);
24
- };
25
- const buildProxiedInstance = (features, instanceType, buildOpts) => {
26
- // demo with prototypes: https://jsfiddle.net/bgenc58r/
27
- const opts = {};
28
- const item = new Proxy({}, {
29
- has(target, key) {
30
- if (typeof key === "symbol") {
31
- return false;
32
- }
33
- if (key === "toJSON") {
34
- return false;
35
- }
36
- const hasInstanceMethod = findPrevInstanceMethod(features, instanceType, key, features.length - 1);
37
- return Boolean(hasInstanceMethod);
38
- },
39
- get(target, key) {
40
- if (typeof key === "symbol") {
41
- return undefined;
42
- }
43
- if (key === "toJSON") {
44
- return {};
45
- }
46
- return (...args) => {
47
- const featureIndex = findPrevInstanceMethod(features, instanceType, key, features.length - 1);
48
- if (featureIndex === null) {
49
- throw (0, errors_1.throwError)(`feature missing for method ${key}`);
50
- }
51
- return invokeInstanceMethod(features, instanceType, opts, key, featureIndex, args);
52
- };
53
- },
54
- });
55
- Object.assign(opts, buildOpts(item));
56
- return [item, noop];
57
- };
58
- exports.buildProxiedInstance = buildProxiedInstance;
@@ -1,2 +0,0 @@
1
- import { InstanceBuilder } from "../features/main/types";
2
- export declare const buildStaticInstance: InstanceBuilder;
@@ -1,26 +0,0 @@
1
- "use strict";
2
- /* eslint-disable no-continue,no-labels,no-extra-label */
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.buildStaticInstance = void 0;
5
- const buildStaticInstance = (features, instanceType, buildOpts) => {
6
- const instance = {};
7
- const finalize = () => {
8
- const opts = buildOpts(instance);
9
- featureLoop: for (let i = 0; i < features.length; i++) {
10
- // Loop goes in forward order, each features overwrite previous ones and wraps those in a prev() fn
11
- const definition = features[i][instanceType];
12
- if (!definition)
13
- continue featureLoop;
14
- methodLoop: for (const [key, method] of Object.entries(definition)) {
15
- if (!method)
16
- continue methodLoop;
17
- const prev = instance[key];
18
- instance[key] = (...args) => {
19
- return method(Object.assign(Object.assign({}, opts), { prev }), ...args);
20
- };
21
- }
22
- }
23
- };
24
- return [instance, finalize];
25
- };
26
- exports.buildStaticInstance = buildStaticInstance;
@@ -1,2 +0,0 @@
1
- import { TreeConfig, TreeInstance } from "../types/core";
2
- export declare const createTree: <T>(initialConfig: TreeConfig<T>) => TreeInstance<T>;