diff --git a/packages/@react-aria/dnd/src/DropTargetKeyboardNavigation.ts b/packages/@react-aria/dnd/src/DropTargetKeyboardNavigation.ts
index fee624a4567..d3e24953de2 100644
--- a/packages/@react-aria/dnd/src/DropTargetKeyboardNavigation.ts
+++ b/packages/@react-aria/dnd/src/DropTargetKeyboardNavigation.ts
@@ -100,19 +100,25 @@ function nextDropTarget(
}
case 'after': {
// If this is the last sibling in a level, traverse to the parent.
- let targetNode = collection.getItem(target.key);
- if (targetNode && targetNode.nextKey == null && targetNode.parentKey != null) {
+ let targetNode = collection.getItem(target.key);
+ let nextItemInSameLevel = targetNode?.nextKey != null ? collection.getItem(targetNode.nextKey) : null;
+ while (nextItemInSameLevel != null && nextItemInSameLevel.type !== 'item') {
+ nextItemInSameLevel = nextItemInSameLevel.nextKey != null ? collection.getItem(nextItemInSameLevel.nextKey) : null;
+ }
+
+ if (targetNode && nextItemInSameLevel == null && targetNode.parentKey != null) {
// If the parent item has an item after it, use the "before" position.
let parentNode = collection.getItem(targetNode.parentKey);
- if (parentNode?.nextKey != null) {
+ const nextNode = parentNode?.nextKey != null ? collection.getItem(parentNode.nextKey) : null;
+ if (nextNode?.type === 'item') {
return {
type: 'item',
- key: parentNode.nextKey,
+ key: nextNode.key,
dropPosition: 'before'
};
}
- if (parentNode) {
+ if (parentNode?.type === 'item') {
return {
type: 'item',
key: parentNode.key,
@@ -121,10 +127,10 @@ function nextDropTarget(
}
}
- if (targetNode?.nextKey != null) {
+ if (nextItemInSameLevel) {
return {
type: 'item',
- key: targetNode.nextKey,
+ key: nextItemInSameLevel.key,
dropPosition: 'on'
};
}
@@ -154,8 +160,11 @@ function previousDropTarget(
let prevKey: Key | null = null;
let lastKey = keyboardDelegate.getLastKey?.();
while (lastKey != null) {
- prevKey = lastKey;
let node = collection.getItem(lastKey);
+ if (node?.type !== 'item') {
+ break;
+ }
+ prevKey = lastKey;
lastKey = node?.parentKey;
}
diff --git a/packages/@react-spectrum/table/test/TableDnd.test.js b/packages/@react-spectrum/table/test/TableDnd.test.js
index a97231f92a7..652940958b1 100644
--- a/packages/@react-spectrum/table/test/TableDnd.test.js
+++ b/packages/@react-spectrum/table/test/TableDnd.test.js
@@ -1967,6 +1967,49 @@ describe('TableView', function () {
});
});
+ it('support drop target keyboard navigation', async () => {
+ render();
+ await user.tab();
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{Enter}');
+ act(() => jest.runAllTimers());
+
+ const labels = ['Vin Charlet', 'Lexy Maddison', 'Robbi Persence', 'Dodie Hurworth', 'Audrye Hember', 'Beau Oller', 'Roarke Gration', 'Cathy Lishman', 'Enrika Soitoux', 'Aloise Tuxsell'];
+
+ for (let i = 0; i < labels.length; i++) {
+ if (i === labels.length - 1) {
+ expect(document.activeElement).toHaveAttribute('aria-label', `Insert after ${labels[i]}`);
+ } else {
+ expect(document.activeElement).toHaveAttribute('aria-label', `Insert between ${labels[i]} and ${labels[i + 1]}`);
+ }
+
+ fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'});
+ fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'});
+ }
+
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Insert before Vin Charlet');
+ await user.keyboard('{End}');
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Insert after Aloise Tuxsell');
+
+ for (let i = labels.length - 1; i >= 0; i--) {
+ fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
+ fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'});
+
+ if (i === 0) {
+ expect(document.activeElement).toHaveAttribute('aria-label', `Insert before ${labels[i]}`);
+ } else {
+ expect(document.activeElement).toHaveAttribute('aria-label', `Insert between ${labels[i - 1]} and ${labels[i]}`);
+ }
+ }
+
+ await user.keyboard('{ArrowUp}');
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Insert after Aloise Tuxsell');
+ await user.keyboard('{Home}');
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Insert before Vin Charlet');
+ await user.keyboard('{Escape}');
+ act(() => jest.runAllTimers());
+ });
+
describe('using util handlers', function () {
async function beginDrag(tree) {
let grids = tree.getAllByRole('grid');
diff --git a/packages/react-aria-components/src/Collection.tsx b/packages/react-aria-components/src/Collection.tsx
index ed1bc25174b..d0e27e11e02 100644
--- a/packages/react-aria-components/src/Collection.tsx
+++ b/packages/react-aria-components/src/Collection.tsx
@@ -195,7 +195,7 @@ export function renderAfterDropIndicators(collection: ICollection>
let afterIndicators: ReactNode[] = [];
if (nextItemInSameLevel == null) {
let current: Node | null = node;
- while (current && (!nextItemInFlattenedCollection || (current.parentKey !== nextItemInFlattenedCollection.parentKey && nextItemInFlattenedCollection.level < current.level))) {
+ while (current?.type === 'item' && (!nextItemInFlattenedCollection || (current.parentKey !== nextItemInFlattenedCollection.parentKey && nextItemInFlattenedCollection.level < current.level))) {
let indicator = renderDropIndicator({
type: 'item',
key: current.key,
diff --git a/packages/react-aria-components/stories/styles.css b/packages/react-aria-components/stories/styles.css
index 451d50eb9ba..f8280e807ee 100644
--- a/packages/react-aria-components/stories/styles.css
+++ b/packages/react-aria-components/stories/styles.css
@@ -205,9 +205,14 @@
outline: 1px solid slateblue;
}
-
:global(.react-aria-Table) {
border-collapse: collapse;
+
+ &[data-drop-target] {
+ outline: 2px solid purple;
+ outline-offset: -2px;
+ background: rgb(from purple r g b / 20%);
+ }
}
:global(.react-aria-Cell) {
diff --git a/packages/react-aria-components/test/Table.test.js b/packages/react-aria-components/test/Table.test.js
index 667096ceabd..8074ff97020 100644
--- a/packages/react-aria-components/test/Table.test.js
+++ b/packages/react-aria-components/test/Table.test.js
@@ -1310,6 +1310,56 @@ describe('Table', () => {
expect(checkbox).toBeChecked();
}
});
+
+ it('support drop target keyboard navigation', async () => {
+ const DndTableExample = stories.DndTableExample;
+ render();
+ 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');
+
+ const labels = ['Pictures', 'Adobe Fresco', 'Apps', 'Adobe Illustrator', 'Adobe Lightroom', 'Adobe Dreamweaver'];
+
+ for (let i = 0; i <= labels.length; i++) {
+ fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'});
+ fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'});
+
+ if (i === 0) {
+ expect(document.activeElement).toHaveAttribute('aria-label', `Insert before ${labels[i]}`);
+ } else if (i === labels.length) {
+ expect(document.activeElement).toHaveAttribute('aria-label', `Insert after ${labels[i - 1]}`);
+ } else {
+ expect(document.activeElement).toHaveAttribute('aria-label', `Insert between ${labels[i - 1]} and ${labels[i]}`);
+ }
+ }
+
+ await user.keyboard('{Home}');
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on');
+
+ for (let i = labels.length; i >= 0; i--) {
+ fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
+ fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'});
+
+ if (i === 0) {
+ expect(document.activeElement).toHaveAttribute('aria-label', `Insert before ${labels[i]}`);
+ } else if (i === labels.length) {
+ expect(document.activeElement).toHaveAttribute('aria-label', `Insert after ${labels[i - 1]}`);
+ } else {
+ expect(document.activeElement).toHaveAttribute('aria-label', `Insert between ${labels[i - 1]} and ${labels[i]}`);
+ }
+ }
+
+ await user.keyboard('{End}');
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Insert after Adobe Dreamweaver');
+ await user.keyboard('{ArrowDown}');
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Drop on');
+ await user.keyboard('{Escape}');
+ act(() => jest.runAllTimers());
+ });
});
describe('column resizing', () => {