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,