diff --git a/packages/react-aria-components/test/Table.test.js b/packages/react-aria-components/test/Table.test.js
index 7c9262292af..2619bd546ba 100644
--- a/packages/react-aria-components/test/Table.test.js
+++ b/packages/react-aria-components/test/Table.test.js
@@ -1515,6 +1515,30 @@ describe('Table', () => {
await user.keyboard('{Escape}');
act(() => jest.runAllTimers());
});
+
+ it('should select dropped item', async () => {
+ const DndTableExample = stories.DndTableExample;
+ let {getAllByRole} = render();
+ let tableTester = testUtilUser.createTester('Table', {root: getAllByRole('grid')[1]});
+ expect(tableTester.rows).toHaveLength(7);
+ expect(tableTester.selectedRows).toHaveLength(0);
+ await user.tab();
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{Enter}');
+ act(() => jest.runAllTimers());
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Insert between Adobe Photoshop and Adobe XD');
+ await user.tab();
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on');
+ await user.keyboard('{ArrowDown}');
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Insert before Pictures');
+ fireEvent.keyDown(document.activeElement, {key: 'Enter'});
+ fireEvent.keyUp(document.activeElement, {key: 'Enter'});
+ // run onInsert promise in DnDTableExample first, otherwise updateFocusAfterDrop doesn't run properly
+ await act(async () => {});
+ act(() => jest.runAllTimers());
+ expect(tableTester.rows).toHaveLength(8);
+ expect(tableTester.selectedRows).toHaveLength(1);
+ });
});
describe('column resizing', () => {
diff --git a/packages/react-aria-components/test/Tree.test.tsx b/packages/react-aria-components/test/Tree.test.tsx
index 30eb7c111ca..434ac46e58a 100644
--- a/packages/react-aria-components/test/Tree.test.tsx
+++ b/packages/react-aria-components/test/Tree.test.tsx
@@ -2045,6 +2045,134 @@ describe('Tree', () => {
expect(getItems).toHaveBeenCalledTimes(1);
expect(getItems).toHaveBeenCalledWith(new Set(['projects', 'reports']));
});
+
+ it('should select the parent and all its children when dropped', async () => {
+ let {getAllByRole} = render();
+ let trees = getAllByRole('treegrid');
+
+ let firstTreeTester = testUtilUser.createTester('Tree', {root: trees[0]});
+ let secondTreeTester = testUtilUser.createTester('Tree', {root: trees[1]});
+ expect(firstTreeTester.rows).toHaveLength(2);
+ // has the empty state row
+ expect(secondTreeTester.rows).toHaveLength(1);
+ await user.tab();
+ // selects and drops first row onto second tree
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{Enter}');
+ act(() => jest.runAllTimers());
+ await user.tab();
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on');
+ await act(async () => {
+ fireEvent.keyDown(document.activeElement as Element, {key: 'Enter'});
+ fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
+ });
+ act(() => jest.runAllTimers());
+ expect(secondTreeTester.rows).toHaveLength(1);
+ // expands tree row children
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{ArrowDown}');
+ await user.keyboard('{ArrowDown}');
+ await user.keyboard('{ArrowRight}');
+ expect(secondTreeTester.selectedRows).toHaveLength(9);
+ });
+
+ it('should focus the parent row when dropped on if it isnt expanded', async () => {
+ let {getAllByRole} = render();
+ let trees = getAllByRole('treegrid');
+
+ let firstTreeTester = testUtilUser.createTester('Tree', {root: trees[0]});
+ let secondTreeTester = testUtilUser.createTester('Tree', {root: trees[1]});
+ expect(firstTreeTester.rows).toHaveLength(2);
+ // has the empty state row
+ expect(secondTreeTester.rows).toHaveLength(1);
+ await user.tab();
+ // selects and drops first row onto second tree
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{Enter}');
+ act(() => jest.runAllTimers());
+ await user.tab();
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on');
+ await act(async () => {
+ fireEvent.keyDown(document.activeElement as Element, {key: 'Enter'});
+ fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
+ });
+ act(() => jest.runAllTimers());
+ expect(secondTreeTester.rows).toHaveLength(1);
+ await user.keyboard('{ArrowRight}');
+ expect(secondTreeTester.rows).toHaveLength(6);
+ // tab back to the first tree and drop a new row onto one of the 2nd tree's child rows as it is expanded
+ await user.tab({shift: true});
+ expect(document.activeElement).toBe(firstTreeTester.rows[0]);
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{Enter}');
+ act(() => jest.runAllTimers());
+ await user.tab();
+ for (let i = 0; i < 7; i++) {
+ await user.keyboard('{ArrowDown}');
+ }
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on Project 2');
+ await act(async () => {
+ fireEvent.keyDown(document.activeElement as Element, {key: 'Enter'});
+ fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
+ });
+ act(() => jest.runAllTimers());
+ expect(document.activeElement).toBe(secondTreeTester.rows[2]);
+ });
+
+ it('should focus the dropped row when dropped on a parent that is expanded', async () => {
+ let {getAllByRole} = render();
+ let trees = getAllByRole('treegrid');
+
+ let firstTreeTester = testUtilUser.createTester('Tree', {root: trees[0]});
+ let secondTreeTester = testUtilUser.createTester('Tree', {root: trees[1]});
+ expect(firstTreeTester.rows).toHaveLength(2);
+ // has the empty state row
+ expect(secondTreeTester.rows).toHaveLength(1);
+ await user.tab();
+ // selects and drops first row onto second tree
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{Enter}');
+ act(() => jest.runAllTimers());
+
+ await user.tab();
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on');
+ await act(async () => {
+ fireEvent.keyDown(document.activeElement as Element, {key: 'Enter'});
+ fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
+ });
+ act(() => jest.runAllTimers());
+ expect(secondTreeTester.rows).toHaveLength(1);
+ // expands tree row children
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{ArrowDown}');
+ await user.keyboard('{ArrowDown}');
+ await user.keyboard('{ArrowRight}');
+ expect(secondTreeTester.rows).toHaveLength(9);
+ // tab back to the first tree and drop a new row onto one of the 2nd tree's child rows as it is expanded
+ await user.tab({shift: true});
+ expect(document.activeElement).toBe(firstTreeTester.rows[0]);
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{Enter}');
+
+ act(() => jest.runAllTimers());
+ await user.tab();
+ for (let i = 0; i < 14; i++) {
+ await user.keyboard('{ArrowDown}');
+ }
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on Project 2');
+ await act(async () => {
+ fireEvent.keyDown(document.activeElement as Element, {key: 'Enter'});
+ fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
+ });
+ act(() => jest.runAllTimers());
+ expect(document.activeElement).toHaveTextContent('Projects');
+ expect(document.activeElement).toBe(secondTreeTester.rows[3]);
+
+ });
});
describe('press events', () => {
diff --git a/packages/react-aria/src/dnd/useDroppableCollection.ts b/packages/react-aria/src/dnd/useDroppableCollection.ts
index ef56edfa27d..a832386e5aa 100644
--- a/packages/react-aria/src/dnd/useDroppableCollection.ts
+++ b/packages/react-aria/src/dnd/useDroppableCollection.ts
@@ -253,10 +253,18 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
state.selectionManager.isSelectionEqual(prevSelectedKeys)
) {
let newKeys = new Set();
- for (let item of state.collection) {
- if (item.type === 'item' && !prevCollection.getItem(item.key)) {
+ let key = state.collection.getFirstKey();
+ while (key != null) {
+ let item = state.collection.getItem(key);
+ if (item?.type === 'item' && !prevCollection.getItem(item.key)) {
newKeys.add(item.key);
}
+
+ if (item?.hasChildNodes && state.collection.getItem(item.lastChildKey!)?.type === 'item') {
+ key = item.firstChildKey!;
+ } else {
+ key = state.collection.getKeyAfter(key);
+ }
}
state.selectionManager.setSelectedKeys(newKeys);
@@ -268,10 +276,11 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
let first: Key | null | undefined = newKeys.keys().next().value;
if (first != null) {
let item = state.collection.getItem(first);
-
+ let dropTarget = droppingState.current.target;
+ let isParentRowExpanded = state.collection['expandedKeys'] ? state.collection['expandedKeys'].has(item?.parentKey) : false;
// If this is a cell, focus the parent row.
// eslint-disable-next-line max-depth
- if (item?.type === 'cell') {
+ if (item && (item?.type === 'cell' || (dropTarget.type === 'item' && dropTarget.dropPosition === 'on' && !isParentRowExpanded))) {
first = item.parentKey;
}