@headless-tree/core 1.6.1 → 1.6.3

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.
@@ -57,6 +57,7 @@ export const dragAndDropFeature: FeatureImplementation = {
57
57
  setDndState: makeStateUpdater("dnd", tree),
58
58
  canReorder: true,
59
59
  openOnDropDelay: 800,
60
+ draggedItemOverwritesSelection: true,
60
61
  ...defaultConfig,
61
62
  }),
62
63
 
@@ -178,47 +179,10 @@ export const dragAndDropFeature: FeatureImplementation = {
178
179
  itemInstance: {
179
180
  getProps: ({ tree, item, prev }) => ({
180
181
  ...prev?.(),
181
-
182
- draggable: true,
182
+ ...(tree.getConfig().seperateDragHandle ? {} : item.getDragHandleProps()),
183
183
 
184
184
  onDragEnter: (e: DragEvent) => e.preventDefault(),
185
185
 
186
- onDragStart: (e: DragEvent) => {
187
- const selectedItems = tree.getSelectedItems
188
- ? tree.getSelectedItems()
189
- : [tree.getFocusedItem()];
190
- const items = selectedItems.includes(item) ? selectedItems : [item];
191
- const config = tree.getConfig();
192
-
193
- if (!selectedItems.includes(item)) {
194
- tree.setSelectedItems?.([item.getItemMeta().itemId]);
195
- }
196
-
197
- if (!(config.canDrag?.(items) ?? true)) {
198
- e.preventDefault();
199
- return;
200
- }
201
-
202
- if (config.setDragImage) {
203
- const { imgElement, xOffset, yOffset } = config.setDragImage(items);
204
- e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
205
- }
206
-
207
- if (config.createForeignDragObject && e.dataTransfer) {
208
- const { format, data, dropEffect, effectAllowed } =
209
- config.createForeignDragObject(items);
210
- e.dataTransfer.setData(format, data);
211
-
212
- if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
213
- if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
214
- }
215
-
216
- tree.applySubStateUpdate("dnd", {
217
- draggedItems: items,
218
- draggingOverItem: tree.getFocusedItem(),
219
- });
220
- },
221
-
222
186
  onDragOver: (e: DragEvent) => {
223
187
  e.stopPropagation(); // don't bubble up to container dragover
224
188
  const dataRef = tree.getDataRef<DndDataRef>();
@@ -236,7 +200,7 @@ export const dragAndDropFeature: FeatureImplementation = {
236
200
 
237
201
  handleAutoOpenFolder(dataRef, tree, item, placement);
238
202
 
239
- const target = getDragTarget(e, item, tree);
203
+ const target = getDragTarget(e, item, tree, false);
240
204
 
241
205
  if (
242
206
  !tree.getState().dnd?.draggedItems &&
@@ -249,7 +213,8 @@ export const dragAndDropFeature: FeatureImplementation = {
249
213
  return;
250
214
  }
251
215
 
252
- if (!canDrop(e.dataTransfer, target, tree)) {
216
+ // dataTransfer.payload is not accessible in onDragOver, so just skip entirely here. It'll be checked again in onDrop
217
+ if (!canDrop(null, target, tree)) {
253
218
  dataRef.current.lastAllowDrop = false;
254
219
  return;
255
220
  }
@@ -276,31 +241,10 @@ export const dragAndDropFeature: FeatureImplementation = {
276
241
  }, 100);
277
242
  },
278
243
 
279
- onDragEnd: (e: DragEvent) => {
280
- const { onCompleteForeignDrop, canDragForeignDragObjectOver } =
281
- tree.getConfig();
282
- const draggedItems = tree.getState().dnd?.draggedItems;
283
-
284
- if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
285
- return;
286
- }
287
-
288
- const target = getDragTarget(e, item, tree);
289
- if (
290
- canDragForeignDragObjectOver &&
291
- e.dataTransfer &&
292
- !canDragForeignDragObjectOver(e.dataTransfer, target)
293
- ) {
294
- return;
295
- }
296
-
297
- onCompleteForeignDrop?.(draggedItems);
298
- },
299
-
300
244
  onDrop: async (e: DragEvent) => {
301
245
  e.stopPropagation();
302
246
  const dataRef = tree.getDataRef<DndDataRef>();
303
- const target = getDragTarget(e, item, tree);
247
+ const target = getDragTarget(e, item, tree, true);
304
248
  const draggedItems = tree.getState().dnd?.draggedItems;
305
249
  const isValidDrop = canDrop(e.dataTransfer, target, tree);
306
250
 
@@ -321,9 +265,79 @@ export const dragAndDropFeature: FeatureImplementation = {
321
265
 
322
266
  if (draggedItems) {
323
267
  await config.onDrop?.(draggedItems, target);
268
+ draggedItems[0].setFocused();
324
269
  } else if (e.dataTransfer) {
325
270
  await config.onDropForeignDragObject?.(e.dataTransfer, target);
326
271
  }
272
+
273
+ tree.applySubStateUpdate("dnd", null);
274
+ tree.updateDomFocus();
275
+ },
276
+ }),
277
+
278
+ getDragHandleProps: ({ tree, item, prev }) => ({
279
+ ...prev?.(),
280
+
281
+ draggable: true,
282
+
283
+ onDragStart: (e: DragEvent) => {
284
+ const { draggedItemOverwritesSelection } = tree.getConfig();
285
+ const selectedItems = tree.getSelectedItems
286
+ ? tree.getSelectedItems()
287
+ : [tree.getFocusedItem()];
288
+ const overwriteSelection =
289
+ !selectedItems.includes(item) && draggedItemOverwritesSelection;
290
+ const items = overwriteSelection ? [item] : selectedItems;
291
+ const config = tree.getConfig();
292
+
293
+ if (overwriteSelection) {
294
+ tree.setSelectedItems?.([item.getItemMeta().itemId]);
295
+ }
296
+
297
+ if (!(config.canDrag?.(items) ?? true)) {
298
+ e.preventDefault();
299
+ return;
300
+ }
301
+
302
+ if (config.setDragImage) {
303
+ const { imgElement, xOffset, yOffset } = config.setDragImage(items);
304
+ e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
305
+ }
306
+
307
+ if (config.createForeignDragObject && e.dataTransfer) {
308
+ const { format, data, dropEffect, effectAllowed } =
309
+ config.createForeignDragObject(items);
310
+ e.dataTransfer.setData(format, data);
311
+
312
+ if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
313
+ if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
314
+ }
315
+
316
+ tree.applySubStateUpdate("dnd", {
317
+ draggedItems: items,
318
+ draggingOverItem: tree.getFocusedItem(),
319
+ });
320
+ },
321
+
322
+ onDragEnd: (e: DragEvent) => {
323
+ const { onCompleteForeignDrop, canDragForeignDragObjectOver } =
324
+ tree.getConfig();
325
+ const draggedItems = tree.getState().dnd?.draggedItems;
326
+
327
+ if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
328
+ return;
329
+ }
330
+
331
+ const target = getDragTarget(e, item, tree, false);
332
+ if (
333
+ canDragForeignDragObjectOver &&
334
+ e.dataTransfer &&
335
+ !canDragForeignDragObjectOver(e.dataTransfer, target)
336
+ ) {
337
+ return;
338
+ }
339
+
340
+ onCompleteForeignDrop?.(draggedItems);
327
341
  },
328
342
  }),
329
343
 
@@ -97,6 +97,13 @@ export type DragAndDropFeatureDef<T> = {
97
97
 
98
98
  /** When dragging for this many ms on a closed folder, the folder will automatically open. Set to zero to disable. */
99
99
  openOnDropDelay?: number;
100
+
101
+ /** If true, `item.getProps()` will not include drag event handlers. Use `item.getDragHandleProps()` on the handler element. */
102
+ seperateDragHandle?: boolean;
103
+
104
+ /** If true, the item that is dragged is not selected, the selected items will be overwritten to just the dragged item.
105
+ * Defaults to true */
106
+ draggedItemOverwritesSelection?: boolean;
100
107
  };
101
108
  treeInstance: {
102
109
  getDragTarget: () => DragTarget<T> | null;
@@ -118,6 +125,10 @@ export type DragAndDropFeatureDef<T> = {
118
125
  isDragTargetAbove: () => boolean;
119
126
  isDragTargetBelow: () => boolean;
120
127
  isDraggingOver: () => boolean;
128
+
129
+ /** Note that `item.getProps()` already passes in all drag event handlers by default. Set `seperateDragHandle` to true to
130
+ * disable the default behavior and use this on the handler element instead. */
131
+ getDragHandleProps: () => Record<string, any>;
121
132
  };
122
133
  hotkeys: never;
123
134
  };
@@ -29,6 +29,8 @@ export type TargetPlacement =
29
29
  export const isOrderedDragTarget = <T>(dragTarget: DragTarget<T>) =>
30
30
  "childIndex" in dragTarget;
31
31
 
32
+ /** @param dataTransfer - If the data transfer object should not be considered, e.g. because the event is
33
+ * onDragOver where the browser does not allow reading the payload, pass null */
32
34
  export const canDrop = (
33
35
  dataTransfer: DataTransfer | null,
34
36
  target: DragTarget<any>,
@@ -197,21 +199,25 @@ export const getReparentTarget = <T>(
197
199
  };
198
200
  };
199
201
 
202
+ /** @param hasDataTransferPayload - If the data transfer object should not be considered, e.g. because the event is
203
+ * onDragOver where the browser does not allow reading the payload, pass false */
200
204
  export const getDragTarget = (
201
205
  e: any,
202
206
  item: ItemInstance<any>,
203
207
  tree: TreeInstance<any>,
208
+ hasDataTransferPayload: boolean,
204
209
  canReorder = tree.getConfig().canReorder,
205
210
  ): DragTarget<any> => {
211
+ const dataTransfer = hasDataTransferPayload ? e.dataTransfer : null;
206
212
  const draggedItems = tree.getState().dnd?.draggedItems;
207
213
  const itemMeta = item.getItemMeta();
208
214
  const parent = item.getParent();
209
215
  const itemTarget: DragTarget<any> = { item };
210
216
  const parentTarget: DragTarget<any> | null = parent ? { item: parent } : null;
211
217
  const canBecomeSibling =
212
- parentTarget && canDrop(e.dataTransfer, parentTarget, tree);
218
+ parentTarget && canDrop(dataTransfer, parentTarget, tree);
213
219
 
214
- const canMakeChild = canDrop(e.dataTransfer, itemTarget, tree);
220
+ const canMakeChild = canDrop(dataTransfer, itemTarget, tree);
215
221
  const placement = getTargetPlacement(e, item, tree, canMakeChild);
216
222
 
217
223
  if (
@@ -229,7 +235,7 @@ export const getDragTarget = (
229
235
 
230
236
  if (!canReorder && parent && !canBecomeSibling) {
231
237
  // TODO! this breaks in story DND/Can Drop. Maybe move this logic into a composable DragTargetStrategy[] ?
232
- return getDragTarget(e, parent, tree, false);
238
+ return getDragTarget(e, parent, tree, hasDataTransferPayload, false);
233
239
  }
234
240
 
235
241
  if (!parent) {
@@ -242,7 +248,7 @@ export const getDragTarget = (
242
248
  }
243
249
 
244
250
  if (!canBecomeSibling) {
245
- return getDragTarget(e, parent, tree, false);
251
+ return getDragTarget(e, parent, tree, hasDataTransferPayload, false);
246
252
  }
247
253
 
248
254
  if (placement.type === PlacementType.Reparent) {
@@ -5,6 +5,11 @@ import {
5
5
  } from "../../types/core";
6
6
  import { HotkeyConfig, HotkeysCoreDataRef } from "./types";
7
7
 
8
+ // e.code may be empty or "Unidentified" on mobile virtual keyboards
9
+ // or during IME composition, so we fall back to e.key
10
+ const resolveKeyCode = (e: KeyboardEvent): string =>
11
+ e.code !== "" && e.code !== "Unidentified" ? e.code : e.key;
12
+
8
13
  const specialKeys: Record<string, RegExp> = {
9
14
  // TODO:breaking deprecate auto-lowercase
10
15
  letter: /^Key[A-Z]$/,
@@ -71,9 +76,11 @@ export const hotkeysCoreFeature: FeatureImplementation = {
71
76
  return;
72
77
  }
73
78
 
79
+ const resolvedCode = resolveKeyCode(e);
80
+
74
81
  data.current.pressedKeys ??= new Set();
75
- const newMatch = !data.current.pressedKeys.has(e.code);
76
- data.current.pressedKeys.add(e.code);
82
+ const newMatch = !data.current.pressedKeys.has(resolvedCode);
83
+ data.current.pressedKeys.add(resolvedCode);
77
84
 
78
85
  const hotkeyName = findHotkeyMatch(
79
86
  data.current.pressedKeys,
@@ -85,7 +92,7 @@ export const hotkeysCoreFeature: FeatureImplementation = {
85
92
  if (e.target instanceof HTMLInputElement) {
86
93
  // JS respects composite keydowns while input elements are focused, and
87
94
  // doesnt send the associated keyup events with the same key name
88
- data.current.pressedKeys.delete(e.code);
95
+ data.current.pressedKeys.delete(resolvedCode);
89
96
  }
90
97
 
91
98
  if (!hotkeyName) return;
@@ -110,7 +117,7 @@ export const hotkeysCoreFeature: FeatureImplementation = {
110
117
 
111
118
  const keyup = (e: KeyboardEvent) => {
112
119
  data.current.pressedKeys ??= new Set();
113
- data.current.pressedKeys.delete(e.code);
120
+ data.current.pressedKeys.delete(resolveKeyCode(e));
114
121
  };
115
122
 
116
123
  const reset = () => {
@@ -254,6 +254,7 @@ export const keyboardDragAndDropFeature: FeatureImplementation = {
254
254
  await config.onDropForeignDragObject?.(dataTransfer, target);
255
255
  }
256
256
 
257
+ tree.updateDomFocus();
257
258
  tree.applySubStateUpdate(
258
259
  "assistiveDndState",
259
260
  AssistiveDndState.Completed,
@@ -59,6 +59,14 @@ export const propMemoizationFeature: FeatureImplementation = {
59
59
  return memoize(props, dataRef.current.memo.item);
60
60
  },
61
61
 
62
+ getDragHandleProps: ({ item, prev }) => {
63
+ const dataRef = item.getDataRef<PropMemoizationDataRef>();
64
+ const props = prev?.() ?? {};
65
+ dataRef.current.memo ??= {};
66
+ dataRef.current.memo.drag ??= {};
67
+ return memoize(props, dataRef.current.memo.drag);
68
+ },
69
+
62
70
  getRenameInputProps: ({ item, prev }) => {
63
71
  const dataRef = item.getDataRef<PropMemoizationDataRef>();
64
72
  const props = prev?.() ?? {};
@@ -2,6 +2,7 @@ export interface PropMemoizationDataRef {
2
2
  memo?: {
3
3
  tree?: Record<string, any>;
4
4
  item?: Record<string, any>;
5
+ drag?: Record<string, any>;
5
6
  search?: Record<string, any>;
6
7
  rename?: Record<string, any>;
7
8
  };
@@ -107,9 +107,13 @@ export const treeFeature: FeatureImplementation<any> = {
107
107
  setTimeout(async () => {
108
108
  const focusedItem = tree.getFocusedItem();
109
109
  tree.getConfig().scrollToItem?.(focusedItem);
110
- await poll(() => focusedItem.getElement() !== null, 20);
110
+ await poll(() => focusedItem.getElement() !== null, 20, 500);
111
111
  const focusedElement = focusedItem.getElement();
112
- if (!focusedElement) return;
112
+ if (!focusedElement) {
113
+ tree.getItems()[0]?.setFocused();
114
+ tree.getItems()[0]?.getElement()?.focus();
115
+ return;
116
+ }
113
117
  focusedElement.focus();
114
118
  });
115
119
  },
package/src/utils.ts CHANGED
@@ -64,5 +64,6 @@ export const poll = (fn: () => boolean, interval = 100, timeout = 1000) =>
64
64
  }, interval);
65
65
  clear = setTimeout(() => {
66
66
  clearInterval(i);
67
+ resolve();
67
68
  }, timeout);
68
69
  });