Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react-aria-components/src/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,7 @@ function GridListDropIndicator(props: GridListDropIndicatorProps, ref: Forwarded
let renderProps = useRenderProps({
...otherProps,
defaultClassName: 'react-aria-DropIndicator',
defaultStyle: {position: 'relative'},
values: {
isDropTarget
}
Expand Down
82 changes: 82 additions & 0 deletions packages/react-aria-components/test/GridList.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,45 @@
* 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';
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: () => undefined,
renderDropIndicator: target => (
<DropIndicator target={target} style={{backgroundColor: 'rgb(255, 0, 0)'}} />
)
});

return (
<GridList
aria-label="Reorderable list"
dragAndDropHooks={dragAndDropHooks}
items={reorderableItems}
style={{display: 'flex', flexDirection: 'column', height: 120, overflow: 'auto'}}>
{item => (
<GridListItem style={{flex: '0 0 40px'}} textValue={item.name}>
<Button slot="drag">Drag</Button>
{item.name}
</GridListItem>
)}
</GridList>
);
}

function Grid() {
return (
<GridList
Expand Down Expand Up @@ -120,3 +150,55 @@ it('virtualizer renders items after toggling display:none', async () => {
await button.click();
await expect(tester.getRows().length).toBeGreaterThan(0);
});

it('scrolls focused drop indicators into view during keyboard reordering', async () => {
let testUtilUser = new User();
let {container} = await render(<ReorderableGridList />);
let gridlist = container.querySelector('[role=grid]') as HTMLElement;
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.
// 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}');

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 = indicatorRow.getBoundingClientRect();

// 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'
});
expect(indicatorRect.top).toBeGreaterThanOrEqual(gridRect.top);
expect(indicatorRect.bottom).toBeLessThanOrEqual(gridRect.bottom);
}

expect(gridlist.scrollTop).toBeGreaterThan(0);
});