From 54e3f2e91ea224df19ed6c85bc4f4076c7477008 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 23 Oct 2025 06:40:27 +1100 Subject: [PATCH 1/2] fix: Styles on Group example to be more consistent (#9071) --- packages/dev/s2-docs/pages/react-aria/Group.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/Group.mdx b/packages/dev/s2-docs/pages/react-aria/Group.mdx index fbc59e4f60b..1cac683c7bd 100644 --- a/packages/dev/s2-docs/pages/react-aria/Group.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Group.mdx @@ -18,19 +18,19 @@ import {Input} from 'react-aria-components'; From 27edf8fca3be1142a3a0dd84f5779127eb51de09 Mon Sep 17 00:00:00 2001 From: chirokas <157580465+chirokas@users.noreply.github.com> Date: Thu, 23 Oct 2025 04:45:49 +0800 Subject: [PATCH 2/2] fix: Fix crash in RAC Table DnD keyboard navigation (#8645) * test * Fix crash on renderAfterDropIndicators in Table * Fix table dnd keyboard navigation --- .../dnd/src/DropTargetKeyboardNavigation.ts | 25 +++++++--- .../table/test/TableDnd.test.js | 43 ++++++++++++++++ .../react-aria-components/src/Collection.tsx | 2 +- .../react-aria-components/stories/styles.css | 7 ++- .../react-aria-components/test/Table.test.js | 50 +++++++++++++++++++ 5 files changed, 117 insertions(+), 10 deletions(-) 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 db7988f0841..6e14b1ead8f 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 b7b7e756a8e..0204f31e265 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 b003525254e..127e9b21222 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', () => {