From 4208ec4b1bbf28b04b92c5709e8438d42b52010b Mon Sep 17 00:00:00 2001
From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Sun, 7 Jun 2026 23:42:22 -0700
Subject: [PATCH] fix: prefer ancestor drop target over nearest-by-distance in
DragManager
---
packages/react-aria/src/dnd/DragManager.ts | 7 +++-
packages/react-aria/test/dnd/dnd.test.js | 49 ++++++++++++++++++++++
2 files changed, 55 insertions(+), 1 deletion(-)
diff --git a/packages/react-aria/src/dnd/DragManager.ts b/packages/react-aria/src/dnd/DragManager.ts
index 637cdf0daf6..8d915d67286 100644
--- a/packages/react-aria/src/dnd/DragManager.ts
+++ b/packages/react-aria/src/dnd/DragManager.ts
@@ -529,8 +529,13 @@ class DragSession {
let minDistance = Infinity;
let nearest = -1;
+ let ancestor = -1;
for (let i = 0; i < this.validDropTargets.length; i++) {
let dropTarget = this.validDropTargets[i];
+ if (ancestor < 0 && nodeContains(dropTarget.element, this.dragTarget.element)) {
+ ancestor = i;
+ }
+
let rect = dropTarget.element.getBoundingClientRect();
let dx = rect.left - dragTargetRect.left;
let dy = rect.top - dragTargetRect.top;
@@ -541,7 +546,7 @@ class DragSession {
}
}
- return nearest;
+ return ancestor >= 0 ? ancestor : nearest;
}
setCurrentDropTarget(dropTarget: DropTarget | null, item?: DroppableItem): void {
diff --git a/packages/react-aria/test/dnd/dnd.test.js b/packages/react-aria/test/dnd/dnd.test.js
index 848278cc8a3..fbc947796a3 100644
--- a/packages/react-aria/test/dnd/dnd.test.js
+++ b/packages/react-aria/test/dnd/dnd.test.js
@@ -1951,6 +1951,55 @@ describe('useDrag and useDrop', function () {
expect(document.activeElement).toBe(droppable);
});
+ it('should prefer an ancestor drop target over the nearest drop target', async () => {
+ let onDropEnter = jest.fn();
+ let onDropEnter2 = jest.fn();
+ let tree = render(
+ <>
+
+
+
+ Drop here 2
+ >
+ );
+
+ let draggable = tree.getByText('Drag me');
+ let droppable = draggable.parentElement;
+ let droppable2 = tree.getByText('Drop here 2');
+ let rect = (left, top) => ({
+ left,
+ top,
+ x: left,
+ y: top,
+ width: 100,
+ height: 50
+ });
+
+ jest.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function () {
+ if (this === droppable) {
+ return rect(1000, 0);
+ }
+
+ if (this === droppable2) {
+ return rect(10, 0);
+ }
+
+ return rect(0, 0);
+ });
+
+ await user.tab();
+ await user.tab();
+ expect(document.activeElement).toBe(draggable);
+
+ await user.keyboard('{Enter}');
+ act(() => jest.runAllTimers());
+ expect(document.activeElement).toBe(droppable);
+ expect(droppable).toHaveAttribute('data-droptarget', 'true');
+ expect(droppable2).toHaveAttribute('data-droptarget', 'false');
+ expect(onDropEnter).toHaveBeenCalledTimes(1);
+ expect(onDropEnter2).not.toHaveBeenCalled();
+ });
+
it('should cancel the drag when pressing the escape key', async () => {
let tree = render(
<>