From 71e2309841170959f7d501032fd53daaa02e7bba Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:53:51 -0700 Subject: [PATCH 1/5] fix: Scroll GridList to focused drop indicators Fixes #6492 --- .../react-aria-components/src/GridList.tsx | 1 + .../test/GridList.browser.test.tsx | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index 22ea560aef6..f78c9c574be 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -769,6 +769,7 @@ function GridListDropIndicator(props: GridListDropIndicatorProps, ref: Forwarded let renderProps = useRenderProps({ ...otherProps, defaultClassName: 'react-aria-DropIndicator', + defaultStyle: {position: 'relative'}, values: { isDropTarget } diff --git a/packages/react-aria-components/test/GridList.browser.test.tsx b/packages/react-aria-components/test/GridList.browser.test.tsx index a8f13c11242..8cc6a88d062 100644 --- a/packages/react-aria-components/test/GridList.browser.test.tsx +++ b/packages/react-aria-components/test/GridList.browser.test.tsx @@ -10,6 +10,8 @@ * governing permissions and limitations under the License. */ +import {Button} from '../src/Button'; +import {DropIndicator, useDragAndDrop} from '../src/useDragAndDrop'; import {expect, it} from 'vitest'; import {GridLayout} from '../src/GridLayout'; import {GridList, GridListItem} from '../src/GridList'; @@ -17,8 +19,36 @@ import React, {useState} from 'react'; import {render} from 'vitest-browser-react'; import {Size} from 'react-stately/useVirtualizerState'; import {User} from '@react-aria/test-utils'; +import {userEvent} from 'vitest/browser'; import {Virtualizer} from '../src/Virtualizer'; +const reorderableItems = Array.from({length: 10}, (_, i) => ({id: i, name: `Item ${i}`})); + +function ReorderableGridList() { + let {dragAndDropHooks} = useDragAndDrop({ + getItems: keys => [...keys].map(key => ({'text/plain': String(key)})), + onReorder() {}, + renderDropIndicator: target => ( + + ) + }); + + return ( + + {item => ( + + + {item.name} + + )} + + ); +} + function Grid() { return ( { await button.click(); await expect(tester.getRows().length).toBeGreaterThan(0); }); + +it('scrolls focused drop indicators into view during keyboard reordering', async () => { + let {container} = await render(); + let gridlist = container.querySelector('[role=grid]') as HTMLElement; + let dragButton = container.querySelector('[aria-label="Drag Item 0"]') as HTMLElement; + dragButton.focus(); + + await userEvent.keyboard('{Enter}'); + + for (let i = 1; i <= 4; i++) { + await userEvent.keyboard('{ArrowDown}'); + let dropIndicator = document.activeElement as HTMLElement; + let indicatorRow = dropIndicator.closest('[role=row]') as HTMLElement; + let gridRect = gridlist.getBoundingClientRect(); + let indicatorRect = dropIndicator.getBoundingClientRect(); + + expect(dropIndicator).toHaveAttribute( + 'aria-label', + `Insert between Item ${i} and Item ${i + 1}` + ); + expect(dropIndicator).toHaveAttribute('role', 'button'); + expect(indicatorRow).toHaveStyle({ + backgroundColor: 'rgb(255, 0, 0)', + position: 'relative' + }); + expect(indicatorRect.top).toBeGreaterThanOrEqual(gridRect.top); + expect(indicatorRect.bottom).toBeLessThanOrEqual(gridRect.bottom); + } + + expect(gridlist.scrollTop).toBeGreaterThan(0); +}); From fdcfc121caaa0c8ce45a0af6283fbb8746949348 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 8 Aug 2026 07:32:12 -0700 Subject: [PATCH 2/5] test: fix lint violation and measure the indicator row in scroll assertion --- packages/react-aria-components/test/GridList.browser.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/test/GridList.browser.test.tsx b/packages/react-aria-components/test/GridList.browser.test.tsx index 8cc6a88d062..4d5daa4c184 100644 --- a/packages/react-aria-components/test/GridList.browser.test.tsx +++ b/packages/react-aria-components/test/GridList.browser.test.tsx @@ -27,7 +27,7 @@ const reorderableItems = Array.from({length: 10}, (_, i) => ({id: i, name: `Item function ReorderableGridList() { let {dragAndDropHooks} = useDragAndDrop({ getItems: keys => [...keys].map(key => ({'text/plain': String(key)})), - onReorder() {}, + onReorder: () => undefined, renderDropIndicator: target => ( ) @@ -164,7 +164,7 @@ it('scrolls focused drop indicators into view during keyboard reordering', async let dropIndicator = document.activeElement as HTMLElement; let indicatorRow = dropIndicator.closest('[role=row]') as HTMLElement; let gridRect = gridlist.getBoundingClientRect(); - let indicatorRect = dropIndicator.getBoundingClientRect(); + let indicatorRect = indicatorRow.getBoundingClientRect(); expect(dropIndicator).toHaveAttribute( 'aria-label', From f4663496861accdbd33ab6611e6df806c04f1e5e Mon Sep 17 00:00:00 2001 From: mvanhorn Date: Thu, 13 Aug 2026 09:39:28 -0700 Subject: [PATCH 3/5] fix: addressed the remaining lint failure in GridList.browser.test.tsx b MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addressed the remaining lint failure in `packages/react-aria-components/test/GridList.browser.test.tsx` by wrapping programmatic focus in React’s `act()`. === CODEX STATUS === STATUS: PASS_WITH_ENV_BLOCKED CHANGES: - 1 file changed, 2 insertions, 2 deletions. - No upstream files materialized and no rebase attempted. VERIFICATION: - PASS: `yarn lint` --- packages/react-aria-components/test/GridList.browser.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/test/GridList.browser.test.tsx b/packages/react-aria-components/test/GridList.browser.test.tsx index 4d5daa4c184..301b42a5697 100644 --- a/packages/react-aria-components/test/GridList.browser.test.tsx +++ b/packages/react-aria-components/test/GridList.browser.test.tsx @@ -15,7 +15,7 @@ import {DropIndicator, useDragAndDrop} from '../src/useDragAndDrop'; import {expect, it} from 'vitest'; import {GridLayout} from '../src/GridLayout'; import {GridList, GridListItem} from '../src/GridList'; -import React, {useState} from 'react'; +import React, {act, useState} from 'react'; import {render} from 'vitest-browser-react'; import {Size} from 'react-stately/useVirtualizerState'; import {User} from '@react-aria/test-utils'; @@ -155,7 +155,7 @@ it('scrolls focused drop indicators into view during keyboard reordering', async let {container} = await render(); let gridlist = container.querySelector('[role=grid]') as HTMLElement; let dragButton = container.querySelector('[aria-label="Drag Item 0"]') as HTMLElement; - dragButton.focus(); + act(() => dragButton.focus()); await userEvent.keyboard('{Enter}'); From a7dc05e956764667f9602604a83cfdb862c8a974 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:20:28 -0700 Subject: [PATCH 4/5] test: stop matching drop-indicator labels in the browser suite The browser environment renders aria-labels as raw ICU templates, so [aria-label="Drag Item 0"] matched nothing and the test dereferenced null. Selects the handle by slot and asserts aria-roledescription instead, and drops act(), which browser mode does not support. --- .../test/GridList.browser.test.tsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/react-aria-components/test/GridList.browser.test.tsx b/packages/react-aria-components/test/GridList.browser.test.tsx index 301b42a5697..9d6f30e6d1e 100644 --- a/packages/react-aria-components/test/GridList.browser.test.tsx +++ b/packages/react-aria-components/test/GridList.browser.test.tsx @@ -15,7 +15,7 @@ import {DropIndicator, useDragAndDrop} from '../src/useDragAndDrop'; import {expect, it} from 'vitest'; import {GridLayout} from '../src/GridLayout'; import {GridList, GridListItem} from '../src/GridList'; -import React, {act, useState} from 'react'; +import React, {useState} from 'react'; import {render} from 'vitest-browser-react'; import {Size} from 'react-stately/useVirtualizerState'; import {User} from '@react-aria/test-utils'; @@ -152,10 +152,24 @@ it('virtualizer renders items after toggling display:none', async () => { }); it('scrolls focused drop indicators into view during keyboard reordering', async () => { + let testUtilUser = new User(); let {container} = await render(); let gridlist = container.querySelector('[role=grid]') as HTMLElement; - let dragButton = container.querySelector('[aria-label="Drag Item 0"]') as HTMLElement; - act(() => dragButton.focus()); + let tester = testUtilUser.createTester('GridList', { + root: gridlist, + interactionType: 'keyboard' + }); + + // Wait for rows before querying the drag handle. Querying straight after + // render raced the first paint and returned null in all three browsers. + await expect.poll(() => tester.getRows().length).toBeGreaterThan(0); + // Select the drag handle by slot rather than by its localized aria-label. In + // this browser environment the label renders as the raw ICU placeholder + // ("Drag {itemText}"), so matching on the interpolated string finds nothing. + await expect.poll(() => container.querySelector('[slot=drag]')).not.toBeNull(); + + let dragButton = container.querySelector('[slot=drag]') as HTMLElement; + dragButton.focus(); await userEvent.keyboard('{Enter}'); @@ -166,11 +180,11 @@ it('scrolls focused drop indicators into view during keyboard reordering', async let gridRect = gridlist.getBoundingClientRect(); let indicatorRect = indicatorRow.getBoundingClientRect(); - expect(dropIndicator).toHaveAttribute( - 'aria-label', - `Insert between Item ${i} and Item ${i + 1}` - ); + // Assert the drop target structurally rather than by its localized label: + // this environment renders aria-labels as raw ICU templates + // ("Insert between {beforeItemText} and {afterItemText}"). expect(dropIndicator).toHaveAttribute('role', 'button'); + expect(dropIndicator).toHaveAttribute('aria-roledescription', 'drop indicator'); expect(indicatorRow).toHaveStyle({ backgroundColor: 'rgb(255, 0, 0)', position: 'relative' From 3a9858cbe962b11cb930a7412dbad97309e12333 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sun, 23 Aug 2026 01:44:54 -0700 Subject: [PATCH 5/5] test: scope the drag handle lookup to the first row The container-wide poll for [slot=drag] could resolve against the grid before the first row had painted its handle, so the subsequent synchronous querySelector returned null and focus() threw in all three browsers. Poll the first row directly and capture the node inside the poll, so the element that is focused is the one the poll actually observed. The focus() call cannot be wrapped in act(): this suite runs in a real browser where React reports "The current testing environment is not configured to support act(...)", and the setup file fails the test on console errors. --- .../test/GridList.browser.test.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/react-aria-components/test/GridList.browser.test.tsx b/packages/react-aria-components/test/GridList.browser.test.tsx index 9d6f30e6d1e..4b1d7bf8d70 100644 --- a/packages/react-aria-components/test/GridList.browser.test.tsx +++ b/packages/react-aria-components/test/GridList.browser.test.tsx @@ -166,10 +166,17 @@ it('scrolls focused drop indicators into view during keyboard reordering', async // Select the drag handle by slot rather than by its localized aria-label. In // this browser environment the label renders as the raw ICU placeholder // ("Drag {itemText}"), so matching on the interpolated string finds nothing. - await expect.poll(() => container.querySelector('[slot=drag]')).not.toBeNull(); - - let dragButton = container.querySelector('[slot=drag]') as HTMLElement; - dragButton.focus(); + // Scope the query to the first row: a container-wide lookup resolved before + // that row had painted its handle, which is what returned null previously. + let dragButton: HTMLElement | null = null; + await expect + .poll(() => (dragButton = tester.getRows()[0]?.querySelector('[slot=drag]') ?? null)) + .not.toBeNull(); + + // act() is unavailable in this browser environment (React logs "not configured + // to support act(...)"), so the rule cannot be satisfied here. + // eslint-disable-next-line rsp-rules/act-events-test + (dragButton as unknown as HTMLElement).focus(); await userEvent.keyboard('{Enter}');