Skip to content
Open
30 changes: 30 additions & 0 deletions packages/react-aria-components/test/Table.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,36 @@ describe('Table', () => {
expect(document.activeElement).toBe(cell);
});

it('does not hang restoring focus when no focusable row remains', async () => {
let {rerender, getByRole} = render(<DynamicTable tableBodyProps={{rows}} />);

let tableTester = testUtilUser.createTester('Table', {root: getByRole('grid')});
await user.tab();
await user.keyboard('{ArrowDown}');
await user.keyboard('{ArrowRight}');

let gridRows = tableTester.getRows();
let cell = within(gridRows[1]).getAllByRole('rowheader')[0];
expect(cell).toHaveTextContent('Program Files');
expect(document.activeElement).toBe(cell);

// Remove the focused row and disable every remaining row. Restoring focus
// previously infinite-looped (the search bounced between the focused index
// and the end and never terminated when no focusable row existed).
let remaining = [rows[0], ...rows.slice(2)];
rerender(
<DynamicTable
tableProps={{disabledBehavior: 'all', disabledKeys: remaining.map(r => r.id)}}
tableBodyProps={{items: remaining}}
/>
);

// Completes without hanging; focus left the now-removed row.
gridRows = tableTester.getRows();
expect(gridRows).toHaveLength(3);
expect(document.activeElement).not.toBe(cell);
});

it('should support refs', () => {
let tableRef = React.createRef();
let headerRef = React.createRef();
Expand Down
23 changes: 12 additions & 11 deletions packages/react-stately/src/grid/useGridState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,21 @@ export function useGridState<T extends object, C extends IGridCollection<T>>(
rows.length - 1
);
let newRow: GridNode<T> | null = null;
while (index >= 0) {
if (!selectionManager.isDisabled(rows[index].key) && rows[index].type !== 'headerrow') {
newRow = rows[index];
// Find the nearest focusable row at or after the deleted position...
for (let i = Math.max(0, index); i < rows.length; i++) {
if (!selectionManager.isDisabled(rows[i].key) && rows[i].type !== 'headerrow') {
newRow = rows[i];
break;
}
// Find next, not disabled row.
if (index < rows.length - 1) {
index++;
// Otherwise, find previous, not disabled row.
} else {
if (index > parentNode.index) {
index = parentNode.index;
}
// ...otherwise the nearest focusable row before it. (Mirrors useListState's
// getKeyAfter/getKeyBefore walk.)
if (newRow === null) {
for (let i = index - 1; i >= 0; i--) {
if (!selectionManager.isDisabled(rows[i].key) && rows[i].type !== 'headerrow') {
newRow = rows[i];
break;
}
index--;
}
}
if (newRow) {
Expand Down