@headless-tree/core 1.6.0 → 1.6.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @headless-tree/core
2
2
 
3
+ ## 1.6.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 39a8b44: Add skipUpdateTree parameter to `updateCachedChildrenIds` and `updateCachedData` in async tree loader
8
+ - 26ecc1b: Fixed an issue where dropping items inside a collapsed folder will make the tree become un-focusable until a new item is clicked with mouse again. This could break usage with keyboard-only drag operations.
9
+ - 0108b7a: Fixed a bug where some hotkeys (like up/down navigation, search and renaming) doesn't work after items are dragged within the tree (#179)
10
+ - ffd2619: Fixed an issue where `canDropForeignDragObject` was being called in `onDragOver` events without payload. `canDropForeignDragObject` should never be called in `onDragOver` events since there, browsers do not allow access to the data transfer payload. Now, `canDropForeignDragObject` is only called in onDrop events, and `canDragForeignDragObjectOver` is always called in `onDragOver` events.
11
+
12
+ ## 1.6.1
13
+
14
+ ### Patch Changes
15
+
16
+ - 4ddeaf3: Fixed behavior where shift-selecting an item with no previously selected or focused item would multiselect all items from the top to the clicked item. Now, shift-selecting an item with no previously clicked items will only select the clicked item (#176)
17
+
3
18
  ## 1.6.0
4
19
 
5
20
  ### Minor Changes
package/dist/index.d.mts CHANGED
@@ -68,6 +68,8 @@ type DragAndDropFeatureDef<T> = {
68
68
  onCompleteForeignDrop?: (items: ItemInstance<T>[]) => void;
69
69
  /** When dragging for this many ms on a closed folder, the folder will automatically open. Set to zero to disable. */
70
70
  openOnDropDelay?: number;
71
+ /** If true, `item.getProps()` will not include drag event handlers. Use `item.getDragHandleProps()` on the handler element. */
72
+ seperateDragHandle?: boolean;
71
73
  };
72
74
  treeInstance: {
73
75
  getDragTarget: () => DragTarget<T> | null;
@@ -84,6 +86,9 @@ type DragAndDropFeatureDef<T> = {
84
86
  isDragTargetAbove: () => boolean;
85
87
  isDragTargetBelow: () => boolean;
86
88
  isDraggingOver: () => boolean;
89
+ /** Note that `item.getProps()` already passes in all drag event handlers by default. Set `seperateDragHandle` to true to
90
+ * disable the default behavior and use this on the handler element instead. */
91
+ getDragHandleProps: () => Record<string, any>;
87
92
  };
88
93
  hotkeys: never;
89
94
  };
@@ -329,9 +334,11 @@ type AsyncDataLoaderFeatureDef<T> = {
329
334
  * @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
330
335
  * the tree will continue to display the old data until the new data has loaded. */
331
336
  invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
332
- /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
333
- updateCachedData: (data: T | undefined) => void;
334
- updateCachedChildrenIds: (childrenIds: string[]) => void;
337
+ /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch.
338
+ * @param skipUpdateTree If true, the tree will not rebuild the tree structure cache afterwards by calling `tree.rebuildTree()`. */
339
+ updateCachedData: (data: T | undefined, skipUpdateTree?: boolean) => void;
340
+ /** @param skipUpdateTree If true, the tree will not rebuild the tree structure cache afterwards by calling `tree.rebuildTree()`. */
341
+ updateCachedChildrenIds: (childrenIds: string[], skipUpdateTree?: boolean) => void;
335
342
  hasLoadedData: () => boolean;
336
343
  isLoading: () => boolean;
337
344
  };
@@ -420,6 +427,7 @@ interface PropMemoizationDataRef {
420
427
  memo?: {
421
428
  tree?: Record<string, any>;
422
429
  item?: Record<string, any>;
430
+ drag?: Record<string, any>;
423
431
  search?: Record<string, any>;
424
432
  rename?: Record<string, any>;
425
433
  };
package/dist/index.d.ts CHANGED
@@ -68,6 +68,8 @@ type DragAndDropFeatureDef<T> = {
68
68
  onCompleteForeignDrop?: (items: ItemInstance<T>[]) => void;
69
69
  /** When dragging for this many ms on a closed folder, the folder will automatically open. Set to zero to disable. */
70
70
  openOnDropDelay?: number;
71
+ /** If true, `item.getProps()` will not include drag event handlers. Use `item.getDragHandleProps()` on the handler element. */
72
+ seperateDragHandle?: boolean;
71
73
  };
72
74
  treeInstance: {
73
75
  getDragTarget: () => DragTarget<T> | null;
@@ -84,6 +86,9 @@ type DragAndDropFeatureDef<T> = {
84
86
  isDragTargetAbove: () => boolean;
85
87
  isDragTargetBelow: () => boolean;
86
88
  isDraggingOver: () => boolean;
89
+ /** Note that `item.getProps()` already passes in all drag event handlers by default. Set `seperateDragHandle` to true to
90
+ * disable the default behavior and use this on the handler element instead. */
91
+ getDragHandleProps: () => Record<string, any>;
87
92
  };
88
93
  hotkeys: never;
89
94
  };
@@ -329,9 +334,11 @@ type AsyncDataLoaderFeatureDef<T> = {
329
334
  * @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
330
335
  * the tree will continue to display the old data until the new data has loaded. */
331
336
  invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
332
- /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
333
- updateCachedData: (data: T | undefined) => void;
334
- updateCachedChildrenIds: (childrenIds: string[]) => void;
337
+ /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch.
338
+ * @param skipUpdateTree If true, the tree will not rebuild the tree structure cache afterwards by calling `tree.rebuildTree()`. */
339
+ updateCachedData: (data: T | undefined, skipUpdateTree?: boolean) => void;
340
+ /** @param skipUpdateTree If true, the tree will not rebuild the tree structure cache afterwards by calling `tree.rebuildTree()`. */
341
+ updateCachedChildrenIds: (childrenIds: string[], skipUpdateTree?: boolean) => void;
335
342
  hasLoadedData: () => boolean;
336
343
  isLoading: () => boolean;
337
344
  };
@@ -420,6 +427,7 @@ interface PropMemoizationDataRef {
420
427
  memo?: {
421
428
  tree?: Record<string, any>;
422
429
  item?: Record<string, any>;
430
+ drag?: Record<string, any>;
423
431
  search?: Record<string, any>;
424
432
  rename?: Record<string, any>;
425
433
  };
package/dist/index.js CHANGED
@@ -125,6 +125,7 @@ var poll = (fn, interval = 100, timeout = 1e3) => new Promise((resolve) => {
125
125
  }, interval);
126
126
  clear = setTimeout(() => {
127
127
  clearInterval(i);
128
+ resolve();
128
129
  }, timeout);
129
130
  });
130
131
 
@@ -214,12 +215,16 @@ var treeFeature = {
214
215
  },
215
216
  updateDomFocus: ({ tree }) => {
216
217
  setTimeout(() => __async(null, null, function* () {
217
- var _a, _b;
218
+ var _a, _b, _c, _d, _e;
218
219
  const focusedItem = tree.getFocusedItem();
219
220
  (_b = (_a = tree.getConfig()).scrollToItem) == null ? void 0 : _b.call(_a, focusedItem);
220
- yield poll(() => focusedItem.getElement() !== null, 20);
221
+ yield poll(() => focusedItem.getElement() !== null, 20, 500);
221
222
  const focusedElement = focusedItem.getElement();
222
- if (!focusedElement) return;
223
+ if (!focusedElement) {
224
+ (_c = tree.getItems()[0]) == null ? void 0 : _c.setFocused();
225
+ (_e = (_d = tree.getItems()[0]) == null ? void 0 : _d.getElement()) == null ? void 0 : _e.focus();
226
+ return;
227
+ }
223
228
  focusedElement.focus();
224
229
  }));
225
230
  },
@@ -759,10 +764,15 @@ var selectionFeature = {
759
764
  const { selectedItems } = tree.getState();
760
765
  return selectedItems.includes(itemId);
761
766
  },
762
- selectUpTo: ({ tree, item }, ctrl) => {
767
+ selectUpTo: ({ tree, item, itemId }, ctrl) => {
763
768
  const indexA = item.getItemMeta().index;
764
- const { selectUpToAnchorId } = tree.getDataRef().current;
765
- const itemB = selectUpToAnchorId ? tree.getItemInstance(selectUpToAnchorId) : tree.getFocusedItem();
769
+ const dataRef = tree.getDataRef();
770
+ if (!dataRef.current.selectUpToAnchorId) {
771
+ dataRef.current.selectUpToAnchorId = itemId;
772
+ tree.setSelectedItems([itemId]);
773
+ return;
774
+ }
775
+ const itemB = tree.getItemInstance(dataRef.current.selectUpToAnchorId);
766
776
  const indexB = itemB.getItemMeta().index;
767
777
  const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
768
778
  const newSelectedItems = tree.getItems().slice(a, b + 1).map((treeItem) => treeItem.getItemMeta().itemId);
@@ -783,7 +793,7 @@ var selectionFeature = {
783
793
  item.select();
784
794
  }
785
795
  },
786
- getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
796
+ getProps: ({ tree, item, itemId, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
787
797
  "aria-selected": item.isSelected() ? "true" : "false",
788
798
  onClick: (e) => {
789
799
  var _a, _b;
@@ -792,10 +802,10 @@ var selectionFeature = {
792
802
  } else if (e.ctrlKey || e.metaKey) {
793
803
  item.toggleSelect();
794
804
  } else {
795
- tree.setSelectedItems([item.getItemMeta().itemId]);
805
+ tree.setSelectedItems([itemId]);
796
806
  }
797
807
  if (!e.shiftKey) {
798
- tree.getDataRef().current.selectUpToAnchorId = item.getId();
808
+ tree.getDataRef().current.selectUpToAnchorId = itemId;
799
809
  }
800
810
  (_b = (_a = prev == null ? void 0 : prev()) == null ? void 0 : _a.onClick) == null ? void 0 : _b.call(_a, e);
801
811
  }
@@ -1269,15 +1279,17 @@ var asyncDataLoaderFeature = {
1269
1279
  }
1270
1280
  yield loadChildrenIds(tree, itemId);
1271
1281
  }),
1272
- updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
1273
- const dataRef = tree.getDataRef();
1274
- dataRef.current.childrenIds[itemId] = childrenIds;
1275
- tree.rebuildTree();
1282
+ updateCachedChildrenIds: ({ tree, itemId }, childrenIds, skipUpdateTree) => {
1283
+ getDataRef(tree).current.childrenIds[itemId] = childrenIds;
1284
+ if (!skipUpdateTree) {
1285
+ tree.rebuildTree();
1286
+ }
1276
1287
  },
1277
- updateCachedData: ({ tree, itemId }, data) => {
1278
- const dataRef = tree.getDataRef();
1279
- dataRef.current.itemData[itemId] = data;
1280
- tree.rebuildTree();
1288
+ updateCachedData: ({ tree, itemId }, data, skipUpdateTree) => {
1289
+ getDataRef(tree).current.itemData[itemId] = data;
1290
+ if (!skipUpdateTree) {
1291
+ tree.rebuildTree();
1292
+ }
1281
1293
  },
1282
1294
  hasLoadedData: ({ tree, itemId }) => {
1283
1295
  const dataRef = tree.getDataRef();
@@ -1444,15 +1456,16 @@ var getReparentTarget = (item, reparentLevel, draggedItems) => {
1444
1456
  dragLineLevel: reparentLevel
1445
1457
  };
1446
1458
  };
1447
- var getDragTarget = (e, item, tree, canReorder = tree.getConfig().canReorder) => {
1459
+ var getDragTarget = (e, item, tree, hasDataTransferPayload, canReorder = tree.getConfig().canReorder) => {
1448
1460
  var _a;
1461
+ const dataTransfer = hasDataTransferPayload ? e.dataTransfer : null;
1449
1462
  const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1450
1463
  const itemMeta = item.getItemMeta();
1451
1464
  const parent = item.getParent();
1452
1465
  const itemTarget = { item };
1453
1466
  const parentTarget = parent ? { item: parent } : null;
1454
- const canBecomeSibling = parentTarget && canDrop(e.dataTransfer, parentTarget, tree);
1455
- const canMakeChild = canDrop(e.dataTransfer, itemTarget, tree);
1467
+ const canBecomeSibling = parentTarget && canDrop(dataTransfer, parentTarget, tree);
1468
+ const canMakeChild = canDrop(dataTransfer, itemTarget, tree);
1456
1469
  const placement = getTargetPlacement(e, item, tree, canMakeChild);
1457
1470
  if (!canReorder && parent && canBecomeSibling && placement.type !== 2 /* MakeChild */) {
1458
1471
  if (draggedItems == null ? void 0 : draggedItems.some((item2) => item2.isDescendentOf(parent.getId()))) {
@@ -1461,7 +1474,7 @@ var getDragTarget = (e, item, tree, canReorder = tree.getConfig().canReorder) =>
1461
1474
  return parentTarget;
1462
1475
  }
1463
1476
  if (!canReorder && parent && !canBecomeSibling) {
1464
- return getDragTarget(e, parent, tree, false);
1477
+ return getDragTarget(e, parent, tree, hasDataTransferPayload, false);
1465
1478
  }
1466
1479
  if (!parent) {
1467
1480
  return itemTarget;
@@ -1470,7 +1483,7 @@ var getDragTarget = (e, item, tree, canReorder = tree.getConfig().canReorder) =>
1470
1483
  return itemTarget;
1471
1484
  }
1472
1485
  if (!canBecomeSibling) {
1473
- return getDragTarget(e, parent, tree, false);
1486
+ return getDragTarget(e, parent, tree, hasDataTransferPayload, false);
1474
1487
  }
1475
1488
  if (placement.type === 3 /* Reparent */) {
1476
1489
  return getReparentTarget(item, placement.reparentLevel, draggedItems);
@@ -1607,36 +1620,8 @@ var dragAndDropFeature = {
1607
1620
  }
1608
1621
  },
1609
1622
  itemInstance: {
1610
- getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
1611
- draggable: true,
1623
+ getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues(__spreadValues({}, prev == null ? void 0 : prev()), tree.getConfig().seperateDragHandle ? {} : item.getDragHandleProps()), {
1612
1624
  onDragEnter: (e) => e.preventDefault(),
1613
- onDragStart: (e) => {
1614
- var _a, _b, _c, _d;
1615
- const selectedItems = tree.getSelectedItems ? tree.getSelectedItems() : [tree.getFocusedItem()];
1616
- const items = selectedItems.includes(item) ? selectedItems : [item];
1617
- const config = tree.getConfig();
1618
- if (!selectedItems.includes(item)) {
1619
- (_a = tree.setSelectedItems) == null ? void 0 : _a.call(tree, [item.getItemMeta().itemId]);
1620
- }
1621
- if (!((_c = (_b = config.canDrag) == null ? void 0 : _b.call(config, items)) != null ? _c : true)) {
1622
- e.preventDefault();
1623
- return;
1624
- }
1625
- if (config.setDragImage) {
1626
- const { imgElement, xOffset, yOffset } = config.setDragImage(items);
1627
- (_d = e.dataTransfer) == null ? void 0 : _d.setDragImage(imgElement, xOffset != null ? xOffset : 0, yOffset != null ? yOffset : 0);
1628
- }
1629
- if (config.createForeignDragObject && e.dataTransfer) {
1630
- const { format, data, dropEffect, effectAllowed } = config.createForeignDragObject(items);
1631
- e.dataTransfer.setData(format, data);
1632
- if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
1633
- if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
1634
- }
1635
- tree.applySubStateUpdate("dnd", {
1636
- draggedItems: items,
1637
- draggingOverItem: tree.getFocusedItem()
1638
- });
1639
- },
1640
1625
  onDragOver: (e) => {
1641
1626
  var _a, _b, _c;
1642
1627
  e.stopPropagation();
@@ -1652,12 +1637,12 @@ var dragAndDropFeature = {
1652
1637
  dataRef.current.lastDragCode = nextDragCode;
1653
1638
  dataRef.current.lastDragEnter = Date.now();
1654
1639
  handleAutoOpenFolder(dataRef, tree, item, placement);
1655
- const target = getDragTarget(e, item, tree);
1640
+ const target = getDragTarget(e, item, tree, false);
1656
1641
  if (!((_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems) && (!e.dataTransfer || !((_c = (_b = tree.getConfig()).canDragForeignDragObjectOver) == null ? void 0 : _c.call(_b, e.dataTransfer, target)))) {
1657
1642
  dataRef.current.lastAllowDrop = false;
1658
1643
  return;
1659
1644
  }
1660
- if (!canDrop(e.dataTransfer, target, tree)) {
1645
+ if (!canDrop(null, target, tree)) {
1661
1646
  dataRef.current.lastAllowDrop = false;
1662
1647
  return;
1663
1648
  }
@@ -1680,24 +1665,11 @@ var dragAndDropFeature = {
1680
1665
  }));
1681
1666
  }, 100);
1682
1667
  },
1683
- onDragEnd: (e) => {
1684
- var _a, _b;
1685
- const { onCompleteForeignDrop, canDragForeignDragObjectOver } = tree.getConfig();
1686
- const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1687
- if (((_b = e.dataTransfer) == null ? void 0 : _b.dropEffect) === "none" || !draggedItems) {
1688
- return;
1689
- }
1690
- const target = getDragTarget(e, item, tree);
1691
- if (canDragForeignDragObjectOver && e.dataTransfer && !canDragForeignDragObjectOver(e.dataTransfer, target)) {
1692
- return;
1693
- }
1694
- onCompleteForeignDrop == null ? void 0 : onCompleteForeignDrop(draggedItems);
1695
- },
1696
1668
  onDrop: (e) => __async(null, null, function* () {
1697
1669
  var _a, _b, _c;
1698
1670
  e.stopPropagation();
1699
1671
  const dataRef = tree.getDataRef();
1700
- const target = getDragTarget(e, item, tree);
1672
+ const target = getDragTarget(e, item, tree, true);
1701
1673
  const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1702
1674
  const isValidDrop = canDrop(e.dataTransfer, target, tree);
1703
1675
  tree.applySubStateUpdate("dnd", {
@@ -1713,11 +1685,57 @@ var dragAndDropFeature = {
1713
1685
  dataRef.current.lastDragCode = void 0;
1714
1686
  if (draggedItems) {
1715
1687
  yield (_b = config.onDrop) == null ? void 0 : _b.call(config, draggedItems, target);
1688
+ draggedItems[0].setFocused();
1716
1689
  } else if (e.dataTransfer) {
1717
1690
  yield (_c = config.onDropForeignDragObject) == null ? void 0 : _c.call(config, e.dataTransfer, target);
1718
1691
  }
1692
+ tree.applySubStateUpdate("dnd", null);
1693
+ tree.updateDomFocus();
1719
1694
  })
1720
1695
  }),
1696
+ getDragHandleProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
1697
+ draggable: true,
1698
+ onDragStart: (e) => {
1699
+ var _a, _b, _c, _d;
1700
+ const selectedItems = tree.getSelectedItems ? tree.getSelectedItems() : [tree.getFocusedItem()];
1701
+ const items = selectedItems.includes(item) ? selectedItems : [item];
1702
+ const config = tree.getConfig();
1703
+ if (!selectedItems.includes(item)) {
1704
+ (_a = tree.setSelectedItems) == null ? void 0 : _a.call(tree, [item.getItemMeta().itemId]);
1705
+ }
1706
+ if (!((_c = (_b = config.canDrag) == null ? void 0 : _b.call(config, items)) != null ? _c : true)) {
1707
+ e.preventDefault();
1708
+ return;
1709
+ }
1710
+ if (config.setDragImage) {
1711
+ const { imgElement, xOffset, yOffset } = config.setDragImage(items);
1712
+ (_d = e.dataTransfer) == null ? void 0 : _d.setDragImage(imgElement, xOffset != null ? xOffset : 0, yOffset != null ? yOffset : 0);
1713
+ }
1714
+ if (config.createForeignDragObject && e.dataTransfer) {
1715
+ const { format, data, dropEffect, effectAllowed } = config.createForeignDragObject(items);
1716
+ e.dataTransfer.setData(format, data);
1717
+ if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
1718
+ if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
1719
+ }
1720
+ tree.applySubStateUpdate("dnd", {
1721
+ draggedItems: items,
1722
+ draggingOverItem: tree.getFocusedItem()
1723
+ });
1724
+ },
1725
+ onDragEnd: (e) => {
1726
+ var _a, _b;
1727
+ const { onCompleteForeignDrop, canDragForeignDragObjectOver } = tree.getConfig();
1728
+ const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1729
+ if (((_b = e.dataTransfer) == null ? void 0 : _b.dropEffect) === "none" || !draggedItems) {
1730
+ return;
1731
+ }
1732
+ const target = getDragTarget(e, item, tree, false);
1733
+ if (canDragForeignDragObjectOver && e.dataTransfer && !canDragForeignDragObjectOver(e.dataTransfer, target)) {
1734
+ return;
1735
+ }
1736
+ onCompleteForeignDrop == null ? void 0 : onCompleteForeignDrop(draggedItems);
1737
+ }
1738
+ }),
1721
1739
  isDragTarget: ({ tree, item }) => {
1722
1740
  const target = tree.getDragTarget();
1723
1741
  return target ? target.item.getId() === item.getId() : false;
@@ -1941,6 +1959,7 @@ var keyboardDragAndDropFeature = {
1941
1959
  } else if (dataTransfer) {
1942
1960
  yield (_d = config.onDropForeignDragObject) == null ? void 0 : _d.call(config, dataTransfer, target);
1943
1961
  }
1962
+ tree.updateDomFocus();
1944
1963
  tree.applySubStateUpdate(
1945
1964
  "assistiveDndState",
1946
1965
  3 /* Completed */
@@ -2291,6 +2310,14 @@ var propMemoizationFeature = {
2291
2310
  (_e = (_d = dataRef.current.memo).item) != null ? _e : _d.item = {};
2292
2311
  return memoize(props, dataRef.current.memo.item);
2293
2312
  },
2313
+ getDragHandleProps: ({ item, prev }) => {
2314
+ var _a, _b, _c, _d, _e;
2315
+ const dataRef = item.getDataRef();
2316
+ const props = (_a = prev == null ? void 0 : prev()) != null ? _a : {};
2317
+ (_c = (_b = dataRef.current).memo) != null ? _c : _b.memo = {};
2318
+ (_e = (_d = dataRef.current.memo).drag) != null ? _e : _d.drag = {};
2319
+ return memoize(props, dataRef.current.memo.drag);
2320
+ },
2294
2321
  getRenameInputProps: ({ item, prev }) => {
2295
2322
  var _a, _b, _c, _d, _e;
2296
2323
  const dataRef = item.getDataRef();
package/dist/index.mjs CHANGED
@@ -81,6 +81,7 @@ var poll = (fn, interval = 100, timeout = 1e3) => new Promise((resolve) => {
81
81
  }, interval);
82
82
  clear = setTimeout(() => {
83
83
  clearInterval(i);
84
+ resolve();
84
85
  }, timeout);
85
86
  });
86
87
 
@@ -170,12 +171,16 @@ var treeFeature = {
170
171
  },
171
172
  updateDomFocus: ({ tree }) => {
172
173
  setTimeout(() => __async(null, null, function* () {
173
- var _a, _b;
174
+ var _a, _b, _c, _d, _e;
174
175
  const focusedItem = tree.getFocusedItem();
175
176
  (_b = (_a = tree.getConfig()).scrollToItem) == null ? void 0 : _b.call(_a, focusedItem);
176
- yield poll(() => focusedItem.getElement() !== null, 20);
177
+ yield poll(() => focusedItem.getElement() !== null, 20, 500);
177
178
  const focusedElement = focusedItem.getElement();
178
- if (!focusedElement) return;
179
+ if (!focusedElement) {
180
+ (_c = tree.getItems()[0]) == null ? void 0 : _c.setFocused();
181
+ (_e = (_d = tree.getItems()[0]) == null ? void 0 : _d.getElement()) == null ? void 0 : _e.focus();
182
+ return;
183
+ }
179
184
  focusedElement.focus();
180
185
  }));
181
186
  },
@@ -715,10 +720,15 @@ var selectionFeature = {
715
720
  const { selectedItems } = tree.getState();
716
721
  return selectedItems.includes(itemId);
717
722
  },
718
- selectUpTo: ({ tree, item }, ctrl) => {
723
+ selectUpTo: ({ tree, item, itemId }, ctrl) => {
719
724
  const indexA = item.getItemMeta().index;
720
- const { selectUpToAnchorId } = tree.getDataRef().current;
721
- const itemB = selectUpToAnchorId ? tree.getItemInstance(selectUpToAnchorId) : tree.getFocusedItem();
725
+ const dataRef = tree.getDataRef();
726
+ if (!dataRef.current.selectUpToAnchorId) {
727
+ dataRef.current.selectUpToAnchorId = itemId;
728
+ tree.setSelectedItems([itemId]);
729
+ return;
730
+ }
731
+ const itemB = tree.getItemInstance(dataRef.current.selectUpToAnchorId);
722
732
  const indexB = itemB.getItemMeta().index;
723
733
  const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
724
734
  const newSelectedItems = tree.getItems().slice(a, b + 1).map((treeItem) => treeItem.getItemMeta().itemId);
@@ -739,7 +749,7 @@ var selectionFeature = {
739
749
  item.select();
740
750
  }
741
751
  },
742
- getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
752
+ getProps: ({ tree, item, itemId, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
743
753
  "aria-selected": item.isSelected() ? "true" : "false",
744
754
  onClick: (e) => {
745
755
  var _a, _b;
@@ -748,10 +758,10 @@ var selectionFeature = {
748
758
  } else if (e.ctrlKey || e.metaKey) {
749
759
  item.toggleSelect();
750
760
  } else {
751
- tree.setSelectedItems([item.getItemMeta().itemId]);
761
+ tree.setSelectedItems([itemId]);
752
762
  }
753
763
  if (!e.shiftKey) {
754
- tree.getDataRef().current.selectUpToAnchorId = item.getId();
764
+ tree.getDataRef().current.selectUpToAnchorId = itemId;
755
765
  }
756
766
  (_b = (_a = prev == null ? void 0 : prev()) == null ? void 0 : _a.onClick) == null ? void 0 : _b.call(_a, e);
757
767
  }
@@ -1225,15 +1235,17 @@ var asyncDataLoaderFeature = {
1225
1235
  }
1226
1236
  yield loadChildrenIds(tree, itemId);
1227
1237
  }),
1228
- updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
1229
- const dataRef = tree.getDataRef();
1230
- dataRef.current.childrenIds[itemId] = childrenIds;
1231
- tree.rebuildTree();
1238
+ updateCachedChildrenIds: ({ tree, itemId }, childrenIds, skipUpdateTree) => {
1239
+ getDataRef(tree).current.childrenIds[itemId] = childrenIds;
1240
+ if (!skipUpdateTree) {
1241
+ tree.rebuildTree();
1242
+ }
1232
1243
  },
1233
- updateCachedData: ({ tree, itemId }, data) => {
1234
- const dataRef = tree.getDataRef();
1235
- dataRef.current.itemData[itemId] = data;
1236
- tree.rebuildTree();
1244
+ updateCachedData: ({ tree, itemId }, data, skipUpdateTree) => {
1245
+ getDataRef(tree).current.itemData[itemId] = data;
1246
+ if (!skipUpdateTree) {
1247
+ tree.rebuildTree();
1248
+ }
1237
1249
  },
1238
1250
  hasLoadedData: ({ tree, itemId }) => {
1239
1251
  const dataRef = tree.getDataRef();
@@ -1400,15 +1412,16 @@ var getReparentTarget = (item, reparentLevel, draggedItems) => {
1400
1412
  dragLineLevel: reparentLevel
1401
1413
  };
1402
1414
  };
1403
- var getDragTarget = (e, item, tree, canReorder = tree.getConfig().canReorder) => {
1415
+ var getDragTarget = (e, item, tree, hasDataTransferPayload, canReorder = tree.getConfig().canReorder) => {
1404
1416
  var _a;
1417
+ const dataTransfer = hasDataTransferPayload ? e.dataTransfer : null;
1405
1418
  const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1406
1419
  const itemMeta = item.getItemMeta();
1407
1420
  const parent = item.getParent();
1408
1421
  const itemTarget = { item };
1409
1422
  const parentTarget = parent ? { item: parent } : null;
1410
- const canBecomeSibling = parentTarget && canDrop(e.dataTransfer, parentTarget, tree);
1411
- const canMakeChild = canDrop(e.dataTransfer, itemTarget, tree);
1423
+ const canBecomeSibling = parentTarget && canDrop(dataTransfer, parentTarget, tree);
1424
+ const canMakeChild = canDrop(dataTransfer, itemTarget, tree);
1412
1425
  const placement = getTargetPlacement(e, item, tree, canMakeChild);
1413
1426
  if (!canReorder && parent && canBecomeSibling && placement.type !== 2 /* MakeChild */) {
1414
1427
  if (draggedItems == null ? void 0 : draggedItems.some((item2) => item2.isDescendentOf(parent.getId()))) {
@@ -1417,7 +1430,7 @@ var getDragTarget = (e, item, tree, canReorder = tree.getConfig().canReorder) =>
1417
1430
  return parentTarget;
1418
1431
  }
1419
1432
  if (!canReorder && parent && !canBecomeSibling) {
1420
- return getDragTarget(e, parent, tree, false);
1433
+ return getDragTarget(e, parent, tree, hasDataTransferPayload, false);
1421
1434
  }
1422
1435
  if (!parent) {
1423
1436
  return itemTarget;
@@ -1426,7 +1439,7 @@ var getDragTarget = (e, item, tree, canReorder = tree.getConfig().canReorder) =>
1426
1439
  return itemTarget;
1427
1440
  }
1428
1441
  if (!canBecomeSibling) {
1429
- return getDragTarget(e, parent, tree, false);
1442
+ return getDragTarget(e, parent, tree, hasDataTransferPayload, false);
1430
1443
  }
1431
1444
  if (placement.type === 3 /* Reparent */) {
1432
1445
  return getReparentTarget(item, placement.reparentLevel, draggedItems);
@@ -1563,36 +1576,8 @@ var dragAndDropFeature = {
1563
1576
  }
1564
1577
  },
1565
1578
  itemInstance: {
1566
- getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
1567
- draggable: true,
1579
+ getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues(__spreadValues({}, prev == null ? void 0 : prev()), tree.getConfig().seperateDragHandle ? {} : item.getDragHandleProps()), {
1568
1580
  onDragEnter: (e) => e.preventDefault(),
1569
- onDragStart: (e) => {
1570
- var _a, _b, _c, _d;
1571
- const selectedItems = tree.getSelectedItems ? tree.getSelectedItems() : [tree.getFocusedItem()];
1572
- const items = selectedItems.includes(item) ? selectedItems : [item];
1573
- const config = tree.getConfig();
1574
- if (!selectedItems.includes(item)) {
1575
- (_a = tree.setSelectedItems) == null ? void 0 : _a.call(tree, [item.getItemMeta().itemId]);
1576
- }
1577
- if (!((_c = (_b = config.canDrag) == null ? void 0 : _b.call(config, items)) != null ? _c : true)) {
1578
- e.preventDefault();
1579
- return;
1580
- }
1581
- if (config.setDragImage) {
1582
- const { imgElement, xOffset, yOffset } = config.setDragImage(items);
1583
- (_d = e.dataTransfer) == null ? void 0 : _d.setDragImage(imgElement, xOffset != null ? xOffset : 0, yOffset != null ? yOffset : 0);
1584
- }
1585
- if (config.createForeignDragObject && e.dataTransfer) {
1586
- const { format, data, dropEffect, effectAllowed } = config.createForeignDragObject(items);
1587
- e.dataTransfer.setData(format, data);
1588
- if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
1589
- if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
1590
- }
1591
- tree.applySubStateUpdate("dnd", {
1592
- draggedItems: items,
1593
- draggingOverItem: tree.getFocusedItem()
1594
- });
1595
- },
1596
1581
  onDragOver: (e) => {
1597
1582
  var _a, _b, _c;
1598
1583
  e.stopPropagation();
@@ -1608,12 +1593,12 @@ var dragAndDropFeature = {
1608
1593
  dataRef.current.lastDragCode = nextDragCode;
1609
1594
  dataRef.current.lastDragEnter = Date.now();
1610
1595
  handleAutoOpenFolder(dataRef, tree, item, placement);
1611
- const target = getDragTarget(e, item, tree);
1596
+ const target = getDragTarget(e, item, tree, false);
1612
1597
  if (!((_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems) && (!e.dataTransfer || !((_c = (_b = tree.getConfig()).canDragForeignDragObjectOver) == null ? void 0 : _c.call(_b, e.dataTransfer, target)))) {
1613
1598
  dataRef.current.lastAllowDrop = false;
1614
1599
  return;
1615
1600
  }
1616
- if (!canDrop(e.dataTransfer, target, tree)) {
1601
+ if (!canDrop(null, target, tree)) {
1617
1602
  dataRef.current.lastAllowDrop = false;
1618
1603
  return;
1619
1604
  }
@@ -1636,24 +1621,11 @@ var dragAndDropFeature = {
1636
1621
  }));
1637
1622
  }, 100);
1638
1623
  },
1639
- onDragEnd: (e) => {
1640
- var _a, _b;
1641
- const { onCompleteForeignDrop, canDragForeignDragObjectOver } = tree.getConfig();
1642
- const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1643
- if (((_b = e.dataTransfer) == null ? void 0 : _b.dropEffect) === "none" || !draggedItems) {
1644
- return;
1645
- }
1646
- const target = getDragTarget(e, item, tree);
1647
- if (canDragForeignDragObjectOver && e.dataTransfer && !canDragForeignDragObjectOver(e.dataTransfer, target)) {
1648
- return;
1649
- }
1650
- onCompleteForeignDrop == null ? void 0 : onCompleteForeignDrop(draggedItems);
1651
- },
1652
1624
  onDrop: (e) => __async(null, null, function* () {
1653
1625
  var _a, _b, _c;
1654
1626
  e.stopPropagation();
1655
1627
  const dataRef = tree.getDataRef();
1656
- const target = getDragTarget(e, item, tree);
1628
+ const target = getDragTarget(e, item, tree, true);
1657
1629
  const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1658
1630
  const isValidDrop = canDrop(e.dataTransfer, target, tree);
1659
1631
  tree.applySubStateUpdate("dnd", {
@@ -1669,11 +1641,57 @@ var dragAndDropFeature = {
1669
1641
  dataRef.current.lastDragCode = void 0;
1670
1642
  if (draggedItems) {
1671
1643
  yield (_b = config.onDrop) == null ? void 0 : _b.call(config, draggedItems, target);
1644
+ draggedItems[0].setFocused();
1672
1645
  } else if (e.dataTransfer) {
1673
1646
  yield (_c = config.onDropForeignDragObject) == null ? void 0 : _c.call(config, e.dataTransfer, target);
1674
1647
  }
1648
+ tree.applySubStateUpdate("dnd", null);
1649
+ tree.updateDomFocus();
1675
1650
  })
1676
1651
  }),
1652
+ getDragHandleProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
1653
+ draggable: true,
1654
+ onDragStart: (e) => {
1655
+ var _a, _b, _c, _d;
1656
+ const selectedItems = tree.getSelectedItems ? tree.getSelectedItems() : [tree.getFocusedItem()];
1657
+ const items = selectedItems.includes(item) ? selectedItems : [item];
1658
+ const config = tree.getConfig();
1659
+ if (!selectedItems.includes(item)) {
1660
+ (_a = tree.setSelectedItems) == null ? void 0 : _a.call(tree, [item.getItemMeta().itemId]);
1661
+ }
1662
+ if (!((_c = (_b = config.canDrag) == null ? void 0 : _b.call(config, items)) != null ? _c : true)) {
1663
+ e.preventDefault();
1664
+ return;
1665
+ }
1666
+ if (config.setDragImage) {
1667
+ const { imgElement, xOffset, yOffset } = config.setDragImage(items);
1668
+ (_d = e.dataTransfer) == null ? void 0 : _d.setDragImage(imgElement, xOffset != null ? xOffset : 0, yOffset != null ? yOffset : 0);
1669
+ }
1670
+ if (config.createForeignDragObject && e.dataTransfer) {
1671
+ const { format, data, dropEffect, effectAllowed } = config.createForeignDragObject(items);
1672
+ e.dataTransfer.setData(format, data);
1673
+ if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
1674
+ if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
1675
+ }
1676
+ tree.applySubStateUpdate("dnd", {
1677
+ draggedItems: items,
1678
+ draggingOverItem: tree.getFocusedItem()
1679
+ });
1680
+ },
1681
+ onDragEnd: (e) => {
1682
+ var _a, _b;
1683
+ const { onCompleteForeignDrop, canDragForeignDragObjectOver } = tree.getConfig();
1684
+ const draggedItems = (_a = tree.getState().dnd) == null ? void 0 : _a.draggedItems;
1685
+ if (((_b = e.dataTransfer) == null ? void 0 : _b.dropEffect) === "none" || !draggedItems) {
1686
+ return;
1687
+ }
1688
+ const target = getDragTarget(e, item, tree, false);
1689
+ if (canDragForeignDragObjectOver && e.dataTransfer && !canDragForeignDragObjectOver(e.dataTransfer, target)) {
1690
+ return;
1691
+ }
1692
+ onCompleteForeignDrop == null ? void 0 : onCompleteForeignDrop(draggedItems);
1693
+ }
1694
+ }),
1677
1695
  isDragTarget: ({ tree, item }) => {
1678
1696
  const target = tree.getDragTarget();
1679
1697
  return target ? target.item.getId() === item.getId() : false;
@@ -1897,6 +1915,7 @@ var keyboardDragAndDropFeature = {
1897
1915
  } else if (dataTransfer) {
1898
1916
  yield (_d = config.onDropForeignDragObject) == null ? void 0 : _d.call(config, dataTransfer, target);
1899
1917
  }
1918
+ tree.updateDomFocus();
1900
1919
  tree.applySubStateUpdate(
1901
1920
  "assistiveDndState",
1902
1921
  3 /* Completed */
@@ -2247,6 +2266,14 @@ var propMemoizationFeature = {
2247
2266
  (_e = (_d = dataRef.current.memo).item) != null ? _e : _d.item = {};
2248
2267
  return memoize(props, dataRef.current.memo.item);
2249
2268
  },
2269
+ getDragHandleProps: ({ item, prev }) => {
2270
+ var _a, _b, _c, _d, _e;
2271
+ const dataRef = item.getDataRef();
2272
+ const props = (_a = prev == null ? void 0 : prev()) != null ? _a : {};
2273
+ (_c = (_b = dataRef.current).memo) != null ? _c : _b.memo = {};
2274
+ (_e = (_d = dataRef.current.memo).drag) != null ? _e : _d.drag = {};
2275
+ return memoize(props, dataRef.current.memo.drag);
2276
+ },
2250
2277
  getRenameInputProps: ({ item, prev }) => {
2251
2278
  var _a, _b, _c, _d, _e;
2252
2279
  const dataRef = item.getDataRef();
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "checkbox",
14
14
  "hook"
15
15
  ],
16
- "version": "1.6.0",
16
+ "version": "1.6.2",
17
17
  "main": "dist/index.d.ts",
18
18
  "module": "dist/index.mjs",
19
19
  "types": "dist/index.d.mts",
@@ -180,15 +180,21 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
180
180
  }
181
181
  await loadChildrenIds(tree, itemId);
182
182
  },
183
- updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
184
- const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
185
- dataRef.current.childrenIds[itemId] = childrenIds;
186
- tree.rebuildTree();
183
+ updateCachedChildrenIds: (
184
+ { tree, itemId },
185
+ childrenIds,
186
+ skipUpdateTree,
187
+ ) => {
188
+ getDataRef(tree).current.childrenIds[itemId] = childrenIds;
189
+ if (!skipUpdateTree) {
190
+ tree.rebuildTree();
191
+ }
187
192
  },
188
- updateCachedData: ({ tree, itemId }, data) => {
189
- const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
190
- dataRef.current.itemData[itemId] = data;
191
- tree.rebuildTree();
193
+ updateCachedData: ({ tree, itemId }, data, skipUpdateTree) => {
194
+ getDataRef(tree).current.itemData[itemId] = data;
195
+ if (!skipUpdateTree) {
196
+ tree.rebuildTree();
197
+ }
192
198
  },
193
199
  hasLoadedData: ({ tree, itemId }) => {
194
200
  const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
@@ -39,6 +39,7 @@ export type AsyncDataLoaderFeatureDef<T> = {
39
39
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
40
40
  loadItemData: (itemId: string) => Promise<T>;
41
41
  loadChildrenIds: (itemId: string) => Promise<string[]>;
42
+
42
43
  /* idea: recursiveLoadItems: (itemId: string, cancelToken?: { current: boolean }, onLoad: (itemIds: string[]) => void) => Promise<T[]> */
43
44
  };
44
45
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
@@ -52,9 +53,16 @@ export type AsyncDataLoaderFeatureDef<T> = {
52
53
  * the tree will continue to display the old data until the new data has loaded. */
53
54
  invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
54
55
 
55
- /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
56
- updateCachedData: (data: T | undefined) => void;
57
- updateCachedChildrenIds: (childrenIds: string[]) => void;
56
+ /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch.
57
+ * @param skipUpdateTree If true, the tree will not rebuild the tree structure cache afterwards by calling `tree.rebuildTree()`. */
58
+ updateCachedData: (data: T | undefined, skipUpdateTree?: boolean) => void;
59
+
60
+ /** @param skipUpdateTree If true, the tree will not rebuild the tree structure cache afterwards by calling `tree.rebuildTree()`. */
61
+ updateCachedChildrenIds: (
62
+ childrenIds: string[],
63
+ skipUpdateTree?: boolean,
64
+ ) => void;
65
+
58
66
  hasLoadedData: () => boolean;
59
67
  isLoading: () => boolean;
60
68
  };
@@ -328,8 +328,7 @@ describe("core-feature/drag-and-drop", () => {
328
328
  "onDropForeignDragObject",
329
329
  );
330
330
  const event = tree.createBottomDragEvent(2);
331
- tree.setElementBoundingBox("x212");
332
- tree.setElementBoundingBox("x213");
331
+ tree.setElementBoundingBox("x112");
333
332
  tree.do.dragOver("x112", event);
334
333
  tree.do.drop("x112", event);
335
334
  expect(onDropForeignDragObject).toHaveBeenCalledWith(
@@ -357,6 +356,38 @@ describe("core-feature/drag-and-drop", () => {
357
356
  tree.do.drop("x11", event);
358
357
  expect(onDropForeignDragObject).not.toHaveBeenCalled();
359
358
  });
359
+
360
+ it("calls canDragForeignDragObjectOver in drag over events, not canDropForeignDragObject", () => {
361
+ const canDropForeignDragObject = tree
362
+ .mockedHandler("canDropForeignDragObject")
363
+ .mockReturnValue(true);
364
+ const canDragForeignDragObjectOver = tree
365
+ .mockedHandler("canDragForeignDragObjectOver")
366
+ .mockReturnValue(true);
367
+ const event = TestTree.dragEvent();
368
+ tree.do.dragOver("x11", event);
369
+ expect(canDragForeignDragObjectOver).toHaveBeenCalledWith(
370
+ event.dataTransfer,
371
+ { item: tree.item("x11") },
372
+ );
373
+ expect(canDropForeignDragObject).not.toHaveBeenCalled();
374
+ });
375
+
376
+ it("calls canDropForeignDragObject in drop events, not canDragForeignDragObjectOver", () => {
377
+ const canDropForeignDragObject = tree
378
+ .mockedHandler("canDropForeignDragObject")
379
+ .mockReturnValue(true);
380
+ const canDragForeignDragObjectOver = tree
381
+ .mockedHandler("canDragForeignDragObjectOver")
382
+ .mockReturnValue(true);
383
+ const event = TestTree.dragEvent();
384
+ tree.do.drop("x11", event);
385
+ expect(canDropForeignDragObject).toHaveBeenCalledWith(
386
+ event.dataTransfer,
387
+ { item: tree.item("x11") },
388
+ );
389
+ expect(canDragForeignDragObjectOver).not.toHaveBeenCalled();
390
+ });
360
391
  });
361
392
 
362
393
  describe("with insertion handlers", () => {
@@ -698,6 +729,54 @@ describe("core-feature/drag-and-drop", () => {
698
729
  });
699
730
  });
700
731
 
732
+ describe("seperateDragHandle", () => {
733
+ it("includes all drag handlers in getProps() when seperateDragHandle is false", () => {
734
+ const props = tree.instance.getItemInstance("x111").getProps();
735
+ expect(props.draggable).toBe(true);
736
+ expect(props.onDragStart).toBeDefined();
737
+ expect(props.onDragEnd).toBeDefined();
738
+ expect(props.onDragEnter).toBeDefined();
739
+ expect(props.onDragOver).toBeDefined();
740
+ expect(props.onDragLeave).toBeDefined();
741
+ expect(props.onDrop).toBeDefined();
742
+ });
743
+
744
+ it("includes drag handle handlers in getDragHandleProps() when seperateDragHandle is false", () => {
745
+ const props = tree.instance
746
+ .getItemInstance("x111")
747
+ .getDragHandleProps();
748
+ expect(props.draggable).toBe(true);
749
+ expect(props.onDragStart).toBeDefined();
750
+ expect(props.onDragEnd).toBeDefined();
751
+ });
752
+
753
+ it("excludes drag handle handlers from getProps() but includes drop handlers when seperateDragHandle is true", async () => {
754
+ const testTree = await tree
755
+ .with({ seperateDragHandle: true })
756
+ .createTestCaseTree();
757
+ const props = testTree.instance.getItemInstance("x111").getProps();
758
+ expect(props.draggable).toBeUndefined();
759
+ expect(props.onDragStart).toBeUndefined();
760
+ expect(props.onDragEnd).toBeUndefined();
761
+ expect(props.onDragEnter).toBeDefined();
762
+ expect(props.onDragOver).toBeDefined();
763
+ expect(props.onDragLeave).toBeDefined();
764
+ expect(props.onDrop).toBeDefined();
765
+ });
766
+
767
+ it("includes drag handle handlers in getDragHandleProps() when seperateDragHandle is true", async () => {
768
+ const testTree = await tree
769
+ .with({ seperateDragHandle: true })
770
+ .createTestCaseTree();
771
+ const props = testTree.instance
772
+ .getItemInstance("x111")
773
+ .getDragHandleProps();
774
+ expect(props.draggable).toBe(true);
775
+ expect(props.onDragStart).toBeDefined();
776
+ expect(props.onDragEnd).toBeDefined();
777
+ });
778
+ });
779
+
701
780
  describe("retains last drag state with dragcode", () => {
702
781
  it("uses constant number of calls to canDrop", () => {
703
782
  const canDrop = tree.mockedHandler("canDrop").mockReturnValue(true);
@@ -178,47 +178,10 @@ export const dragAndDropFeature: FeatureImplementation = {
178
178
  itemInstance: {
179
179
  getProps: ({ tree, item, prev }) => ({
180
180
  ...prev?.(),
181
-
182
- draggable: true,
181
+ ...(tree.getConfig().seperateDragHandle ? {} : item.getDragHandleProps()),
183
182
 
184
183
  onDragEnter: (e: DragEvent) => e.preventDefault(),
185
184
 
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
185
  onDragOver: (e: DragEvent) => {
223
186
  e.stopPropagation(); // don't bubble up to container dragover
224
187
  const dataRef = tree.getDataRef<DndDataRef>();
@@ -236,7 +199,7 @@ export const dragAndDropFeature: FeatureImplementation = {
236
199
 
237
200
  handleAutoOpenFolder(dataRef, tree, item, placement);
238
201
 
239
- const target = getDragTarget(e, item, tree);
202
+ const target = getDragTarget(e, item, tree, false);
240
203
 
241
204
  if (
242
205
  !tree.getState().dnd?.draggedItems &&
@@ -249,7 +212,8 @@ export const dragAndDropFeature: FeatureImplementation = {
249
212
  return;
250
213
  }
251
214
 
252
- if (!canDrop(e.dataTransfer, target, tree)) {
215
+ // dataTransfer.payload is not accessible in onDragOver, so just skip entirely here. It'll be checked again in onDrop
216
+ if (!canDrop(null, target, tree)) {
253
217
  dataRef.current.lastAllowDrop = false;
254
218
  return;
255
219
  }
@@ -276,31 +240,10 @@ export const dragAndDropFeature: FeatureImplementation = {
276
240
  }, 100);
277
241
  },
278
242
 
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
243
  onDrop: async (e: DragEvent) => {
301
244
  e.stopPropagation();
302
245
  const dataRef = tree.getDataRef<DndDataRef>();
303
- const target = getDragTarget(e, item, tree);
246
+ const target = getDragTarget(e, item, tree, true);
304
247
  const draggedItems = tree.getState().dnd?.draggedItems;
305
248
  const isValidDrop = canDrop(e.dataTransfer, target, tree);
306
249
 
@@ -321,9 +264,76 @@ export const dragAndDropFeature: FeatureImplementation = {
321
264
 
322
265
  if (draggedItems) {
323
266
  await config.onDrop?.(draggedItems, target);
267
+ draggedItems[0].setFocused();
324
268
  } else if (e.dataTransfer) {
325
269
  await config.onDropForeignDragObject?.(e.dataTransfer, target);
326
270
  }
271
+
272
+ tree.applySubStateUpdate("dnd", null);
273
+ tree.updateDomFocus();
274
+ },
275
+ }),
276
+
277
+ getDragHandleProps: ({ tree, item, prev }) => ({
278
+ ...prev?.(),
279
+
280
+ draggable: true,
281
+
282
+ onDragStart: (e: DragEvent) => {
283
+ const selectedItems = tree.getSelectedItems
284
+ ? tree.getSelectedItems()
285
+ : [tree.getFocusedItem()];
286
+ const items = selectedItems.includes(item) ? selectedItems : [item];
287
+ const config = tree.getConfig();
288
+
289
+ if (!selectedItems.includes(item)) {
290
+ tree.setSelectedItems?.([item.getItemMeta().itemId]);
291
+ }
292
+
293
+ if (!(config.canDrag?.(items) ?? true)) {
294
+ e.preventDefault();
295
+ return;
296
+ }
297
+
298
+ if (config.setDragImage) {
299
+ const { imgElement, xOffset, yOffset } = config.setDragImage(items);
300
+ e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
301
+ }
302
+
303
+ if (config.createForeignDragObject && e.dataTransfer) {
304
+ const { format, data, dropEffect, effectAllowed } =
305
+ config.createForeignDragObject(items);
306
+ e.dataTransfer.setData(format, data);
307
+
308
+ if (dropEffect) e.dataTransfer.dropEffect = dropEffect;
309
+ if (effectAllowed) e.dataTransfer.effectAllowed = effectAllowed;
310
+ }
311
+
312
+ tree.applySubStateUpdate("dnd", {
313
+ draggedItems: items,
314
+ draggingOverItem: tree.getFocusedItem(),
315
+ });
316
+ },
317
+
318
+ onDragEnd: (e: DragEvent) => {
319
+ const { onCompleteForeignDrop, canDragForeignDragObjectOver } =
320
+ tree.getConfig();
321
+ const draggedItems = tree.getState().dnd?.draggedItems;
322
+
323
+ if (e.dataTransfer?.dropEffect === "none" || !draggedItems) {
324
+ return;
325
+ }
326
+
327
+ const target = getDragTarget(e, item, tree, false);
328
+ if (
329
+ canDragForeignDragObjectOver &&
330
+ e.dataTransfer &&
331
+ !canDragForeignDragObjectOver(e.dataTransfer, target)
332
+ ) {
333
+ return;
334
+ }
335
+
336
+ onCompleteForeignDrop?.(draggedItems);
327
337
  },
328
338
  }),
329
339
 
@@ -97,6 +97,9 @@ 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;
100
103
  };
101
104
  treeInstance: {
102
105
  getDragTarget: () => DragTarget<T> | null;
@@ -118,6 +121,10 @@ export type DragAndDropFeatureDef<T> = {
118
121
  isDragTargetAbove: () => boolean;
119
122
  isDragTargetBelow: () => boolean;
120
123
  isDraggingOver: () => boolean;
124
+
125
+ /** Note that `item.getProps()` already passes in all drag event handlers by default. Set `seperateDragHandle` to true to
126
+ * disable the default behavior and use this on the handler element instead. */
127
+ getDragHandleProps: () => Record<string, any>;
121
128
  };
122
129
  hotkeys: never;
123
130
  };
@@ -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) {
@@ -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
  };
@@ -49,13 +49,17 @@ export const selectionFeature: FeatureImplementation = {
49
49
  return selectedItems.includes(itemId);
50
50
  },
51
51
 
52
- selectUpTo: ({ tree, item }, ctrl: boolean) => {
52
+ selectUpTo: ({ tree, item, itemId }, ctrl: boolean) => {
53
53
  const indexA = item.getItemMeta().index;
54
- const { selectUpToAnchorId } =
55
- tree.getDataRef<SelectionDataRef>().current;
56
- const itemB = selectUpToAnchorId
57
- ? tree.getItemInstance(selectUpToAnchorId)
58
- : tree.getFocusedItem();
54
+ const dataRef = tree.getDataRef<SelectionDataRef>();
55
+
56
+ if (!dataRef.current.selectUpToAnchorId) {
57
+ dataRef.current.selectUpToAnchorId = itemId;
58
+ tree.setSelectedItems([itemId]);
59
+ return;
60
+ }
61
+
62
+ const itemB = tree.getItemInstance(dataRef.current.selectUpToAnchorId);
59
63
  const indexB = itemB.getItemMeta().index;
60
64
  const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
61
65
  const newSelectedItems = tree
@@ -83,7 +87,7 @@ export const selectionFeature: FeatureImplementation = {
83
87
  }
84
88
  },
85
89
 
86
- getProps: ({ tree, item, prev }) => ({
90
+ getProps: ({ tree, item, itemId, prev }) => ({
87
91
  ...prev?.(),
88
92
  "aria-selected": item.isSelected() ? "true" : "false",
89
93
  onClick: (e: MouseEvent) => {
@@ -92,12 +96,12 @@ export const selectionFeature: FeatureImplementation = {
92
96
  } else if (e.ctrlKey || e.metaKey) {
93
97
  item.toggleSelect();
94
98
  } else {
95
- tree.setSelectedItems([item.getItemMeta().itemId]);
99
+ tree.setSelectedItems([itemId]);
96
100
  }
97
101
 
98
102
  if (!e.shiftKey) {
99
103
  tree.getDataRef<SelectionDataRef>().current.selectUpToAnchorId =
100
- item.getId();
104
+ itemId;
101
105
  }
102
106
 
103
107
  prev?.()?.onClick?.(e);
@@ -176,9 +176,8 @@ describe("core-feature/selections", () => {
176
176
 
177
177
  it("should handle selectUpTo without ctrl", () => {
178
178
  const setSelectedItems = tree.mockedHandler("setSelectedItems");
179
- tree.instance.getItemInstance("x111").toggleSelect();
180
- tree.instance.getItemInstance("x112").toggleSelect();
181
- tree.instance.getItemInstance("x112").setFocused();
179
+ tree.do.ctrlSelectItem("x111");
180
+ tree.do.ctrlSelectItem("x112");
182
181
  tree.instance.getItemInstance("x114").selectUpTo(false);
183
182
  expect(setSelectedItems).toHaveBeenCalledWith([
184
183
  "x112",
@@ -190,9 +189,8 @@ describe("core-feature/selections", () => {
190
189
 
191
190
  it("should handle selectUpTo with ctrl", () => {
192
191
  const setSelectedItems = tree.mockedHandler("setSelectedItems");
193
- tree.instance.getItemInstance("x111").toggleSelect();
194
- tree.instance.getItemInstance("x112").toggleSelect();
195
- tree.instance.getItemInstance("x112").setFocused();
192
+ tree.do.ctrlSelectItem("x111");
193
+ tree.do.ctrlSelectItem("x112");
196
194
  tree.instance.getItemInstance("x114").selectUpTo(true);
197
195
  expect(setSelectedItems).toHaveBeenCalledWith([
198
196
  "x111",
@@ -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
  });