diff --git a/packages/@react-aria/test-utils/src/gridlist.ts b/packages/@react-aria/test-utils/src/gridlist.ts
index 2052131f678..3b5a32418a0 100644
--- a/packages/@react-aria/test-utils/src/gridlist.ts
+++ b/packages/@react-aria/test-utils/src/gridlist.ts
@@ -64,6 +64,11 @@ export class GridListTester {
if (targetIndex === -1) {
throw new Error('Option provided is not in the gridlist');
}
+
+ if (document.activeElement !== this._gridlist || !this._gridlist.contains(document.activeElement)) {
+ act(() => this._gridlist.focus());
+ }
+
if (document.activeElement === this._gridlist) {
await this.user.keyboard('[ArrowDown]');
} else if (this._gridlist.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
@@ -161,10 +166,6 @@ export class GridListTester {
return;
}
- if (document.activeElement !== this._gridlist || !this._gridlist.contains(document.activeElement)) {
- act(() => this._gridlist.focus());
- }
-
await this.keyboardNavigateToRow({row});
await this.user.keyboard('[Enter]');
} else {
diff --git a/packages/@react-aria/test-utils/src/listbox.ts b/packages/@react-aria/test-utils/src/listbox.ts
index 8fbec312b7b..12245a601b5 100644
--- a/packages/@react-aria/test-utils/src/listbox.ts
+++ b/packages/@react-aria/test-utils/src/listbox.ts
@@ -93,6 +93,10 @@ export class ListBoxTester {
throw new Error('Option provided is not in the listbox');
}
+ if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) {
+ act(() => this._listbox.focus());
+ }
+
if (document.activeElement === this._listbox) {
await this.user.keyboard('[ArrowDown]');
}
@@ -135,10 +139,6 @@ export class ListBoxTester {
return;
}
- if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) {
- act(() => this._listbox.focus());
- }
-
await this.keyboardNavigateToOption({option});
await this.user.keyboard(`[${keyboardActivation}]`);
} else {
@@ -179,10 +179,6 @@ export class ListBoxTester {
return;
}
- if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) {
- act(() => this._listbox.focus());
- }
-
await this.keyboardNavigateToOption({option});
await this.user.keyboard('[Enter]');
} else {
diff --git a/packages/@react-aria/test-utils/src/tabs.ts b/packages/@react-aria/test-utils/src/tabs.ts
index 8a940d4f1d3..91773649178 100644
--- a/packages/@react-aria/test-utils/src/tabs.ts
+++ b/packages/@react-aria/test-utils/src/tabs.ts
@@ -84,6 +84,10 @@ export class TabsTester {
throw new Error('Tab provided is not in the tablist');
}
+ if (document.activeElement !== this._tablist || !this._tablist.contains(document.activeElement)) {
+ act(() => this._tablist.focus());
+ }
+
if (!this._tablist.contains(document.activeElement)) {
let selectedTab = this.selectedTab;
if (selectedTab != null) {
@@ -137,10 +141,6 @@ export class TabsTester {
}
if (interactionType === 'keyboard') {
- if (document.activeElement !== this._tablist || !this._tablist.contains(document.activeElement)) {
- act(() => this._tablist.focus());
- }
-
let tabsOrientation = this._tablist.getAttribute('aria-orientation') || 'horizontal';
await this.keyboardNavigateToTab({tab, orientation: tabsOrientation as Orientation});
if (manualActivation) {
diff --git a/packages/@react-aria/test-utils/src/tree.ts b/packages/@react-aria/test-utils/src/tree.ts
index 93e4ec65ebf..968e4400742 100644
--- a/packages/@react-aria/test-utils/src/tree.ts
+++ b/packages/@react-aria/test-utils/src/tree.ts
@@ -71,6 +71,11 @@ export class TreeTester {
if (targetIndex === -1) {
throw new Error('Option provided is not in the tree');
}
+
+ if (document.activeElement !== this._tree || !this._tree.contains(document.activeElement)) {
+ act(() => this._tree.focus());
+ }
+
if (document.activeElement === this.tree) {
await this.user.keyboard('[ArrowDown]');
} else if (this._tree.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
@@ -89,6 +94,8 @@ export class TreeTester {
}
};
+ // TODO: we'll need to support selectionBehavior="replace", where clicks/keyboard will need to go through different flows
+ // for both single selection and multiple selection due to the nature of the selection replacement on focus
/**
* Toggles the selection for the specified tree row. Defaults to using the interaction type set on the tree tester.
*/
@@ -135,7 +142,7 @@ export class TreeTester {
// Note that long press interactions with rows is strictly touch only for grid rows
await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
} else {
- await pressElement(this.user, cell, interactionType);
+ await pressElement(this.user, row, interactionType);
}
}
};
@@ -206,10 +213,6 @@ export class TreeTester {
return;
}
- if (document.activeElement !== this._tree || !this._tree.contains(document.activeElement)) {
- act(() => this._tree.focus());
- }
-
await this.keyboardNavigateToRow({row});
await this.user.keyboard('[Enter]');
} else {
diff --git a/packages/@react-spectrum/tree/test/TreeView.test.tsx b/packages/@react-spectrum/tree/test/TreeView.test.tsx
index 5d2fee7c61d..23cdd6f25b2 100644
--- a/packages/@react-spectrum/tree/test/TreeView.test.tsx
+++ b/packages/@react-spectrum/tree/test/TreeView.test.tsx
@@ -463,6 +463,102 @@ describe('Tree', () => {
expect(treeTester.selectedRows[0]).toBe(row1);
});
+ it('highlight selection TreeView can select a row via keyboard', async function () {
+ let items = [
+ {
+ id: 'projects',
+ name: 'Projects',
+ childItems: [
+ {id: 'project-1', name: 'Project 1'},
+ {
+ id: 'project-2',
+ name: 'Project 2',
+ childItems: [
+ {id: 'document-a', name: 'Document A'},
+ {id: 'document-b', name: 'Document B'}
+ ]
+ }
+ ]
+ },
+ {
+ id: 'reports',
+ name: 'Reports',
+ childItems: [
+ {id: 'report-1', name: 'Reports 1'}
+ ]
+ }
+ ];
+
+ let {getByTestId} = render(
+
+ {(item: any) => (
+
+ )}
+
+ );
+ let treeTester = testUtilUser.createTester('Tree', {
+ user,
+ root: getByTestId('action-rail-tree'),
+ interactionType: 'keyboard'
+ });
+
+ let rows = treeTester.rows;
+ await treeTester.toggleRowSelection({row: 0});
+ expect(treeTester.selectedRows).toHaveLength(1);
+ expect(rows[0]).toHaveAttribute('aria-selected', 'true');
+
+ await treeTester.toggleRowSelection({row: 1});
+ expect(treeTester.selectedRows).toHaveLength(1);
+ expect(rows[1]).toHaveAttribute('aria-selected', 'true');
+
+ await treeTester.toggleRowSelection({row: 0});
+ expect(treeTester.selectedRows).toHaveLength(1);
+ expect(rows[0]).toHaveAttribute('aria-selected', 'true');
+ });
+
+ // TODO: replace the test above this one with a similar set up as the below
+ it('should perform selection for highlight mode with single selection', async () => {
+ let {getByRole} = render();
+ let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')});
+ let rows = treeTester.rows;
+
+ for (let row of treeTester.rows) {
+ let checkbox = within(row).queryByRole('checkbox');
+ expect(checkbox).toBeNull();
+ expect(row).toHaveAttribute('aria-selected', 'false');
+ expect(row).not.toHaveAttribute('data-selected');
+ expect(row).toHaveAttribute('data-selection-mode', 'multiple');
+ }
+
+ let row2 = rows[2];
+ await treeTester.toggleRowSelection({row: 'Projects-1'});
+ expect(row2).toHaveAttribute('aria-selected', 'true');
+ expect(row2).toHaveAttribute('data-selected', 'true');
+ expect(onSelectionChange).toHaveBeenCalledTimes(1);
+ expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Projects-1']));
+ expect(treeTester.selectedRows).toHaveLength(1);
+ expect(treeTester.selectedRows[0]).toBe(row2);
+
+ let row1 = rows[1];
+ await treeTester.toggleRowSelection({row: row1});
+ expect(row1).toHaveAttribute('aria-selected', 'true');
+ expect(row1).toHaveAttribute('data-selected', 'true');
+ expect(row2).toHaveAttribute('aria-selected', 'false');
+ expect(row2).not.toHaveAttribute('data-selected');
+ expect(onSelectionChange).toHaveBeenCalledTimes(2);
+ expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Projects']));
+ expect(treeTester.selectedRows).toHaveLength(1);
+ expect(treeTester.selectedRows[0]).toBe(row1);
+ });
+
it('should render a chevron for an expandable row marked with hasChildItems', () => {
let {getAllByRole} = render(