Skip to content

Commit fabca84

Browse files
authored
Fix useListState infinite loop (#6621)
* Fix useListState infinite loop
1 parent 8173dda commit fabca84

File tree

2 files changed

+45
-1
lines changed

2 files changed

+45
-1
lines changed

packages/@react-stately/list/src/useListState.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,18 @@ export function useListState<T extends object>(props: ListProps<T>): ListState<T
8282
),
8383
itemNodes.length - 1);
8484
let newNode:Node<T>;
85+
let isReverseSearching = false;
8586
while (index >= 0) {
8687
if (!selectionManager.isDisabled(itemNodes[index].key)) {
8788
newNode = itemNodes[index];
8889
break;
8990
}
9091
// Find next, not disabled item.
91-
if (index < itemNodes.length - 1) {
92+
if (index < itemNodes.length - 1 && !isReverseSearching) {
9293
index++;
9394
// Otherwise, find previous, not disabled item.
9495
} else {
96+
isReverseSearching = true;
9597
if (index > startItem.index) {
9698
index = startItem.index;
9799
}

packages/react-aria-components/test/TagGroup.test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import {Button, Label, RouterProvider, Tag, TagGroup, TagList, Text} from '../';
1414
import {fireEvent, mockClickDefault, pointerMap, render} from '@react-spectrum/test-utils-internal';
1515
import React from 'react';
16+
import {useListData} from '@react-stately/data';
1617
import userEvent from '@testing-library/user-event';
1718

1819
let TestTagGroup = ({tagGroupProps, tagListProps, itemProps}) => (
@@ -351,4 +352,45 @@ describe('TagGroup', () => {
351352
});
352353
});
353354
});
355+
it('if we cannot restore focus to next, then restore to previous but do not try focusing next again', async () => {
356+
function MyTagGroup(props) {
357+
const fruitsList = useListData({
358+
initialItems: [
359+
{id: 2, name: 'Grape'},
360+
{id: 3, name: 'Plum'},
361+
{id: 4, name: 'Watermelon'}
362+
]
363+
});
364+
return (
365+
<TagGroup
366+
data-testid="group"
367+
aria-label="Fruits"
368+
items={fruitsList.items}
369+
selectionMode="multiple"
370+
disabledKeys={[2, 3]}
371+
onRemove={(keys) => fruitsList.remove(...keys)}>
372+
<TagList items={fruitsList.items}>
373+
{(item) => <MyTag item={item}>{item.name}</MyTag>}
374+
</TagList>
375+
</TagGroup>
376+
);
377+
}
378+
function MyTag({children, item, ...props}) {
379+
return (
380+
<Tag textValue={item.name} {...props}>
381+
{({isDisabled}) => (
382+
<>
383+
{children}
384+
<Button slot="remove" isDisabled={isDisabled} aria-label={'remove'} />
385+
</>
386+
)}
387+
</Tag>
388+
);
389+
}
390+
let {getByRole} = render(<MyTagGroup />);
391+
let grid = getByRole('grid');
392+
await user.tab();
393+
await user.keyboard('{Backspace}');
394+
expect(grid).toHaveFocus();
395+
});
354396
});

0 commit comments

Comments
 (0)