@headless-tree/core 0.0.13 → 0.0.15

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 +18 -3
  9. package/lib/cjs/features/drag-and-drop/utils.js +49 -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 +207 -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 +2 -2
  19. package/lib/cjs/features/prop-memoization/types.d.ts +2 -2
  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 -7
  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 +18 -3
  52. package/lib/esm/features/drag-and-drop/utils.js +44 -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 +204 -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 +2 -2
  62. package/lib/esm/features/prop-memoization/types.d.ts +2 -2
  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 -7
  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 +21 -22
  96. package/src/features/drag-and-drop/types.ts +23 -35
  97. package/src/features/drag-and-drop/utils.ts +67 -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 +401 -0
  104. package/src/features/keyboard-drag-and-drop/types.ts +30 -0
  105. package/src/features/prop-memoization/feature.ts +2 -2
  106. package/src/features/prop-memoization/prop-memoization.spec.ts +2 -2
  107. package/src/features/prop-memoization/types.ts +2 -2
  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 -11
  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,255 @@
1
+ import {
2
+ FeatureImplementation,
3
+ ItemInstance,
4
+ TreeInstance,
5
+ } from "../../types/core";
6
+ import { DndDataRef, DragTarget } from "../drag-and-drop/types";
7
+ import {
8
+ ItemDropCategory,
9
+ canDrop,
10
+ getInsertionIndex,
11
+ getItemDropCategory,
12
+ getReparentTarget,
13
+ } from "../drag-and-drop/utils";
14
+ import { makeStateUpdater } from "../../utils";
15
+ import { AssistiveDndState, KDndDataRef } from "./types";
16
+
17
+ const getNextDragTarget = <T>(
18
+ tree: TreeInstance<T>,
19
+ isUp: boolean,
20
+ dragTarget: DragTarget<T>,
21
+ ): DragTarget<T> | undefined => {
22
+ const direction = isUp ? 0 : 1;
23
+ const draggedItems = tree.getState().dnd?.draggedItems;
24
+
25
+ // currently hovering between items
26
+ if ("childIndex" in dragTarget) {
27
+ // TODO move check in reusable function
28
+ const parent = dragTarget.item.getParent();
29
+ const targetedItem = tree.getItems()[dragTarget.dragLineIndex - 1]; // item above dragline
30
+
31
+ const targetCategory = targetedItem
32
+ ? getItemDropCategory(targetedItem)
33
+ : ItemDropCategory.Item;
34
+ const maxLevel = targetedItem?.getItemMeta().level ?? 0;
35
+ const minLevel = targetedItem?.getItemBelow()?.getItemMeta().level ?? 0;
36
+
37
+ // reparenting
38
+ if (targetCategory === ItemDropCategory.LastInGroup) {
39
+ if (isUp && dragTarget.dragLineLevel < maxLevel) {
40
+ return getReparentTarget(
41
+ targetedItem,
42
+ dragTarget.dragLineLevel + 1,
43
+ draggedItems,
44
+ );
45
+ }
46
+ if (!isUp && dragTarget.dragLineLevel > minLevel && parent) {
47
+ return getReparentTarget(
48
+ targetedItem,
49
+ dragTarget.dragLineLevel - 1,
50
+ draggedItems,
51
+ );
52
+ }
53
+ }
54
+
55
+ const newIndex = dragTarget.dragLineIndex - 1 + direction;
56
+ const item = tree.getItems()[newIndex];
57
+ return item ? { item } : undefined;
58
+ }
59
+
60
+ // moving upwards outside of an open folder
61
+ const targetingExpandedFolder =
62
+ getItemDropCategory(dragTarget.item) === ItemDropCategory.ExpandedFolder;
63
+ if (targetingExpandedFolder && !isUp) {
64
+ return {
65
+ item: dragTarget.item,
66
+ childIndex: 0,
67
+ insertionIndex: getInsertionIndex(
68
+ dragTarget.item.getChildren(),
69
+ 0,
70
+ draggedItems,
71
+ ),
72
+ dragLineIndex: dragTarget.item.getItemMeta().index + direction,
73
+ dragLineLevel: dragTarget.item.getItemMeta().level + 1,
74
+ };
75
+ }
76
+
77
+ // currently hovering over item
78
+ const childIndex = dragTarget.item.getIndexInParent() + direction;
79
+ return {
80
+ item: dragTarget.item.getParent()!,
81
+ childIndex,
82
+ insertionIndex: getInsertionIndex(
83
+ dragTarget.item.getParent()!.getChildren(),
84
+ childIndex,
85
+ draggedItems,
86
+ ),
87
+ dragLineIndex: dragTarget.item.getItemMeta().index + direction,
88
+ dragLineLevel: dragTarget.item.getItemMeta().level,
89
+ };
90
+ };
91
+
92
+ const getNextValidDragTarget = <T>(
93
+ tree: TreeInstance<T>,
94
+ isUp: boolean,
95
+ previousTarget = tree.getState().dnd?.dragTarget,
96
+ ): DragTarget<T> | undefined => {
97
+ if (!previousTarget) return undefined;
98
+ const nextTarget = getNextDragTarget(tree, isUp, previousTarget);
99
+ const dataTransfer =
100
+ tree.getDataRef<KDndDataRef>().current.kDndDataTransfer ?? null;
101
+ if (!nextTarget) return undefined;
102
+ if (canDrop(dataTransfer, nextTarget, tree)) {
103
+ return nextTarget;
104
+ }
105
+ return getNextValidDragTarget(tree, isUp, nextTarget);
106
+ };
107
+
108
+ const updateScroll = <T>(tree: TreeInstance<T>) => {
109
+ const state = tree.getState().dnd;
110
+ if (!state?.dragTarget || "childIndex" in state.dragTarget) return;
111
+ state.dragTarget.item.scrollTo({ block: "nearest", inline: "nearest" });
112
+ };
113
+
114
+ const initiateDrag = <T>(
115
+ tree: TreeInstance<T>,
116
+ draggedItems?: ItemInstance<T>[],
117
+ dataTransfer?: DataTransfer,
118
+ ) => {
119
+ const focusedItem = tree.getFocusedItem();
120
+ const { canDrag } = tree.getConfig();
121
+
122
+ if (draggedItems && canDrag && !canDrag(draggedItems)) {
123
+ return;
124
+ }
125
+
126
+ if (draggedItems) {
127
+ tree.applySubStateUpdate("dnd", { draggedItems });
128
+ // getNextValidDragTarget->canDrop needs the draggedItems in state
129
+ tree.getConfig().onStartKeyboardDrag?.(draggedItems);
130
+ } else if (dataTransfer) {
131
+ tree.getDataRef<KDndDataRef>().current.kDndDataTransfer = dataTransfer;
132
+ }
133
+
134
+ const dragTarget = getNextValidDragTarget(tree, false, {
135
+ item: focusedItem,
136
+ });
137
+ if (!dragTarget) return;
138
+
139
+ tree.applySubStateUpdate("dnd", {
140
+ draggedItems,
141
+ dragTarget,
142
+ });
143
+ tree.applySubStateUpdate("assistiveDndState", AssistiveDndState.Started);
144
+ updateScroll(tree);
145
+ };
146
+
147
+ const moveDragPosition = <T>(tree: TreeInstance<T>, isUp: boolean) => {
148
+ const dragTarget = getNextValidDragTarget(tree, isUp);
149
+ if (!dragTarget) return;
150
+ tree.applySubStateUpdate("dnd", {
151
+ draggedItems: tree.getState().dnd?.draggedItems,
152
+ dragTarget,
153
+ });
154
+ tree.applySubStateUpdate("assistiveDndState", AssistiveDndState.Dragging);
155
+ if (!("childIndex" in dragTarget)) {
156
+ dragTarget.item.setFocused();
157
+ }
158
+ updateScroll(tree);
159
+ };
160
+
161
+ export const keyboardDragAndDropFeature: FeatureImplementation = {
162
+ key: "keyboard-drag-and-drop",
163
+ deps: ["drag-and-drop"],
164
+
165
+ getDefaultConfig: (defaultConfig, tree) => ({
166
+ setAssistiveDndState: makeStateUpdater("assistiveDndState", tree),
167
+ ...defaultConfig,
168
+ }),
169
+
170
+ stateHandlerNames: {
171
+ assistiveDndState: "setAssistiveDndState",
172
+ },
173
+
174
+ treeInstance: {
175
+ startKeyboardDrag: ({ tree }, draggedItems) => {
176
+ initiateDrag(tree, draggedItems, undefined);
177
+ },
178
+ startKeyboardDragOnForeignObject: ({ tree }, dataTransfer) => {
179
+ initiateDrag(tree, undefined, dataTransfer);
180
+ },
181
+ stopKeyboardDrag: ({ tree }) => {
182
+ tree.getDataRef<KDndDataRef>().current.kDndDataTransfer = undefined;
183
+ tree.applySubStateUpdate("dnd", null);
184
+ tree.applySubStateUpdate("assistiveDndState", AssistiveDndState.None);
185
+ },
186
+ },
187
+
188
+ hotkeys: {
189
+ startDrag: {
190
+ hotkey: "Control+Shift+D",
191
+ preventDefault: true,
192
+ isEnabled: (tree) => !tree.getState().dnd,
193
+ handler: (_, tree) => {
194
+ tree.startKeyboardDrag(tree.getSelectedItems());
195
+ },
196
+ },
197
+ dragUp: {
198
+ hotkey: "ArrowUp",
199
+ preventDefault: true,
200
+ isEnabled: (tree) => !!tree.getState().dnd,
201
+ handler: (_, tree) => {
202
+ moveDragPosition(tree, true);
203
+ },
204
+ },
205
+ dragDown: {
206
+ hotkey: "ArrowDown",
207
+ preventDefault: true,
208
+ isEnabled: (tree) => !!tree.getState().dnd,
209
+ handler: (_, tree) => {
210
+ moveDragPosition(tree, false);
211
+ },
212
+ },
213
+ cancelDrag: {
214
+ hotkey: "Escape",
215
+ isEnabled: (tree) => !!tree.getState().dnd,
216
+ handler: (_, tree) => {
217
+ tree.stopKeyboardDrag();
218
+ },
219
+ },
220
+ completeDrag: {
221
+ hotkey: "Enter",
222
+ preventDefault: true,
223
+ isEnabled: (tree) => !!tree.getState().dnd,
224
+ handler: async (e, tree) => {
225
+ e.stopPropagation();
226
+ // TODO copied from keyboard onDrop, unify them
227
+ const dataRef = tree.getDataRef<DndDataRef & KDndDataRef>();
228
+ const target = tree.getDragTarget();
229
+ const dataTransfer = dataRef.current.kDndDataTransfer ?? null;
230
+
231
+ if (!target || !canDrop(dataTransfer, target, tree)) {
232
+ return;
233
+ }
234
+
235
+ const config = tree.getConfig();
236
+ const draggedItems = tree.getState().dnd?.draggedItems;
237
+
238
+ dataRef.current.lastDragCode = undefined;
239
+ tree.applySubStateUpdate("dnd", null);
240
+
241
+ if (draggedItems) {
242
+ await config.onDrop?.(draggedItems, target);
243
+ tree.getItemInstance(draggedItems[0].getId()).setFocused();
244
+ } else if (dataTransfer) {
245
+ await config.onDropForeignDragObject?.(dataTransfer, target);
246
+ }
247
+
248
+ tree.applySubStateUpdate(
249
+ "assistiveDndState",
250
+ AssistiveDndState.Completed,
251
+ );
252
+ },
253
+ },
254
+ },
255
+ };
@@ -0,0 +1,401 @@
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
+
10
+ const isItem = (item: unknown): item is ItemInstance<any> =>
11
+ !!item && typeof item === "object" && "getId" in item;
12
+ const areItemsEqual = (a: ItemInstance<any>, b: ItemInstance<any>) => {
13
+ if (!isItem(a) || !isItem(b)) return undefined;
14
+ if (a.getId() === b.getId()) return true;
15
+ console.warn("Items are not equal:", a.getId(), b.getId());
16
+ return false;
17
+ };
18
+ expect.addEqualityTesters([areItemsEqual]);
19
+
20
+ const factory = TestTree.default({
21
+ initialState: {
22
+ expandedItems: ["x1", "x11", "x2", "x21"],
23
+ },
24
+ onDrop: vi.fn(),
25
+ }).withFeatures(
26
+ selectionFeature,
27
+ dragAndDropFeature,
28
+ keyboardDragAndDropFeature,
29
+ propMemoizationFeature,
30
+ );
31
+
32
+ describe("core-feature/keyboard-drag-and-drop", () => {
33
+ factory.forSuits((tree) => {
34
+ describe("happy paths", () => {
35
+ it("correct initial state", () => {
36
+ tree.do.selectMultiple("x111", "x112");
37
+ tree.do.hotkey("startDrag");
38
+ tree.expect.substate("dnd", {
39
+ dragTarget: {
40
+ childIndex: 2,
41
+ dragLineIndex: 4,
42
+ dragLineLevel: 2,
43
+ insertionIndex: 0,
44
+ item: tree.item("x11"),
45
+ },
46
+ draggedItems: [tree.item("x111"), tree.item("x112")],
47
+ });
48
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Started);
49
+ });
50
+
51
+ it("moves down 1", () => {
52
+ tree.do.selectMultiple("x111", "x112");
53
+ tree.do.hotkey("startDrag");
54
+ tree.do.hotkey("dragDown");
55
+ tree.expect.substate("dnd", {
56
+ dragTarget: {
57
+ childIndex: 3,
58
+ dragLineIndex: 5,
59
+ dragLineLevel: 2,
60
+ insertionIndex: 1,
61
+ item: tree.item("x11"),
62
+ },
63
+ draggedItems: [tree.item("x111"), tree.item("x112")],
64
+ });
65
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Dragging);
66
+ });
67
+
68
+ it("moves down 2", () => {
69
+ tree.do.selectMultiple("x111", "x112");
70
+ tree.do.hotkey("startDrag");
71
+ tree.do.hotkey("dragDown");
72
+ tree.do.hotkey("dragDown");
73
+ tree.expect.substate("dnd", {
74
+ dragTarget: {
75
+ childIndex: 4,
76
+ dragLineIndex: 6,
77
+ dragLineLevel: 2,
78
+ insertionIndex: 2,
79
+ item: tree.item("x11"),
80
+ },
81
+ draggedItems: [tree.item("x111"), tree.item("x112")],
82
+ });
83
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Dragging);
84
+ });
85
+
86
+ it("reparent down", () => {
87
+ tree.do.selectMultiple("x111", "x112");
88
+ tree.do.hotkey("startDrag");
89
+ tree.do.hotkey("dragDown");
90
+ tree.do.hotkey("dragDown");
91
+ tree.do.hotkey("dragDown");
92
+ tree.expect.substate("dnd", {
93
+ dragTarget: {
94
+ childIndex: 1,
95
+ dragLineIndex: 6,
96
+ dragLineLevel: 1,
97
+ insertionIndex: 1,
98
+ item: tree.item("x1"),
99
+ },
100
+ draggedItems: [tree.item("x111"), tree.item("x112")],
101
+ });
102
+ });
103
+
104
+ it("doesn't reparent further", () => {
105
+ tree.do.selectMultiple("x111", "x112");
106
+ tree.do.hotkey("startDrag");
107
+ tree.do.hotkey("dragDown");
108
+ tree.do.hotkey("dragDown");
109
+ tree.do.hotkey("dragDown");
110
+ tree.do.hotkey("dragDown");
111
+ tree.expect.substate("dnd", {
112
+ dragTarget: { item: tree.item("x12") },
113
+ draggedItems: [tree.item("x111"), tree.item("x112")],
114
+ });
115
+ });
116
+
117
+ it("reparent back up", () => {
118
+ tree.do.selectMultiple("x111", "x112");
119
+ tree.do.hotkey("startDrag");
120
+ tree.do.hotkey("dragDown");
121
+ tree.do.hotkey("dragDown");
122
+ tree.do.hotkey("dragDown");
123
+ tree.do.hotkey("dragUp");
124
+ tree.expect.substate("dnd", {
125
+ dragTarget: {
126
+ childIndex: 4,
127
+ dragLineIndex: 6,
128
+ dragLineLevel: 2,
129
+ insertionIndex: 2,
130
+ item: tree.item("x11"),
131
+ },
132
+ draggedItems: [tree.item("x111"), tree.item("x112")],
133
+ });
134
+ });
135
+ });
136
+
137
+ describe("dropping", () => {
138
+ it("drops below starting items", async () => {
139
+ tree.do.selectMultiple("x111", "x112");
140
+ tree.do.hotkey("startDrag");
141
+ tree.do.hotkey("dragDown");
142
+ tree.do.hotkey("dragDown");
143
+ tree.do.hotkey("completeDrag");
144
+ tree.expect.dropped(["x111", "x112"], {
145
+ childIndex: 4,
146
+ insertionIndex: 2,
147
+ dragLineIndex: 6,
148
+ dragLineLevel: 2,
149
+ item: tree.item("x11"),
150
+ });
151
+ await vi.waitFor(() =>
152
+ tree.expect.substate(
153
+ "assistiveDndState",
154
+ AssistiveDndState.Completed,
155
+ ),
156
+ );
157
+ });
158
+
159
+ it("drops above starting items", async () => {
160
+ tree.do.selectMultiple("x111", "x112");
161
+ tree.do.hotkey("startDrag");
162
+ tree.do.hotkey("dragUp");
163
+ tree.do.hotkey("dragUp");
164
+ tree.do.hotkey("completeDrag");
165
+ tree.expect.dropped(["x111", "x112"], {
166
+ childIndex: 0,
167
+ insertionIndex: 0,
168
+ dragLineIndex: 2,
169
+ dragLineLevel: 2,
170
+ item: tree.item("x11"),
171
+ });
172
+ await vi.waitFor(() =>
173
+ tree.expect.substate(
174
+ "assistiveDndState",
175
+ AssistiveDndState.Completed,
176
+ ),
177
+ );
178
+ });
179
+
180
+ it("drops inside folder", () => {
181
+ tree.do.selectMultiple("x111", "x112");
182
+ tree.do.hotkey("startDrag");
183
+ tree.do.hotkey("dragDown");
184
+ tree.do.hotkey("dragDown");
185
+ tree.do.hotkey("dragDown");
186
+ tree.do.hotkey("dragDown");
187
+ tree.do.hotkey("completeDrag");
188
+ tree.expect.dropped(["x111", "x112"], {
189
+ item: tree.item("x12"),
190
+ });
191
+ });
192
+
193
+ it("cancels drag", async () => {
194
+ const onDrop = tree.mockedHandler("onDrop");
195
+ tree.do.selectMultiple("x111", "x112");
196
+ tree.do.hotkey("startDrag");
197
+ tree.do.hotkey("cancelDrag");
198
+ expect(onDrop).not.toBeCalled();
199
+ tree.expect.substate("dnd", null);
200
+ tree.expect.substate("assistiveDndState", AssistiveDndState.None);
201
+ });
202
+ });
203
+
204
+ describe("foreign drag", () => {
205
+ it("drags items out of tree", () => {
206
+ tree.do.selectMultiple("x111", "x112");
207
+ tree.do.hotkey("startDrag");
208
+ expect(tree.instance.getState().dnd?.draggedItems).toStrictEqual([
209
+ tree.item("x111"),
210
+ tree.item("x112"),
211
+ ]);
212
+ tree.instance.stopKeyboardDrag();
213
+ expect(tree.instance.getState().dnd).toBe(null);
214
+ });
215
+
216
+ it("drags items inside of tree", async () => {
217
+ tree.mockedHandler("canDropForeignDragObject").mockReturnValue(true);
218
+ tree.item("x111").setFocused();
219
+ const dataTransfer = {
220
+ getData: vi.fn().mockReturnValue("hello world"),
221
+ };
222
+ tree.instance.startKeyboardDragOnForeignObject(
223
+ dataTransfer as unknown as DataTransfer,
224
+ );
225
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Started);
226
+ tree.expect.substate("dnd", {
227
+ draggedItems: undefined,
228
+ dragTarget: {
229
+ childIndex: 1,
230
+ dragLineIndex: 3,
231
+ dragLineLevel: 2,
232
+ insertionIndex: 1,
233
+ item: tree.item("x11"),
234
+ },
235
+ });
236
+ });
237
+
238
+ it("starts at first valid location when dragging foreign object", async () => {
239
+ const canDropForeignDragObject = tree
240
+ .mockedHandler("canDropForeignDragObject")
241
+ .mockReturnValue(true);
242
+ canDropForeignDragObject.mockReturnValueOnce(false);
243
+ canDropForeignDragObject.mockReturnValueOnce(false);
244
+ tree.item("x111").setFocused();
245
+ const dataTransfer = {
246
+ getData: vi.fn().mockReturnValue("hello world"),
247
+ };
248
+ tree.instance.startKeyboardDragOnForeignObject(
249
+ dataTransfer as unknown as DataTransfer,
250
+ );
251
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Started);
252
+ tree.expect.substate("dnd", {
253
+ draggedItems: undefined,
254
+ dragTarget: {
255
+ childIndex: 2,
256
+ dragLineIndex: 4,
257
+ dragLineLevel: 2,
258
+ insertionIndex: 2,
259
+ item: tree.item("x11"),
260
+ },
261
+ });
262
+ });
263
+
264
+ it("skips invalid positions to inbetween when dragging foreign object", async () => {
265
+ const canDropForeignDragObject = tree
266
+ .mockedHandler("canDropForeignDragObject")
267
+ .mockReturnValue(true);
268
+ tree.item("x111").setFocused();
269
+ const dataTransfer = {
270
+ getData: vi.fn().mockReturnValue("hello world"),
271
+ };
272
+ tree.instance.startKeyboardDragOnForeignObject(
273
+ dataTransfer as unknown as DataTransfer,
274
+ );
275
+ canDropForeignDragObject.mockReturnValueOnce(false);
276
+ canDropForeignDragObject.mockReturnValueOnce(false);
277
+ canDropForeignDragObject.mockReturnValueOnce(false);
278
+ tree.do.hotkey("dragDown");
279
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Dragging);
280
+ tree.expect.substate("dnd", {
281
+ draggedItems: undefined,
282
+ dragTarget: {
283
+ childIndex: 3,
284
+ dragLineIndex: 5,
285
+ dragLineLevel: 2,
286
+ insertionIndex: 3,
287
+ item: tree.item("x11"),
288
+ },
289
+ });
290
+ });
291
+
292
+ it("skips invalid positions to folder when dragging foreign object", async () => {
293
+ const canDropForeignDragObject = tree
294
+ .mockedHandler("canDropForeignDragObject")
295
+ .mockReturnValue(true);
296
+ tree.item("x111").setFocused();
297
+ const dataTransfer = {
298
+ getData: vi.fn().mockReturnValue("hello world"),
299
+ };
300
+ tree.instance.startKeyboardDragOnForeignObject(
301
+ dataTransfer as unknown as DataTransfer,
302
+ );
303
+ canDropForeignDragObject.mockReturnValueOnce(false);
304
+ canDropForeignDragObject.mockReturnValueOnce(false);
305
+ canDropForeignDragObject.mockReturnValueOnce(false);
306
+ canDropForeignDragObject.mockReturnValueOnce(false);
307
+ tree.do.hotkey("dragDown");
308
+ tree.expect.substate("assistiveDndState", AssistiveDndState.Dragging);
309
+ tree.expect.substate("dnd", {
310
+ draggedItems: undefined,
311
+ dragTarget: {
312
+ item: tree.item("x114"),
313
+ },
314
+ });
315
+ });
316
+ });
317
+
318
+ describe("drag restrictions", () => {
319
+ const expectChildIndex = (index: number) => {
320
+ const state = tree.instance.getState().dnd?.dragTarget;
321
+ if (!state || !("childIndex" in state))
322
+ throw new Error("No childIndex");
323
+ expect(state.childIndex).toEqual(index);
324
+ };
325
+
326
+ it("doesnt drag when canDrag=false", () => {
327
+ const canDrag = tree.mockedHandler("canDrag").mockReturnValue(false);
328
+ tree.do.selectMultiple("x111", "x112");
329
+ tree.do.hotkey("startDrag");
330
+ expect(canDrag).toHaveBeenCalledWith([
331
+ tree.item("x111"),
332
+ tree.item("x112"),
333
+ ]);
334
+ tree.expect.substate("dnd", undefined);
335
+ tree.expect.substate("assistiveDndState", undefined);
336
+ });
337
+
338
+ it("skips positions during arrowing that have canDrop=false", () => {
339
+ // note that, with mocked canDrop, non-folders are viable targets
340
+ const canDrop = tree.mockedHandler("canDrop").mockReturnValue(true);
341
+ tree.do.selectMultiple("x111");
342
+ tree.do.hotkey("startDrag");
343
+ tree.do.hotkey("dragDown");
344
+ tree.do.hotkey("dragDown");
345
+ expect(canDrop).toBeCalled();
346
+ expectChildIndex(2);
347
+
348
+ canDrop.mockReturnValueOnce(false);
349
+ canDrop.mockReturnValueOnce(false);
350
+ canDrop.mockReturnValueOnce(false);
351
+ tree.do.hotkey("dragDown");
352
+ expectChildIndex(4);
353
+ });
354
+
355
+ it("doesnt go below end of tree", () => {
356
+ const lastState = {
357
+ draggedItems: [tree.item("x111")],
358
+ dragTarget: {
359
+ item: tree.item("x"),
360
+ childIndex: 4,
361
+ dragLineIndex: 20,
362
+ dragLineLevel: 0,
363
+ insertionIndex: 4,
364
+ },
365
+ };
366
+
367
+ tree.do.selectMultiple("x111");
368
+ tree.item("x3").setFocused();
369
+ tree.do.hotkey("startDrag");
370
+ tree.do.hotkey("dragDown");
371
+ tree.do.hotkey("dragDown");
372
+ tree.do.hotkey("dragDown");
373
+ tree.expect.substate("dnd", lastState);
374
+ tree.do.hotkey("dragDown");
375
+ tree.expect.substate("dnd", lastState);
376
+ });
377
+
378
+ it("doesnt go above top of tree", () => {
379
+ const firstState = {
380
+ draggedItems: [tree.item("x111")],
381
+ dragTarget: {
382
+ item: tree.item("x"),
383
+ childIndex: 0,
384
+ dragLineIndex: 0,
385
+ dragLineLevel: 0,
386
+ insertionIndex: 0,
387
+ },
388
+ };
389
+
390
+ tree.do.selectMultiple("x111");
391
+ tree.item("x1").setFocused();
392
+ tree.do.hotkey("startDrag");
393
+ tree.do.hotkey("dragUp");
394
+ tree.do.hotkey("dragUp");
395
+ tree.expect.substate("dnd", firstState);
396
+ tree.do.hotkey("dragUp");
397
+ tree.expect.substate("dnd", firstState);
398
+ });
399
+ });
400
+ });
401
+ });
@@ -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
+ };
@@ -34,9 +34,9 @@ export const propMemoizationFeature: FeatureImplementation = {
34
34
  ],
35
35
 
36
36
  treeInstance: {
37
- getContainerProps: ({ tree, prev }) => {
37
+ getContainerProps: ({ tree, prev }, treeLabel) => {
38
38
  const dataRef = tree.getDataRef<PropMemoizationDataRef>();
39
- const props = prev?.() ?? {};
39
+ const props = prev?.(treeLabel) ?? {};
40
40
  return memoize(props, dataRef.current);
41
41
  },
42
42
  },