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}');