From 23d3f387808c0aa28e6f853b2d007cdb0b4a24be Mon Sep 17 00:00:00 2001 From: Sandro Roth <16229645+rothsandro@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:57:49 +0100 Subject: [PATCH 1/2] Add failing test --- .../react-aria-components/test/Tree.test.tsx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/react-aria-components/test/Tree.test.tsx b/packages/react-aria-components/test/Tree.test.tsx index 3de2839df18..6af91779418 100644 --- a/packages/react-aria-components/test/Tree.test.tsx +++ b/packages/react-aria-components/test/Tree.test.tsx @@ -2045,6 +2045,65 @@ describe('Tree', () => { expect(getItems).toHaveBeenCalledTimes(1); expect(getItems).toHaveBeenCalledWith(new Set(['projects', 'reports'])); }); + + it('should allow dropping a child before its parent when parent is the only root node', () => { + let onMove = jest.fn(); + let items = [ + {id: 'projects', name: 'Projects', childItems: [ + {id: 'project-1', name: 'Project 1'} + ]} + ]; + + let realGetBoundingClientRect = window.HTMLElement.prototype.getBoundingClientRect; + let rectSpy = jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement): DOMRect { + let rect = realGetBoundingClientRect.call(this); + + if (this.getAttribute('role') === 'treegrid') { + return {...rect, top: 0, left: 0, width: 100, height: 100, bottom: 100, right: 100}; + } + + if (this.getAttribute('data-key') === 'projects') { + return {...rect, top: 10, left: 0, width: 100, height: 20, bottom: 30, right: 100}; + } + + if (this.getAttribute('data-key') === 'project-1') { + return {...rect, top: 30, left: 0, width: 100, height: 20, bottom: 50, right: 100}; + } + + return rect; + }); + + function SingleRootDraggableTree() { + let {dragAndDropHooks} = useDragAndDrop({ + getItems: (keys) => [...keys].map((key) => ({'text/plain': key.toString()})), + onMove + }); + + return ; + } + + let {getByRole} = render(); + + let tree = getByRole('treegrid'); + let project1Row = getByRole('row', {name: 'Project 1'}); + let dataTransfer = new DataTransfer(); + + fireEvent(project1Row, new DragEvent('dragstart', {dataTransfer, clientX: 50, clientY: 35})); + act(() => jest.runAllTimers()); + + fireEvent(tree, new DragEvent('dragenter', {dataTransfer, clientX: 50, clientY: 0})); + fireEvent(tree, new DragEvent('dragover', {dataTransfer, clientX: 50, clientY: 0})); + + fireEvent(tree, new DragEvent('drop', {dataTransfer, clientX: 50, clientY: 0})); + fireEvent(project1Row, new DragEvent('dragend', {dataTransfer, clientX: 50, clientY: 0})); + act(() => jest.runAllTimers()); + + expect(onMove).toHaveBeenCalledTimes(1); + expect(onMove.mock.calls[0][0].keys).toEqual(new Set(['project-1'])); + expect(onMove.mock.calls[0][0].target).toEqual({type: 'item', key: 'projects', dropPosition: 'before'}); + + rectSpy.mockRestore(); + }); }); describe('press events', () => { From be99aa2d07ef5fbfba007d032a092db58bdda751 Mon Sep 17 00:00:00 2001 From: Sandro Roth <16229645+rothsandro@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:58:43 +0100 Subject: [PATCH 2/2] Check node type --- packages/react-aria-components/src/TreeDropTargetDelegate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria-components/src/TreeDropTargetDelegate.ts b/packages/react-aria-components/src/TreeDropTargetDelegate.ts index b7407e537c9..cab04e8ecb4 100644 --- a/packages/react-aria-components/src/TreeDropTargetDelegate.ts +++ b/packages/react-aria-components/src/TreeDropTargetDelegate.ts @@ -194,7 +194,7 @@ export class TreeDropTargetDelegate { if (potentialTargets.length === 1) { let nextKey = collection.getKeyAfter(target.key); let nextNode = nextKey ? collection.getItem(nextKey) : null; - if (nextKey != null && nextNode && currentItem && nextNode.level != null && currentItem.level != null && nextNode.level > currentItem.level) { + if (nextKey != null && nextNode && currentItem && nextNode.type === 'item' && nextNode.level != null && currentItem.level != null && nextNode.level > currentItem.level) { let beforeTarget = { type: 'item', key: nextKey,