@headless-tree/core 0.0.14 → 1.0.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 (125) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/cjs/core/create-tree.js +13 -4
  3. package/lib/cjs/features/async-data-loader/feature.js +73 -48
  4. package/lib/cjs/features/async-data-loader/types.d.ts +17 -14
  5. package/lib/cjs/features/drag-and-drop/feature.js +98 -93
  6. package/lib/cjs/features/drag-and-drop/types.d.ts +17 -29
  7. package/lib/cjs/features/drag-and-drop/types.js +7 -7
  8. package/lib/cjs/features/drag-and-drop/utils.d.ts +25 -3
  9. package/lib/cjs/features/drag-and-drop/utils.js +51 -51
  10. package/lib/cjs/features/expand-all/feature.js +26 -3
  11. package/lib/cjs/features/expand-all/types.d.ts +3 -1
  12. package/lib/cjs/features/hotkeys-core/feature.js +7 -3
  13. package/lib/cjs/features/hotkeys-core/types.d.ts +4 -5
  14. package/lib/cjs/features/keyboard-drag-and-drop/feature.d.ts +2 -0
  15. package/lib/cjs/features/keyboard-drag-and-drop/feature.js +206 -0
  16. package/lib/cjs/features/keyboard-drag-and-drop/types.d.ts +27 -0
  17. package/lib/cjs/features/keyboard-drag-and-drop/types.js +11 -0
  18. package/lib/cjs/features/prop-memoization/feature.js +33 -11
  19. package/lib/cjs/features/prop-memoization/types.d.ts +8 -3
  20. package/lib/cjs/features/renaming/feature.js +1 -1
  21. package/lib/cjs/features/search/feature.js +2 -0
  22. package/lib/cjs/features/search/types.d.ts +2 -2
  23. package/lib/cjs/features/selection/feature.js +4 -4
  24. package/lib/cjs/features/selection/types.d.ts +1 -1
  25. package/lib/cjs/features/sync-data-loader/feature.js +31 -5
  26. package/lib/cjs/features/sync-data-loader/types.d.ts +5 -5
  27. package/lib/cjs/features/tree/feature.js +4 -9
  28. package/lib/cjs/features/tree/types.d.ts +7 -5
  29. package/lib/cjs/index.d.ts +2 -0
  30. package/lib/cjs/index.js +2 -0
  31. package/lib/cjs/mddocs-entry.d.ts +10 -0
  32. package/lib/cjs/test-utils/test-tree-do.d.ts +2 -2
  33. package/lib/cjs/test-utils/test-tree-do.js +19 -6
  34. package/lib/cjs/test-utils/test-tree-expect.d.ts +5 -3
  35. package/lib/cjs/test-utils/test-tree-expect.js +3 -0
  36. package/lib/cjs/test-utils/test-tree.d.ts +2 -1
  37. package/lib/cjs/test-utils/test-tree.js +24 -21
  38. package/lib/cjs/types/core.d.ts +2 -1
  39. package/lib/cjs/utilities/create-on-drop-handler.d.ts +2 -2
  40. package/lib/cjs/utilities/create-on-drop-handler.js +13 -4
  41. package/lib/cjs/utilities/insert-items-at-target.d.ts +2 -2
  42. package/lib/cjs/utilities/insert-items-at-target.js +21 -12
  43. package/lib/cjs/utilities/remove-items-from-parents.d.ts +1 -1
  44. package/lib/cjs/utilities/remove-items-from-parents.js +12 -3
  45. package/lib/esm/core/create-tree.js +13 -4
  46. package/lib/esm/features/async-data-loader/feature.js +73 -48
  47. package/lib/esm/features/async-data-loader/types.d.ts +17 -14
  48. package/lib/esm/features/drag-and-drop/feature.js +99 -94
  49. package/lib/esm/features/drag-and-drop/types.d.ts +17 -29
  50. package/lib/esm/features/drag-and-drop/types.js +6 -6
  51. package/lib/esm/features/drag-and-drop/utils.d.ts +25 -3
  52. package/lib/esm/features/drag-and-drop/utils.js +45 -49
  53. package/lib/esm/features/expand-all/feature.js +26 -3
  54. package/lib/esm/features/expand-all/types.d.ts +3 -1
  55. package/lib/esm/features/hotkeys-core/feature.js +7 -3
  56. package/lib/esm/features/hotkeys-core/types.d.ts +4 -5
  57. package/lib/esm/features/keyboard-drag-and-drop/feature.d.ts +2 -0
  58. package/lib/esm/features/keyboard-drag-and-drop/feature.js +203 -0
  59. package/lib/esm/features/keyboard-drag-and-drop/types.d.ts +27 -0
  60. package/lib/esm/features/keyboard-drag-and-drop/types.js +8 -0
  61. package/lib/esm/features/prop-memoization/feature.js +33 -11
  62. package/lib/esm/features/prop-memoization/types.d.ts +8 -3
  63. package/lib/esm/features/renaming/feature.js +1 -1
  64. package/lib/esm/features/search/feature.js +2 -0
  65. package/lib/esm/features/search/types.d.ts +2 -2
  66. package/lib/esm/features/selection/feature.js +4 -4
  67. package/lib/esm/features/selection/types.d.ts +1 -1
  68. package/lib/esm/features/sync-data-loader/feature.js +31 -5
  69. package/lib/esm/features/sync-data-loader/types.d.ts +5 -5
  70. package/lib/esm/features/tree/feature.js +4 -9
  71. package/lib/esm/features/tree/types.d.ts +7 -5
  72. package/lib/esm/index.d.ts +2 -0
  73. package/lib/esm/index.js +2 -0
  74. package/lib/esm/mddocs-entry.d.ts +10 -0
  75. package/lib/esm/test-utils/test-tree-do.d.ts +2 -2
  76. package/lib/esm/test-utils/test-tree-do.js +19 -6
  77. package/lib/esm/test-utils/test-tree-expect.d.ts +5 -3
  78. package/lib/esm/test-utils/test-tree-expect.js +3 -0
  79. package/lib/esm/test-utils/test-tree.d.ts +2 -1
  80. package/lib/esm/test-utils/test-tree.js +24 -21
  81. package/lib/esm/types/core.d.ts +2 -1
  82. package/lib/esm/utilities/create-on-drop-handler.d.ts +2 -2
  83. package/lib/esm/utilities/create-on-drop-handler.js +13 -4
  84. package/lib/esm/utilities/insert-items-at-target.d.ts +2 -2
  85. package/lib/esm/utilities/insert-items-at-target.js +21 -12
  86. package/lib/esm/utilities/remove-items-from-parents.d.ts +1 -1
  87. package/lib/esm/utilities/remove-items-from-parents.js +12 -3
  88. package/package.json +2 -2
  89. package/src/core/core.spec.ts +31 -0
  90. package/src/core/create-tree.ts +15 -5
  91. package/src/features/async-data-loader/async-data-loader.spec.ts +10 -6
  92. package/src/features/async-data-loader/feature.ts +76 -48
  93. package/src/features/async-data-loader/types.ts +18 -11
  94. package/src/features/drag-and-drop/drag-and-drop.spec.ts +75 -89
  95. package/src/features/drag-and-drop/feature.ts +26 -22
  96. package/src/features/drag-and-drop/types.ts +23 -35
  97. package/src/features/drag-and-drop/utils.ts +70 -57
  98. package/src/features/expand-all/feature.ts +29 -5
  99. package/src/features/expand-all/types.ts +3 -1
  100. package/src/features/hotkeys-core/feature.ts +4 -0
  101. package/src/features/hotkeys-core/types.ts +4 -13
  102. package/src/features/keyboard-drag-and-drop/feature.ts +255 -0
  103. package/src/features/keyboard-drag-and-drop/keyboard-drag-and-drop.spec.ts +402 -0
  104. package/src/features/keyboard-drag-and-drop/types.ts +30 -0
  105. package/src/features/prop-memoization/feature.ts +27 -8
  106. package/src/features/prop-memoization/prop-memoization.spec.ts +2 -2
  107. package/src/features/prop-memoization/types.ts +8 -3
  108. package/src/features/renaming/feature.ts +8 -2
  109. package/src/features/search/feature.ts +2 -0
  110. package/src/features/search/types.ts +2 -2
  111. package/src/features/selection/feature.ts +4 -4
  112. package/src/features/selection/types.ts +1 -1
  113. package/src/features/sync-data-loader/feature.ts +26 -7
  114. package/src/features/sync-data-loader/types.ts +5 -5
  115. package/src/features/tree/feature.ts +8 -13
  116. package/src/features/tree/types.ts +7 -5
  117. package/src/index.ts +2 -0
  118. package/src/mddocs-entry.ts +16 -0
  119. package/src/test-utils/test-tree-do.ts +3 -3
  120. package/src/test-utils/test-tree-expect.ts +7 -2
  121. package/src/test-utils/test-tree.ts +26 -22
  122. package/src/types/core.ts +2 -0
  123. package/src/utilities/create-on-drop-handler.ts +4 -4
  124. package/src/utilities/insert-items-at-target.ts +18 -14
  125. package/src/utilities/remove-items-from-parents.ts +6 -3
@@ -0,0 +1,402 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { TestTree } from "../../test-utils/test-tree";
3
+ import { selectionFeature } from "../selection/feature";
4
+ import { ItemInstance } from "../../types/core";
5
+ import { propMemoizationFeature } from "../prop-memoization/feature";
6
+ import { keyboardDragAndDropFeature } from "./feature";
7
+ import { dragAndDropFeature } from "../drag-and-drop/feature";
8
+ import { AssistiveDndState } from "./types";
9
+ import { isOrderedDragTarget } from "../drag-and-drop/utils";
10
+
11
+ const isItem = (item: unknown): item is ItemInstance<any> =>
12
+ !!item && typeof item === "object" && "getId" in item;
13
+ const areItemsEqual = (a: ItemInstance<any>, b: ItemInstance<any>) => {
14
+ if (!isItem(a) || !isItem(b)) return undefined;
15
+ if (a.getId() === b.getId()) return true;
16
+ console.warn("Items are not equal:", a.getId(), b.getId());
17
+ return false;
18
+ };
19
+ expect.addEqualityTesters([areItemsEqual]);
20
+
21
+ const factory = TestTree.default({
22
+ initialState: {
23
+ expandedItems: ["x1", "x11", "x2", "x21"],
24
+ },
25
+ onDrop: vi.fn(),
26
+ }).withFeatures(
27
+ selectionFeature,
28
+ dragAndDropFeature,
29
+ keyboardDragAndDropFeature,
30
+ propMemoizationFeature,
31
+ );
32
+
33
+ describe("core-feature/keyboard-drag-and-drop", () => {
34
+ factory.forSuits((tree) => {
35
+ describe("happy paths", () => {
36
+ it("correct initial state", () => {
37
+ tree.do.selectMultiple("x111", "x112");
38
+ tree.do.hotkey("startDrag");
39
+ tree.expect.substate("dnd", {
40
+ dragTarget: {
41
+ childIndex: 2,
42
+ dragLineIndex: 4,
43
+ dragLineLevel: 2,
44
+ insertionIndex: 0,
45
+ item: tree.item("x11"),
46
+ },
47
+ draggedItems: [tree.item("x111"), tree.item("x112")],
48
+ });
49
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Started);
50
+ });
51
+
52
+ it("moves down 1", () => {
53
+ tree.do.selectMultiple("x111", "x112");
54
+ tree.do.hotkey("startDrag");
55
+ tree.do.hotkey("dragDown");
56
+ tree.expect.substate("dnd", {
57
+ dragTarget: {
58
+ childIndex: 3,
59
+ dragLineIndex: 5,
60
+ dragLineLevel: 2,
61
+ insertionIndex: 1,
62
+ item: tree.item("x11"),
63
+ },
64
+ draggedItems: [tree.item("x111"), tree.item("x112")],
65
+ });
66
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Dragging);
67
+ });
68
+
69
+ it("moves down 2", () => {
70
+ tree.do.selectMultiple("x111", "x112");
71
+ tree.do.hotkey("startDrag");
72
+ tree.do.hotkey("dragDown");
73
+ tree.do.hotkey("dragDown");
74
+ tree.expect.substate("dnd", {
75
+ dragTarget: {
76
+ childIndex: 4,
77
+ dragLineIndex: 6,
78
+ dragLineLevel: 2,
79
+ insertionIndex: 2,
80
+ item: tree.item("x11"),
81
+ },
82
+ draggedItems: [tree.item("x111"), tree.item("x112")],
83
+ });
84
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Dragging);
85
+ });
86
+
87
+ it("reparent down", () => {
88
+ tree.do.selectMultiple("x111", "x112");
89
+ tree.do.hotkey("startDrag");
90
+ tree.do.hotkey("dragDown");
91
+ tree.do.hotkey("dragDown");
92
+ tree.do.hotkey("dragDown");
93
+ tree.expect.substate("dnd", {
94
+ dragTarget: {
95
+ childIndex: 1,
96
+ dragLineIndex: 6,
97
+ dragLineLevel: 1,
98
+ insertionIndex: 1,
99
+ item: tree.item("x1"),
100
+ },
101
+ draggedItems: [tree.item("x111"), tree.item("x112")],
102
+ });
103
+ });
104
+
105
+ it("doesn't reparent further", () => {
106
+ tree.do.selectMultiple("x111", "x112");
107
+ tree.do.hotkey("startDrag");
108
+ tree.do.hotkey("dragDown");
109
+ tree.do.hotkey("dragDown");
110
+ tree.do.hotkey("dragDown");
111
+ tree.do.hotkey("dragDown");
112
+ tree.expect.substate("dnd", {
113
+ dragTarget: { item: tree.item("x12") },
114
+ draggedItems: [tree.item("x111"), tree.item("x112")],
115
+ });
116
+ });
117
+
118
+ it("reparent back up", () => {
119
+ tree.do.selectMultiple("x111", "x112");
120
+ tree.do.hotkey("startDrag");
121
+ tree.do.hotkey("dragDown");
122
+ tree.do.hotkey("dragDown");
123
+ tree.do.hotkey("dragDown");
124
+ tree.do.hotkey("dragUp");
125
+ tree.expect.substate("dnd", {
126
+ dragTarget: {
127
+ childIndex: 4,
128
+ dragLineIndex: 6,
129
+ dragLineLevel: 2,
130
+ insertionIndex: 2,
131
+ item: tree.item("x11"),
132
+ },
133
+ draggedItems: [tree.item("x111"), tree.item("x112")],
134
+ });
135
+ });
136
+ });
137
+
138
+ describe("dropping", () => {
139
+ it("drops below starting items", async () => {
140
+ tree.do.selectMultiple("x111", "x112");
141
+ tree.do.hotkey("startDrag");
142
+ tree.do.hotkey("dragDown");
143
+ tree.do.hotkey("dragDown");
144
+ tree.do.hotkey("completeDrag");
145
+ tree.expect.dropped(["x111", "x112"], {
146
+ childIndex: 4,
147
+ insertionIndex: 2,
148
+ dragLineIndex: 6,
149
+ dragLineLevel: 2,
150
+ item: tree.item("x11"),
151
+ });
152
+ await vi.waitFor(() =>
153
+ tree.expect.substate(
154
+ "assistiveDndState",
155
+ AssistiveDndState.Completed,
156
+ ),
157
+ );
158
+ });
159
+
160
+ it("drops above starting items", async () => {
161
+ tree.do.selectMultiple("x111", "x112");
162
+ tree.do.hotkey("startDrag");
163
+ tree.do.hotkey("dragUp");
164
+ tree.do.hotkey("dragUp");
165
+ tree.do.hotkey("completeDrag");
166
+ tree.expect.dropped(["x111", "x112"], {
167
+ childIndex: 0,
168
+ insertionIndex: 0,
169
+ dragLineIndex: 2,
170
+ dragLineLevel: 2,
171
+ item: tree.item("x11"),
172
+ });
173
+ await vi.waitFor(() =>
174
+ tree.expect.substate(
175
+ "assistiveDndState",
176
+ AssistiveDndState.Completed,
177
+ ),
178
+ );
179
+ });
180
+
181
+ it("drops inside folder", () => {
182
+ tree.do.selectMultiple("x111", "x112");
183
+ tree.do.hotkey("startDrag");
184
+ tree.do.hotkey("dragDown");
185
+ tree.do.hotkey("dragDown");
186
+ tree.do.hotkey("dragDown");
187
+ tree.do.hotkey("dragDown");
188
+ tree.do.hotkey("completeDrag");
189
+ tree.expect.dropped(["x111", "x112"], {
190
+ item: tree.item("x12"),
191
+ });
192
+ });
193
+
194
+ it("cancels drag", async () => {
195
+ const onDrop = tree.mockedHandler("onDrop");
196
+ tree.do.selectMultiple("x111", "x112");
197
+ tree.do.hotkey("startDrag");
198
+ tree.do.hotkey("cancelDrag");
199
+ expect(onDrop).not.toBeCalled();
200
+ tree.expect.substate("dnd", null);
201
+ tree.expect.substate("assistiveDndState", AssistiveDndState.None);
202
+ });
203
+ });
204
+
205
+ describe("foreign drag", () => {
206
+ it("drags items out of tree", () => {
207
+ tree.do.selectMultiple("x111", "x112");
208
+ tree.do.hotkey("startDrag");
209
+ expect(tree.instance.getState().dnd?.draggedItems).toStrictEqual([
210
+ tree.item("x111"),
211
+ tree.item("x112"),
212
+ ]);
213
+ tree.instance.stopKeyboardDrag();
214
+ expect(tree.instance.getState().dnd).toBe(null);
215
+ });
216
+
217
+ it("drags items inside of tree", async () => {
218
+ tree.mockedHandler("canDropForeignDragObject").mockReturnValue(true);
219
+ tree.item("x111").setFocused();
220
+ const dataTransfer = {
221
+ getData: vi.fn().mockReturnValue("hello world"),
222
+ };
223
+ tree.instance.startKeyboardDragOnForeignObject(
224
+ dataTransfer as unknown as DataTransfer,
225
+ );
226
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Started);
227
+ tree.expect.substate("dnd", {
228
+ draggedItems: undefined,
229
+ dragTarget: {
230
+ childIndex: 1,
231
+ dragLineIndex: 3,
232
+ dragLineLevel: 2,
233
+ insertionIndex: 1,
234
+ item: tree.item("x11"),
235
+ },
236
+ });
237
+ });
238
+
239
+ it("starts at first valid location when dragging foreign object", async () => {
240
+ const canDropForeignDragObject = tree
241
+ .mockedHandler("canDropForeignDragObject")
242
+ .mockReturnValue(true);
243
+ canDropForeignDragObject.mockReturnValueOnce(false);
244
+ canDropForeignDragObject.mockReturnValueOnce(false);
245
+ tree.item("x111").setFocused();
246
+ const dataTransfer = {
247
+ getData: vi.fn().mockReturnValue("hello world"),
248
+ };
249
+ tree.instance.startKeyboardDragOnForeignObject(
250
+ dataTransfer as unknown as DataTransfer,
251
+ );
252
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Started);
253
+ tree.expect.substate("dnd", {
254
+ draggedItems: undefined,
255
+ dragTarget: {
256
+ childIndex: 2,
257
+ dragLineIndex: 4,
258
+ dragLineLevel: 2,
259
+ insertionIndex: 2,
260
+ item: tree.item("x11"),
261
+ },
262
+ });
263
+ });
264
+
265
+ it("skips invalid positions to inbetween when dragging foreign object", async () => {
266
+ const canDropForeignDragObject = tree
267
+ .mockedHandler("canDropForeignDragObject")
268
+ .mockReturnValue(true);
269
+ tree.item("x111").setFocused();
270
+ const dataTransfer = {
271
+ getData: vi.fn().mockReturnValue("hello world"),
272
+ };
273
+ tree.instance.startKeyboardDragOnForeignObject(
274
+ dataTransfer as unknown as DataTransfer,
275
+ );
276
+ canDropForeignDragObject.mockReturnValueOnce(false);
277
+ canDropForeignDragObject.mockReturnValueOnce(false);
278
+ canDropForeignDragObject.mockReturnValueOnce(false);
279
+ tree.do.hotkey("dragDown");
280
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Dragging);
281
+ tree.expect.substate("dnd", {
282
+ draggedItems: undefined,
283
+ dragTarget: {
284
+ childIndex: 3,
285
+ dragLineIndex: 5,
286
+ dragLineLevel: 2,
287
+ insertionIndex: 3,
288
+ item: tree.item("x11"),
289
+ },
290
+ });
291
+ });
292
+
293
+ it("skips invalid positions to folder when dragging foreign object", async () => {
294
+ const canDropForeignDragObject = tree
295
+ .mockedHandler("canDropForeignDragObject")
296
+ .mockReturnValue(true);
297
+ tree.item("x111").setFocused();
298
+ const dataTransfer = {
299
+ getData: vi.fn().mockReturnValue("hello world"),
300
+ };
301
+ tree.instance.startKeyboardDragOnForeignObject(
302
+ dataTransfer as unknown as DataTransfer,
303
+ );
304
+ canDropForeignDragObject.mockReturnValueOnce(false);
305
+ canDropForeignDragObject.mockReturnValueOnce(false);
306
+ canDropForeignDragObject.mockReturnValueOnce(false);
307
+ canDropForeignDragObject.mockReturnValueOnce(false);
308
+ tree.do.hotkey("dragDown");
309
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Dragging);
310
+ tree.expect.substate("dnd", {
311
+ draggedItems: undefined,
312
+ dragTarget: {
313
+ item: tree.item("x114"),
314
+ },
315
+ });
316
+ });
317
+ });
318
+
319
+ describe("drag restrictions", () => {
320
+ const expectChildIndex = (index: number) => {
321
+ const state = tree.instance.getState().dnd?.dragTarget;
322
+ if (!state || !isOrderedDragTarget(state))
323
+ throw new Error("No childIndex");
324
+ expect(state.childIndex).toEqual(index);
325
+ };
326
+
327
+ it("doesnt drag when canDrag=false", () => {
328
+ const canDrag = tree.mockedHandler("canDrag").mockReturnValue(false);
329
+ tree.do.selectMultiple("x111", "x112");
330
+ tree.do.hotkey("startDrag");
331
+ expect(canDrag).toHaveBeenCalledWith([
332
+ tree.item("x111"),
333
+ tree.item("x112"),
334
+ ]);
335
+ tree.expect.substate("dnd", undefined);
336
+ tree.expect.substate("assistiveDndState", undefined);
337
+ });
338
+
339
+ it("skips positions during arrowing that have canDrop=false", () => {
340
+ // note that, with mocked canDrop, non-folders are viable targets
341
+ const canDrop = tree.mockedHandler("canDrop").mockReturnValue(true);
342
+ tree.do.selectMultiple("x111");
343
+ tree.do.hotkey("startDrag");
344
+ tree.do.hotkey("dragDown");
345
+ tree.do.hotkey("dragDown");
346
+ expect(canDrop).toBeCalled();
347
+ expectChildIndex(2);
348
+
349
+ canDrop.mockReturnValueOnce(false);
350
+ canDrop.mockReturnValueOnce(false);
351
+ canDrop.mockReturnValueOnce(false);
352
+ tree.do.hotkey("dragDown");
353
+ expectChildIndex(4);
354
+ });
355
+
356
+ it("doesnt go below end of tree", () => {
357
+ const lastState = {
358
+ draggedItems: [tree.item("x111")],
359
+ dragTarget: {
360
+ item: tree.item("x"),
361
+ childIndex: 4,
362
+ dragLineIndex: 20,
363
+ dragLineLevel: 0,
364
+ insertionIndex: 4,
365
+ },
366
+ };
367
+
368
+ tree.do.selectMultiple("x111");
369
+ tree.item("x3").setFocused();
370
+ tree.do.hotkey("startDrag");
371
+ tree.do.hotkey("dragDown");
372
+ tree.do.hotkey("dragDown");
373
+ tree.do.hotkey("dragDown");
374
+ tree.expect.substate("dnd", lastState);
375
+ tree.do.hotkey("dragDown");
376
+ tree.expect.substate("dnd", lastState);
377
+ });
378
+
379
+ it("doesnt go above top of tree", () => {
380
+ const firstState = {
381
+ draggedItems: [tree.item("x111")],
382
+ dragTarget: {
383
+ item: tree.item("x"),
384
+ childIndex: 0,
385
+ dragLineIndex: 0,
386
+ dragLineLevel: 0,
387
+ insertionIndex: 0,
388
+ },
389
+ };
390
+
391
+ tree.do.selectMultiple("x111");
392
+ tree.item("x1").setFocused();
393
+ tree.do.hotkey("startDrag");
394
+ tree.do.hotkey("dragUp");
395
+ tree.do.hotkey("dragUp");
396
+ tree.expect.substate("dnd", firstState);
397
+ tree.do.hotkey("dragUp");
398
+ tree.expect.substate("dnd", firstState);
399
+ });
400
+ });
401
+ });
402
+ });
@@ -0,0 +1,30 @@
1
+ import { ItemInstance, SetStateFn } from "../../types/core";
2
+
3
+ export interface KDndDataRef {
4
+ kDndDataTransfer: DataTransfer | undefined;
5
+ }
6
+
7
+ export enum AssistiveDndState {
8
+ None,
9
+ Started,
10
+ Dragging,
11
+ Completed,
12
+ Aborted,
13
+ }
14
+
15
+ export type KeyboardDragAndDropFeatureDef<T> = {
16
+ state: {
17
+ assistiveDndState?: AssistiveDndState | null;
18
+ };
19
+ config: {
20
+ setAssistiveDndState?: SetStateFn<AssistiveDndState | undefined | null>;
21
+ onStartKeyboardDrag?: (items: ItemInstance<T>[]) => void;
22
+ };
23
+ treeInstance: {
24
+ startKeyboardDrag: (items: ItemInstance<T>[]) => void;
25
+ startKeyboardDragOnForeignObject: (dataTransfer: DataTransfer) => void;
26
+ stopKeyboardDrag: () => void;
27
+ };
28
+ itemInstance: {};
29
+ hotkeys: "startDrag" | "cancelDrag" | "completeDrag" | "dragUp" | "dragDown";
30
+ };
@@ -3,15 +3,14 @@ import { PropMemoizationDataRef } from "./types";
3
3
 
4
4
  const memoize = (
5
5
  props: Record<string, any>,
6
- dataRef: PropMemoizationDataRef,
6
+ memoizedProps: Record<string, any>,
7
7
  ) => {
8
- dataRef.memoizedProps ??= {};
9
8
  for (const key in props) {
10
9
  if (typeof props[key] === "function") {
11
- if (key in dataRef.memoizedProps) {
12
- props[key] = dataRef.memoizedProps[key];
10
+ if (memoizedProps && key in memoizedProps) {
11
+ props[key] = memoizedProps[key];
13
12
  } else {
14
- dataRef.memoizedProps[key] = props[key];
13
+ memoizedProps[key] = props[key];
15
14
  }
16
15
  }
17
16
  }
@@ -34,10 +33,20 @@ export const propMemoizationFeature: FeatureImplementation = {
34
33
  ],
35
34
 
36
35
  treeInstance: {
37
- getContainerProps: ({ tree, prev }) => {
36
+ getContainerProps: ({ tree, prev }, treeLabel) => {
37
+ const dataRef = tree.getDataRef<PropMemoizationDataRef>();
38
+ const props = prev?.(treeLabel) ?? {};
39
+ dataRef.current.memo ??= {};
40
+ dataRef.current.memo.tree ??= {};
41
+ return memoize(props, dataRef.current.memo.tree);
42
+ },
43
+
44
+ getSearchInputElementProps: ({ tree, prev }) => {
38
45
  const dataRef = tree.getDataRef<PropMemoizationDataRef>();
39
46
  const props = prev?.() ?? {};
40
- return memoize(props, dataRef.current);
47
+ dataRef.current.memo ??= {};
48
+ dataRef.current.memo.search ??= {};
49
+ return memoize(props, dataRef.current.memo.search);
41
50
  },
42
51
  },
43
52
 
@@ -45,7 +54,17 @@ export const propMemoizationFeature: FeatureImplementation = {
45
54
  getProps: ({ item, prev }) => {
46
55
  const dataRef = item.getDataRef<PropMemoizationDataRef>();
47
56
  const props = prev?.() ?? {};
48
- return memoize(props, dataRef.current);
57
+ dataRef.current.memo ??= {};
58
+ dataRef.current.memo.item ??= {};
59
+ return memoize(props, dataRef.current.memo.item);
60
+ },
61
+
62
+ getRenameInputProps: ({ item, prev }) => {
63
+ const dataRef = item.getDataRef<PropMemoizationDataRef>();
64
+ const props = prev?.() ?? {};
65
+ dataRef.current.memo ??= {};
66
+ dataRef.current.memo.rename ??= {};
67
+ return memoize(props, dataRef.current.memo.rename);
49
68
  },
50
69
  },
51
70
  };
@@ -17,8 +17,8 @@ const customFeature: FeatureImplementation = {
17
17
  }),
18
18
  },
19
19
  treeInstance: {
20
- getContainerProps: ({ prev }) => ({
21
- ...prev?.(),
20
+ getContainerProps: ({ prev }, treeLabel) => ({
21
+ ...prev?.(treeLabel),
22
22
  customValue: createTreeValue(),
23
23
  onCustomEvent: () => treeHandler(),
24
24
  }),
@@ -1,6 +1,11 @@
1
- export type PropMemoizationDataRef = {
2
- memoizedProps?: Record<string, any>;
3
- };
1
+ export interface PropMemoizationDataRef {
2
+ memo?: {
3
+ tree?: Record<string, any>;
4
+ item?: Record<string, any>;
5
+ search?: Record<string, any>;
6
+ rename?: Record<string, any>;
7
+ };
8
+ }
4
9
 
5
10
  export type PropMemoizationFeatureDef = {
6
11
  state: {};
@@ -1,6 +1,12 @@
1
1
  import { FeatureImplementation, ItemInstance } from "../../types/core";
2
2
  import { makeStateUpdater } from "../../utils";
3
3
 
4
+ type InputEvent = {
5
+ target?: {
6
+ value: string;
7
+ };
8
+ };
9
+
4
10
  export const renamingFeature: FeatureImplementation = {
5
11
  key: "renaming",
6
12
 
@@ -53,10 +59,10 @@ export const renamingFeature: FeatureImplementation = {
53
59
  },
54
60
 
55
61
  getRenameInputProps: ({ tree }) => ({
62
+ ref: (r: HTMLInputElement) => r?.focus(),
56
63
  onBlur: () => tree.abortRenaming(),
57
64
  value: tree.getRenamingValue(),
58
- onChange: (e: any) => {
59
- // TODO custom type with e.target.value
65
+ onChange: (e: InputEvent) => {
60
66
  tree.applySubStateUpdate("renamingValue", e.target?.value);
61
67
  },
62
68
  }),
@@ -56,10 +56,12 @@ export const searchFeature: FeatureImplementation = {
56
56
  getSearchInputElement: ({ tree }) =>
57
57
  tree.getDataRef<SearchFeatureDataRef>().current.searchInput ?? null,
58
58
 
59
+ // TODO memoize with propMemoizationFeature
59
60
  getSearchInputElementProps: ({ tree }) => ({
60
61
  value: tree.getSearchValue(),
61
62
  onChange: (e: any) => tree.setSearch(e.target.value),
62
63
  onBlur: () => tree.closeSearch(),
64
+ ref: tree.registerSearchInputElement,
63
65
  }),
64
66
 
65
67
  getSearchMatchingItems: memo(
@@ -1,10 +1,10 @@
1
1
  import { ItemInstance, SetStateFn } from "../../types/core";
2
2
  import { HotkeysCoreDataRef } from "../hotkeys-core/types";
3
3
 
4
- export type SearchFeatureDataRef<T = any> = HotkeysCoreDataRef & {
4
+ export interface SearchFeatureDataRef<T = any> extends HotkeysCoreDataRef {
5
5
  matchingItems: ItemInstance<T>[];
6
6
  searchInput: HTMLInputElement | null;
7
- };
7
+ }
8
8
 
9
9
  export type SearchFeatureDef<T> = {
10
10
  state: {
@@ -23,7 +23,6 @@ export const selectionFeature: FeatureImplementation = {
23
23
  tree.applySubStateUpdate("selectedItems", selectedItems);
24
24
  },
25
25
 
26
- // TODO memo
27
26
  getSelectedItems: ({ tree }) => {
28
27
  return tree.getState().selectedItems.map(tree.getItemInstance);
29
28
  },
@@ -103,9 +102,10 @@ export const selectionFeature: FeatureImplementation = {
103
102
  // tree.setSelectedItems([tree.getFocusedItem().getId()]);
104
103
  // },
105
104
  // },
106
- toggleSelectItem: {
107
- hotkey: "ctrl+space",
108
- handler: (e, tree) => {
105
+ toggleSelectedItem: {
106
+ hotkey: "Control+Space",
107
+ preventDefault: true,
108
+ handler: (_, tree) => {
109
109
  tree.getFocusedItem().toggleSelect();
110
110
  },
111
111
  },
@@ -19,7 +19,7 @@ export type SelectionFeatureDef<T> = {
19
19
  selectUpTo: (ctrl: boolean) => void;
20
20
  };
21
21
  hotkeys:
22
- | "toggleSelectItem"
22
+ | "toggleSelectedItem"
23
23
  | "selectUpwards"
24
24
  | "selectDownwards"
25
25
  | "selectAll";
@@ -1,29 +1,48 @@
1
1
  import { FeatureImplementation } from "../../types/core";
2
2
  import { makeStateUpdater } from "../../utils";
3
+ import { throwError } from "../../utilities/errors";
4
+
5
+ const promiseErrorMessage = "sync dataLoader returned promise";
3
6
 
4
7
  export const syncDataLoaderFeature: FeatureImplementation = {
5
8
  key: "sync-data-loader",
6
9
 
7
10
  getInitialState: (initialState) => ({
8
- loadingItems: [],
11
+ loadingItemData: [],
12
+ loadingItemChildrens: [],
9
13
  ...initialState,
10
14
  }),
11
15
 
12
16
  getDefaultConfig: (defaultConfig, tree) => ({
13
- setLoadingItems: makeStateUpdater("loadingItems", tree),
17
+ setLoadingItemData: makeStateUpdater("loadingItemData", tree),
18
+ setLoadingItemChildrens: makeStateUpdater("loadingItemChildrens", tree),
14
19
  ...defaultConfig,
15
20
  }),
16
21
 
17
22
  stateHandlerNames: {
18
- loadingItems: "setLoadingItems",
23
+ loadingItemData: "setLoadingItemData",
24
+ loadingItemChildrens: "setLoadingItemChildrens",
19
25
  },
20
26
 
21
27
  treeInstance: {
22
- retrieveItemData: ({ tree }, itemId) =>
23
- tree.getConfig().dataLoader!.getItem(itemId),
28
+ waitForItemDataLoaded: async () => {},
29
+ waitForItemChildrenLoaded: async () => {},
30
+
31
+ retrieveItemData: ({ tree }, itemId) => {
32
+ const data = tree.getConfig().dataLoader.getItem(itemId);
33
+ if (typeof data === "object" && "then" in data) {
34
+ throw throwError(promiseErrorMessage);
35
+ }
36
+ return data;
37
+ },
24
38
 
25
- retrieveChildrenIds: ({ tree }, itemId) =>
26
- tree.getConfig().dataLoader!.getChildren(itemId),
39
+ retrieveChildrenIds: ({ tree }, itemId) => {
40
+ const data = tree.getConfig().dataLoader.getChildren(itemId);
41
+ if (typeof data === "object" && "then" in data) {
42
+ throw throwError(promiseErrorMessage);
43
+ }
44
+ return data;
45
+ },
27
46
  },
28
47
 
29
48
  itemInstance: {
@@ -1,13 +1,13 @@
1
- export type SyncTreeDataLoader<T> = {
2
- getItem: (itemId: string) => T;
3
- getChildren: (itemId: string) => string[];
4
- };
1
+ export interface TreeDataLoader<T> {
2
+ getItem: (itemId: string) => T | Promise<T>;
3
+ getChildren: (itemId: string) => string[] | Promise<string[]>;
4
+ }
5
5
 
6
6
  export type SyncDataLoaderFeatureDef<T> = {
7
7
  state: {};
8
8
  config: {
9
9
  rootItemId: string;
10
- dataLoader?: SyncTreeDataLoader<T>;
10
+ dataLoader: TreeDataLoader<T>;
11
11
  };
12
12
  treeInstance: {
13
13
  retrieveItemData: (itemId: string) => T;