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( <>