Skip to content

Commit 9b90f43

Browse files
committed
fix focus issues when dropping on a collapsed/expanded tree item and select all child rows on drop
1 parent d752135 commit 9b90f43

2 files changed

Lines changed: 134 additions & 3 deletions

File tree

packages/react-aria-components/test/Tree.test.tsx

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2045,6 +2045,131 @@ describe('Tree', () => {
20452045
expect(getItems).toHaveBeenCalledTimes(1);
20462046
expect(getItems).toHaveBeenCalledWith(new Set(['projects', 'reports']));
20472047
});
2048+
2049+
it('should select the parent and all its children when dropped', async () => {
2050+
let {getAllByRole} = render(<TreeWithDragAndDrop selectionMode="multiple" />);
2051+
let trees = getAllByRole('treegrid');
2052+
2053+
let firstTreeTester = testUtilUser.createTester('Tree', {root: trees[0]});
2054+
let secondTreeTester = testUtilUser.createTester('Tree', {root: trees[1]});
2055+
expect(firstTreeTester.rows).toHaveLength(2);
2056+
// has the empty state row
2057+
expect(secondTreeTester.rows).toHaveLength(1);
2058+
await user.tab();
2059+
// selects and drops first row onto second tree
2060+
await user.keyboard('{ArrowRight}');
2061+
await user.keyboard('{ArrowRight}');
2062+
await user.keyboard('{ArrowRight}');
2063+
await user.keyboard('{Enter}');
2064+
act(() => jest.runAllTimers());
2065+
await user.tab();
2066+
expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on');
2067+
fireEvent.keyDown(document.activeElement, {key: 'Enter'});
2068+
fireEvent.keyUp(document.activeElement, {key: 'Enter'});
2069+
// run onInsert promise
2070+
await act(async () => {});
2071+
act(() => jest.runAllTimers());
2072+
expect(secondTreeTester.rows).toHaveLength(1);
2073+
// expands tree row children
2074+
await user.keyboard('{ArrowRight}');
2075+
await user.keyboard('{ArrowDown}');
2076+
await user.keyboard('{ArrowDown}');
2077+
await user.keyboard('{ArrowRight}');
2078+
expect(secondTreeTester.selectedRows).toHaveLength(9);
2079+
});
2080+
2081+
it('should focus the parent row when dropped on if it isnt expanded', async () => {
2082+
let {getAllByRole} = render(<TreeWithDragAndDrop />);
2083+
let trees = getAllByRole('treegrid');
2084+
2085+
let firstTreeTester = testUtilUser.createTester('Tree', {root: trees[0]});
2086+
let secondTreeTester = testUtilUser.createTester('Tree', {root: trees[1]});
2087+
expect(firstTreeTester.rows).toHaveLength(2);
2088+
// has the empty state row
2089+
expect(secondTreeTester.rows).toHaveLength(1);
2090+
await user.tab();
2091+
// selects and drops first row onto second tree
2092+
await user.keyboard('{ArrowRight}');
2093+
await user.keyboard('{ArrowRight}');
2094+
await user.keyboard('{Enter}');
2095+
act(() => jest.runAllTimers());
2096+
await user.tab();
2097+
expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on');
2098+
fireEvent.keyDown(document.activeElement as Element, {key: 'Enter'});
2099+
fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
2100+
// run onInsert promise
2101+
await act(async () => {});
2102+
act(() => jest.runAllTimers());
2103+
expect(secondTreeTester.rows).toHaveLength(1);
2104+
await user.keyboard('{ArrowRight}');
2105+
expect(secondTreeTester.rows).toHaveLength(6);
2106+
// tab back to the first tree and drop a new row onto one of the 2nd tree's child rows as it is expanded
2107+
await user.tab({shift: true});
2108+
expect(document.activeElement).toBe(firstTreeTester.rows[0]);
2109+
await user.keyboard('{ArrowRight}');
2110+
await user.keyboard('{Enter}');
2111+
act(() => jest.runAllTimers());
2112+
await user.tab();
2113+
for (let i = 0; i < 7; i++) {
2114+
await user.keyboard('{ArrowDown}');
2115+
}
2116+
expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on Project 2');
2117+
await user.keyboard('{Enter}');
2118+
await act(async () => {});
2119+
act(() => jest.runAllTimers());
2120+
expect(document.activeElement).toBe(secondTreeTester.rows[2]);
2121+
});
2122+
2123+
it('should focus the dropped row when dropped on a parent that is expanded', async () => {
2124+
let {getAllByRole} = render(<TreeWithDragAndDrop />);
2125+
let trees = getAllByRole('treegrid');
2126+
2127+
let firstTreeTester = testUtilUser.createTester('Tree', {root: trees[0]});
2128+
let secondTreeTester = testUtilUser.createTester('Tree', {root: trees[1]});
2129+
expect(firstTreeTester.rows).toHaveLength(2);
2130+
// has the empty state row
2131+
expect(secondTreeTester.rows).toHaveLength(1);
2132+
await user.tab();
2133+
// selects and drops first row onto second tree
2134+
await user.keyboard('{ArrowRight}');
2135+
await user.keyboard('{ArrowRight}');
2136+
await user.keyboard('{Enter}');
2137+
act(() => jest.runAllTimers());
2138+
2139+
await user.tab();
2140+
expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on');
2141+
fireEvent.keyDown(document.activeElement as Element, {key: 'Enter'});
2142+
fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
2143+
// run onInsert promise
2144+
await act(async () => {});
2145+
act(() => jest.runAllTimers());
2146+
expect(secondTreeTester.rows).toHaveLength(1);
2147+
// expands tree row children
2148+
await user.keyboard('{ArrowRight}');
2149+
await user.keyboard('{ArrowDown}');
2150+
await user.keyboard('{ArrowDown}');
2151+
await user.keyboard('{ArrowRight}');
2152+
expect(secondTreeTester.rows).toHaveLength(9);
2153+
// tab back to the first tree and drop a new row onto one of the 2nd tree's child rows as it is expanded
2154+
await user.tab({shift: true});
2155+
expect(document.activeElement).toBe(firstTreeTester.rows[0]);
2156+
await user.keyboard('{ArrowRight}');
2157+
await user.keyboard('{Enter}');
2158+
2159+
act(() => jest.runAllTimers());
2160+
await user.tab();
2161+
for (let i = 0; i < 14; i++) {
2162+
await user.keyboard('{ArrowDown}');
2163+
}
2164+
expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on Project 2');
2165+
fireEvent.keyDown(document.activeElement as Element, {key: 'Enter'});
2166+
fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
2167+
await act(async () => {});
2168+
act(() => jest.runAllTimers());
2169+
expect(document.activeElement).toHaveTextContent('Projects');
2170+
expect(document.activeElement).toBe(secondTreeTester.rows[3]);
2171+
2172+
});
20482173
});
20492174

20502175
describe('press events', () => {

packages/react-aria/src/dnd/useDroppableCollection.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,12 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
259259
if (item?.type === 'item' && !prevCollection.getItem(item.key)) {
260260
newKeys.add(item.key);
261261
}
262-
key = state.collection.getKeyAfter(key);
262+
263+
if (item?.hasChildNodes && state.collection.getItem(item.lastChildKey!)?.type === 'item') {
264+
key = item.firstChildKey!;
265+
} else {
266+
key = state.collection.getKeyAfter(key);
267+
}
263268
}
264269

265270
state.selectionManager.setSelectedKeys(newKeys);
@@ -271,10 +276,11 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
271276
let first: Key | null | undefined = newKeys.keys().next().value;
272277
if (first != null) {
273278
let item = state.collection.getItem(first);
274-
279+
let dropTarget = droppingState.current.target;
280+
let isParentRowExpanded = state.collection['expandedKeys'] ? state.collection['expandedKeys'].has(item?.parentKey) : false;
275281
// If this is a cell, focus the parent row.
276282
// eslint-disable-next-line max-depth
277-
if (item?.type === 'cell') {
283+
if (item && (item?.type === 'cell' || (dropTarget.type === 'item' && dropTarget.dropPosition === 'on' && !isParentRowExpanded))) {
278284
first = item.parentKey;
279285
}
280286

0 commit comments

Comments
 (0)