@headless-tree/core 0.0.0-20250322153940 → 0.0.0-20250330164609

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 (52) hide show
  1. package/CHANGELOG.md +2 -1
  2. package/lib/cjs/features/drag-and-drop/feature.js +11 -11
  3. package/lib/cjs/features/drag-and-drop/types.d.ts +11 -11
  4. package/lib/cjs/features/drag-and-drop/types.js +7 -7
  5. package/lib/cjs/features/drag-and-drop/utils.d.ts +18 -3
  6. package/lib/cjs/features/drag-and-drop/utils.js +42 -30
  7. package/lib/cjs/features/hotkeys-core/feature.js +1 -0
  8. package/lib/cjs/features/keyboard-drag-and-drop/feature.d.ts +2 -0
  9. package/lib/cjs/features/keyboard-drag-and-drop/feature.js +207 -0
  10. package/lib/cjs/features/keyboard-drag-and-drop/types.d.ts +27 -0
  11. package/lib/cjs/features/keyboard-drag-and-drop/types.js +11 -0
  12. package/lib/cjs/index.d.ts +2 -0
  13. package/lib/cjs/index.js +2 -0
  14. package/lib/cjs/mddocs-entry.d.ts +10 -0
  15. package/lib/cjs/test-utils/test-tree-expect.d.ts +5 -3
  16. package/lib/cjs/test-utils/test-tree-expect.js +3 -0
  17. package/lib/cjs/types/core.d.ts +2 -1
  18. package/lib/cjs/utilities/create-on-drop-handler.d.ts +2 -2
  19. package/lib/cjs/utilities/insert-items-at-target.d.ts +2 -2
  20. package/lib/esm/features/drag-and-drop/feature.js +12 -12
  21. package/lib/esm/features/drag-and-drop/types.d.ts +11 -11
  22. package/lib/esm/features/drag-and-drop/types.js +6 -6
  23. package/lib/esm/features/drag-and-drop/utils.d.ts +18 -3
  24. package/lib/esm/features/drag-and-drop/utils.js +37 -28
  25. package/lib/esm/features/hotkeys-core/feature.js +1 -0
  26. package/lib/esm/features/keyboard-drag-and-drop/feature.d.ts +2 -0
  27. package/lib/esm/features/keyboard-drag-and-drop/feature.js +204 -0
  28. package/lib/esm/features/keyboard-drag-and-drop/types.d.ts +27 -0
  29. package/lib/esm/features/keyboard-drag-and-drop/types.js +8 -0
  30. package/lib/esm/index.d.ts +2 -0
  31. package/lib/esm/index.js +2 -0
  32. package/lib/esm/mddocs-entry.d.ts +10 -0
  33. package/lib/esm/test-utils/test-tree-expect.d.ts +5 -3
  34. package/lib/esm/test-utils/test-tree-expect.js +3 -0
  35. package/lib/esm/types/core.d.ts +2 -1
  36. package/lib/esm/utilities/create-on-drop-handler.d.ts +2 -2
  37. package/lib/esm/utilities/insert-items-at-target.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/src/features/drag-and-drop/drag-and-drop.spec.ts +6 -6
  40. package/src/features/drag-and-drop/feature.ts +12 -12
  41. package/src/features/drag-and-drop/types.ts +11 -11
  42. package/src/features/drag-and-drop/utils.ts +64 -39
  43. package/src/features/hotkeys-core/feature.ts +1 -0
  44. package/src/features/keyboard-drag-and-drop/feature.ts +255 -0
  45. package/src/features/keyboard-drag-and-drop/keyboard-drag-and-drop.spec.ts +401 -0
  46. package/src/features/keyboard-drag-and-drop/types.ts +30 -0
  47. package/src/index.ts +2 -0
  48. package/src/mddocs-entry.ts +16 -0
  49. package/src/test-utils/test-tree-expect.ts +7 -2
  50. package/src/types/core.ts +2 -0
  51. package/src/utilities/create-on-drop-handler.ts +2 -2
  52. package/src/utilities/insert-items-at-target.ts +2 -2
@@ -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
+ };
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from "./core/create-tree";
4
4
  export * from "./features/tree/types";
5
5
  export { MainFeatureDef, InstanceBuilder } from "./features/main/types";
6
6
  export * from "./features/drag-and-drop/types";
7
+ export * from "./features/keyboard-drag-and-drop/types";
7
8
  export * from "./features/selection/types";
8
9
  export * from "./features/async-data-loader/types";
9
10
  export * from "./features/sync-data-loader/types";
@@ -18,6 +19,7 @@ export * from "./features/hotkeys-core/feature";
18
19
  export * from "./features/async-data-loader/feature";
19
20
  export * from "./features/sync-data-loader/feature";
20
21
  export * from "./features/drag-and-drop/feature";
22
+ export * from "./features/keyboard-drag-and-drop/feature";
21
23
  export * from "./features/search/feature";
22
24
  export * from "./features/renaming/feature";
23
25
  export * from "./features/expand-all/feature";