Skip to content

Commit 4f2b248

Browse files
authored
fix: improve keyboard navigation when Table has expandable rows (#9842)
* fix: speed up keyboard navigation with Table expandable rows * fix tests * copy pasta * cleanup * fix lint?
1 parent 471ef60 commit 4f2b248

File tree

2 files changed

+63
-16
lines changed

2 files changed

+63
-16
lines changed

packages/@adobe/react-spectrum/test/table/TreeGridTable.test.tsx

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -692,42 +692,78 @@ describe('TableView with expandable rows', function () {
692692
expect(document.activeElement).toBe(getCell(treegrid, 'Row 1, Lvl 2, Foo'));
693693
});
694694

695-
it('should properly wrap focus with ArrowRight (RTL)', function () {
695+
it('should move focus to parent if already collapsed (RTL)', function () {
696696
let treegrid = render(<ManyRowsExpandableTable />, undefined, 'ar-AE');
697-
let row = treegrid.getAllByRole('row')[2];
697+
let row1 = treegrid.getAllByRole('row')[1];
698+
let row2 = treegrid.getAllByRole('row')[2];
698699
focusCell(treegrid, 'Row 1, Lvl 2, Foo');
699700
moveFocus('ArrowRight');
701+
expect(document.activeElement).toBe(row2);
702+
expect(row2).toContainElement(getCell(treegrid, 'Row 1, Lvl 2, Foo'));
703+
// Focus doesn't move because Arrow Right on the row will collapse it here
704+
moveFocus('ArrowRight');
705+
expect(document.activeElement).toBe(row2);
706+
moveFocus('ArrowRight');
707+
expect(document.activeElement).toBe(row1);
708+
moveFocus('ArrowRight');
709+
expect(document.activeElement).toBe(row1);
710+
moveFocus('ArrowRight');
711+
});
712+
713+
it('should properly wrap focus with ArrowRight when top level row is collapsed (RTL)', function () {
714+
let treegrid = render(<ManyRowsExpandableTable />, undefined, 'ar-AE');
715+
let row = treegrid.getAllByRole('row')[4];
716+
focusCell(treegrid, 'Row 2, Lvl 1, Foo');
717+
moveFocus('ArrowRight');
700718
expect(document.activeElement).toBe(row);
701-
expect(row).toContainElement(getCell(treegrid, 'Row 1, Lvl 2, Foo'));
702-
// Focus doesn't move because Arrow Right on the row will collapse it
719+
expect(row).toContainElement(getCell(treegrid, 'Row 2, Lvl 1, Foo'));
720+
// Focus doesn't move because Arrow Right on the row will collapse it here
703721
moveFocus('ArrowRight');
704722
expect(document.activeElement).toBe(row);
705723
moveFocus('ArrowRight');
706-
expect(document.activeElement).toBe(getCell(treegrid, 'Row 1, Lvl 2, Baz'));
724+
expect(document.activeElement).toBe(getCell(treegrid, 'Row 2, Lvl 1, Baz'));
707725
moveFocus('ArrowRight');
708-
expect(document.activeElement).toBe(getCell(treegrid, 'Row 1, Lvl 2, Bar'));
726+
expect(document.activeElement).toBe(getCell(treegrid, 'Row 2, Lvl 1, Bar'));
709727
moveFocus('ArrowRight');
710-
expect(document.activeElement).toBe(getCell(treegrid, 'Row 1, Lvl 2, Foo'));
728+
expect(document.activeElement).toBe(getCell(treegrid, 'Row 2, Lvl 1, Foo'));
711729
});
712730
});
713731

714732
describe('ArrowLeft', function () {
715-
it('should properly wrap focus with ArrowLeft', function () {
733+
it('should move focus to parent if already collapsed', function () {
716734
let treegrid = render(<ManyRowsExpandableTable />);
717-
let row = treegrid.getAllByRole('row')[2];
735+
let row1 = treegrid.getAllByRole('row')[1];
736+
let row2 = treegrid.getAllByRole('row')[2];
718737
focusCell(treegrid, 'Row 1, Lvl 2, Foo');
719738
moveFocus('ArrowLeft');
739+
expect(document.activeElement).toBe(row2);
740+
expect(row2).toContainElement(getCell(treegrid, 'Row 1, Lvl 2, Foo'));
741+
// Focus doesn't move because Arrow Left on the row will collapse it here
742+
moveFocus('ArrowLeft');
743+
expect(document.activeElement).toBe(row2);
744+
moveFocus('ArrowLeft');
745+
expect(document.activeElement).toBe(row1);
746+
moveFocus('ArrowLeft');
747+
expect(document.activeElement).toBe(row1);
748+
moveFocus('ArrowLeft');
749+
});
750+
751+
it('should properly wrap focus with ArrowLeft when top level row is collapsed', function () {
752+
let treegrid = render(<ManyRowsExpandableTable />);
753+
let row = treegrid.getAllByRole('row')[4];
754+
focusCell(treegrid, 'Row 2, Lvl 1, Foo');
755+
moveFocus('ArrowLeft');
720756
expect(document.activeElement).toBe(row);
721-
expect(row).toContainElement(getCell(treegrid, 'Row 1, Lvl 2, Foo'));
757+
expect(row).toContainElement(getCell(treegrid, 'Row 2, Lvl 1, Foo'));
722758
// Focus doesn't move because Arrow Left on the row will collapse it here
723759
moveFocus('ArrowLeft');
724760
expect(document.activeElement).toBe(row);
725761
moveFocus('ArrowLeft');
726-
expect(document.activeElement).toBe(getCell(treegrid, 'Row 1, Lvl 2, Baz'));
762+
expect(document.activeElement).toBe(getCell(treegrid, 'Row 2, Lvl 1, Baz'));
727763
moveFocus('ArrowLeft');
728-
expect(document.activeElement).toBe(getCell(treegrid, 'Row 1, Lvl 2, Bar'));
764+
expect(document.activeElement).toBe(getCell(treegrid, 'Row 2, Lvl 1, Bar'));
729765
moveFocus('ArrowLeft');
730-
expect(document.activeElement).toBe(getCell(treegrid, 'Row 1, Lvl 2, Foo'));
766+
expect(document.activeElement).toBe(getCell(treegrid, 'Row 2, Lvl 1, Foo'));
731767
});
732768

733769
it('should properly wrap focus with ArrowLeft (RTL)', function () {

packages/react-aria/src/table/useTableRow.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,20 @@ export function useTableRow<T>(props: GridRowProps<T>, state: TableState<T> | Tr
8484
if ((e.key === EXPANSION_KEYS['expand'][direction]) && state.selectionManager.focusedKey === treeNode.key && hasChildRows && state.expandedKeys !== 'all' && !state.expandedKeys.has(treeNode.key)) {
8585
state.toggleKey(treeNode.key);
8686
e.stopPropagation();
87-
} else if ((e.key === EXPANSION_KEYS['collapse'][direction]) && state.selectionManager.focusedKey === treeNode.key && hasChildRows && (state.expandedKeys === 'all' || state.expandedKeys.has(treeNode.key))) {
88-
state.toggleKey(treeNode.key);
89-
e.stopPropagation();
87+
} else if ((e.key === EXPANSION_KEYS['collapse'][direction]) && state.selectionManager.focusedKey === treeNode.key) {
88+
if (state.expandedKeys !== 'all') {
89+
if (hasChildRows && state.expandedKeys.has(treeNode.key)) {
90+
state.toggleKey(treeNode.key);
91+
e.stopPropagation();
92+
} else if (!state.expandedKeys.has(treeNode.key) && treeNode.parentKey && treeNode.level > 0) {
93+
// Item is a leaf or already collapsed, move focus to parent
94+
state.selectionManager.setFocusedKey(treeNode.parentKey);
95+
e.stopPropagation();
96+
}
97+
} else if (state.expandedKeys === 'all') {
98+
state.toggleKey(treeNode.key);
99+
e.stopPropagation();
100+
}
90101
}
91102
},
92103
'aria-expanded': hasChildRows ? state.expandedKeys === 'all' || state.expandedKeys.has(node.key) : undefined,

0 commit comments

Comments
 (0)