Skip to content

Commit 7b9da59

Browse files
authored
fix: Tree keyboard drag and drop should skip content nodes (adobe#9724)
1 parent 2fd0040 commit 7b9da59

File tree

2 files changed

+54
-10
lines changed

2 files changed

+54
-10
lines changed

packages/@react-aria/dnd/src/DropTargetKeyboardNavigation.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,7 @@ function nextDropTarget(
5858
} else {
5959
nextKey = keyboardDelegate.getKeyBelow?.(target.key);
6060
}
61-
let nextCollectionKey = collection.getKeyAfter(target.key);
62-
let nextCollectionNode = nextCollectionKey && collection.getItem(nextCollectionKey);
63-
while (nextCollectionNode && nextCollectionNode.type === 'content') {
64-
nextCollectionKey = nextCollectionKey ? collection.getKeyAfter(nextCollectionKey) : null;
65-
nextCollectionNode = nextCollectionKey && collection.getItem(nextCollectionKey);
66-
}
61+
let nextCollectionKey = getNextItem(collection, target.key, key => collection.getKeyAfter(key));
6762

6863
// If the keyboard delegate did not move to the next key in the collection,
6964
// jump to that key with the same drop position. Otherwise, try the other
@@ -191,7 +186,7 @@ function previousDropTarget(
191186
} else {
192187
prevKey = keyboardDelegate.getKeyAbove?.(target.key);
193188
}
194-
let prevCollectionKey = collection.getKeyBefore(target.key);
189+
let prevCollectionKey = getNextItem(collection, target.key, key => collection.getKeyBefore(key));
195190

196191
// If the keyboard delegate did not move to the next key in the collection,
197192
// jump to that key with the same drop position. Otherwise, try the other
@@ -263,7 +258,7 @@ function getLastChild(collection: Collection<Node<unknown>>, key: Key): DropTarg
263258
// getChildNodes still returns child tree items even when the item is collapsed.
264259
// Checking if the next item has a greater level is a silly way to determine if the item is expanded.
265260
let targetNode = collection.getItem(key);
266-
let nextKey = collection.getKeyAfter(key);
261+
let nextKey = getNextItem(collection, key, key => collection.getKeyAfter(key));
267262
let nextNode = nextKey != null ? collection.getItem(nextKey) : null;
268263
if (targetNode && nextNode && nextNode.level > targetNode.level) {
269264
let children = getChildNodes(targetNode, collection);
@@ -285,3 +280,14 @@ function getLastChild(collection: Collection<Node<unknown>>, key: Key): DropTarg
285280

286281
return null;
287282
}
283+
284+
// Find the next or previous item in a collection, skipping over other types of nodes (e.g. content).
285+
function getNextItem(collection: Collection<Node<unknown>>, key: Key, getNextKey: (key: Key) => Key | null): Key | null {
286+
let nextCollectionKey = getNextKey(key);
287+
let nextCollectionNode = nextCollectionKey != null ? collection.getItem(nextCollectionKey) : null;
288+
while (nextCollectionNode && nextCollectionNode.type !== 'item') {
289+
nextCollectionKey = getNextKey(nextCollectionNode.key);
290+
nextCollectionNode = nextCollectionKey != null ? collection.getItem(nextCollectionKey) : null;
291+
}
292+
return nextCollectionKey;
293+
}

packages/@react-aria/dnd/test/DropTargetKeyboardNavigation.test.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class TestCollection implements Collection<Node<Item>> {
4848
constructor(items: Item[]) {
4949
this.map = new Map<Key, Node<Item>>();
5050
let visitItem = (item: Item, index: number, level = 0, parentKey: string | null = null): Node<Item> => {
51-
let childNodes = item.childItems ? visitItems(item.childItems, level + 1, item.id) : [];
51+
let childNodes = visitItems(item.childItems ?? [], level + 1, item.id);
5252
return {
5353
type: 'item',
5454
key: item.id,
@@ -67,6 +67,24 @@ class TestCollection implements Collection<Node<Item>> {
6767
let last: Node<Item> | null = null;
6868
let index = 0;
6969
let nodes: Node<Item>[] = [];
70+
if (parentKey != null) {
71+
let contentNode = {
72+
type: 'content',
73+
key: parentKey + '-content',
74+
value: null,
75+
level: level,
76+
hasChildNodes: false,
77+
childNodes: [],
78+
rendered: null,
79+
textValue: '',
80+
index: index++,
81+
parentKey
82+
};
83+
this.map.set(contentNode.key, contentNode);
84+
nodes.push(contentNode);
85+
last = contentNode;
86+
}
87+
7088
for (let item of items) {
7189
let node = visitItem(item, index, level, parentKey);
7290
this.map.set(item.id, node);
@@ -236,25 +254,45 @@ describe('drop target keyboard navigation', () => {
236254

237255
let expectedKeys = [
238256
'projects',
257+
'projects-content',
239258
'project-1',
259+
'project-1-content',
240260
'project-2',
261+
'project-2-content',
241262
'project-2A',
263+
'project-2A-content',
242264
'project-2B',
265+
'project-2B-content',
243266
'project-2C',
267+
'project-2C-content',
244268
'project-3',
269+
'project-3-content',
245270
'project-4',
271+
'project-4-content',
246272
'project-5',
273+
'project-5-content',
247274
'project-5A',
275+
'project-5A-content',
248276
'project-5B',
277+
'project-5B-content',
249278
'project-5C',
279+
'project-5C-content',
250280
'reports',
281+
'reports-content',
251282
'reports-1',
283+
'reports-1-content',
252284
'reports-1A',
285+
'reports-1A-content',
253286
'reports-1AB',
287+
'reports-1AB-content',
254288
'reports-1ABC',
289+
'reports-1ABC-content',
255290
'reports-1B',
291+
'reports-1B-content',
256292
'reports-1C',
257-
'reports-2'
293+
'reports-1C-content',
294+
'reports-2',
295+
'reports-2-content'
258296
];
259297

260298
expect(nextKeys).toEqual(expectedKeys);

0 commit comments

Comments
 (0)