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; }