@react-aria/test-utils 1.0.0-alpha.7 → 1.0.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/combobox.main.js +5 -4
- package/dist/combobox.main.js.map +1 -1
- package/dist/combobox.mjs +5 -4
- package/dist/combobox.module.js +5 -4
- package/dist/combobox.module.js.map +1 -1
- package/dist/events.main.js +31 -1
- package/dist/events.main.js.map +1 -1
- package/dist/events.mjs +30 -2
- package/dist/events.module.js +30 -2
- package/dist/events.module.js.map +1 -1
- package/dist/gridlist.main.js +23 -9
- package/dist/gridlist.main.js.map +1 -1
- package/dist/gridlist.mjs +24 -10
- package/dist/gridlist.module.js +24 -10
- package/dist/gridlist.module.js.map +1 -1
- package/dist/listbox.main.js +22 -13
- package/dist/listbox.main.js.map +1 -1
- package/dist/listbox.mjs +23 -14
- package/dist/listbox.module.js +23 -14
- package/dist/listbox.module.js.map +1 -1
- package/dist/menu.main.js +1 -1
- package/dist/menu.main.js.map +1 -1
- package/dist/menu.mjs +1 -1
- package/dist/menu.module.js +1 -1
- package/dist/menu.module.js.map +1 -1
- package/dist/select.main.js +7 -4
- package/dist/select.main.js.map +1 -1
- package/dist/select.mjs +7 -4
- package/dist/select.module.js +7 -4
- package/dist/select.module.js.map +1 -1
- package/dist/table.main.js +42 -9
- package/dist/table.main.js.map +1 -1
- package/dist/table.mjs +44 -11
- package/dist/table.module.js +44 -11
- package/dist/table.module.js.map +1 -1
- package/dist/tabs.main.js +1 -1
- package/dist/tabs.main.js.map +1 -1
- package/dist/tabs.mjs +1 -1
- package/dist/tabs.module.js +1 -1
- package/dist/tabs.module.js.map +1 -1
- package/dist/tree.main.js +27 -8
- package/dist/tree.main.js.map +1 -1
- package/dist/tree.mjs +28 -9
- package/dist/tree.module.js +28 -9
- package/dist/tree.module.js.map +1 -1
- package/dist/types.d.ts +21 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/userEventMaps.main.js.map +1 -1
- package/dist/userEventMaps.module.js.map +1 -1
- package/package.json +7 -3
- package/src/combobox.ts +10 -7
- package/src/events.ts +34 -2
- package/src/gridlist.ts +35 -12
- package/src/listbox.ts +47 -19
- package/src/menu.ts +1 -1
- package/src/select.ts +10 -4
- package/src/table.ts +71 -10
- package/src/tabs.ts +1 -1
- package/src/tree.ts +40 -10
- package/src/types.ts +11 -1
- package/src/userEventMaps.ts +1 -1
package/src/events.ts
CHANGED
|
@@ -14,6 +14,37 @@ import {act, fireEvent} from '@testing-library/react';
|
|
|
14
14
|
import {UserOpts} from './types';
|
|
15
15
|
|
|
16
16
|
export const DEFAULT_LONG_PRESS_TIME = 500;
|
|
17
|
+
function testPlatform(re: RegExp) {
|
|
18
|
+
return typeof window !== 'undefined' && window.navigator != null
|
|
19
|
+
? re.test(window.navigator['userAgentData']?.platform || window.navigator.platform)
|
|
20
|
+
: false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cached(fn: () => boolean) {
|
|
24
|
+
if (process.env.NODE_ENV === 'test') {
|
|
25
|
+
return fn;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let res: boolean | null = null;
|
|
29
|
+
return () => {
|
|
30
|
+
if (res == null) {
|
|
31
|
+
res = fn();
|
|
32
|
+
}
|
|
33
|
+
return res;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isMac = cached(function () {
|
|
38
|
+
return testPlatform(/^Mac/i);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export function getAltKey(): 'Alt' | 'ControlLeft' {
|
|
42
|
+
return isMac() ? 'Alt' : 'ControlLeft';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getMetaKey(): 'MetaLeft' | 'ControlLeft' {
|
|
46
|
+
return isMac() ? 'MetaLeft' : 'ControlLeft';
|
|
47
|
+
}
|
|
17
48
|
|
|
18
49
|
/**
|
|
19
50
|
* Simulates a "long press" event on a element.
|
|
@@ -58,9 +89,10 @@ export async function triggerLongPress(opts: {element: HTMLElement, advanceTimer
|
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
// Docs cannot handle the types that userEvent actually declares, so hopefully this sub set is okay
|
|
61
|
-
export async function pressElement(user: {click: (element: Element) => Promise<void>, keyboard: (keys: string) => Promise<void>, pointer: (opts: {target: Element, keys: string}) => Promise<void>}, element: HTMLElement, interactionType: UserOpts['interactionType']): Promise<void> {
|
|
92
|
+
export async function pressElement(user: {click: (element: Element) => Promise<void>, keyboard: (keys: string) => Promise<void>, pointer: (opts: {target: Element, keys: string, coords?: any}) => Promise<void>}, element: HTMLElement, interactionType: UserOpts['interactionType']): Promise<void> {
|
|
62
93
|
if (interactionType === 'mouse') {
|
|
63
|
-
|
|
94
|
+
// Add coords with pressure so this isn't detected as a virtual click
|
|
95
|
+
await user.pointer({target: element, keys: '[MouseLeft]', coords: {pressure: .5}});
|
|
64
96
|
} else if (interactionType === 'keyboard') {
|
|
65
97
|
// TODO: For the keyboard flow, I wonder if it would be reasonable to just do fireEvent directly on the obtained row node or if we should
|
|
66
98
|
// stick to simulting an actual user's keyboard operations as closely as possible
|
package/src/gridlist.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {act, within} from '@testing-library/react';
|
|
14
|
+
import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
|
|
14
15
|
import {GridListTesterOpts, GridRowActionOpts, ToggleGridRowOpts, UserOpts} from './types';
|
|
15
|
-
import {pressElement, triggerLongPress} from './events';
|
|
16
16
|
|
|
17
17
|
interface GridListToggleRowOpts extends ToggleGridRowOpts {}
|
|
18
18
|
interface GridListRowActionOpts extends GridRowActionOpts {}
|
|
@@ -57,20 +57,21 @@ export class GridListTester {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// TODO: RTL
|
|
60
|
-
private async keyboardNavigateToRow(opts: {row: HTMLElement}) {
|
|
61
|
-
let {row} = opts;
|
|
60
|
+
private async keyboardNavigateToRow(opts: {row: HTMLElement, selectionOnNav?: 'default' | 'none'}) {
|
|
61
|
+
let {row, selectionOnNav = 'default'} = opts;
|
|
62
|
+
let altKey = getAltKey();
|
|
62
63
|
let rows = this.rows;
|
|
63
64
|
let targetIndex = rows.indexOf(row);
|
|
64
65
|
if (targetIndex === -1) {
|
|
65
66
|
throw new Error('Option provided is not in the gridlist');
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
if (document.activeElement !== this._gridlist
|
|
69
|
+
if (document.activeElement !== this._gridlist && !this._gridlist.contains(document.activeElement)) {
|
|
69
70
|
act(() => this._gridlist.focus());
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
if (document.activeElement === this._gridlist) {
|
|
73
|
-
await this.user.keyboard('[ArrowDown]');
|
|
74
|
+
await this.user.keyboard(`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`);
|
|
74
75
|
} else if (this._gridlist.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
|
|
75
76
|
do {
|
|
76
77
|
await this.user.keyboard('[ArrowLeft]');
|
|
@@ -82,22 +83,33 @@ export class GridListTester {
|
|
|
82
83
|
}
|
|
83
84
|
let direction = targetIndex > currIndex ? 'down' : 'up';
|
|
84
85
|
|
|
86
|
+
if (selectionOnNav === 'none') {
|
|
87
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
88
|
+
}
|
|
85
89
|
for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
|
|
86
90
|
await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
|
|
87
91
|
}
|
|
92
|
+
if (selectionOnNav === 'none') {
|
|
93
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
94
|
+
}
|
|
88
95
|
};
|
|
89
96
|
|
|
90
97
|
/**
|
|
91
98
|
* Toggles the selection for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester.
|
|
99
|
+
* Note that this will endevor to always add/remove JUST the provided row to the set of selected rows.
|
|
92
100
|
*/
|
|
93
101
|
async toggleRowSelection(opts: GridListToggleRowOpts): Promise<void> {
|
|
94
102
|
let {
|
|
95
103
|
row,
|
|
96
104
|
needsLongPress,
|
|
97
105
|
checkboxSelection = true,
|
|
98
|
-
interactionType = this._interactionType
|
|
106
|
+
interactionType = this._interactionType,
|
|
107
|
+
selectionBehavior = 'toggle'
|
|
99
108
|
} = opts;
|
|
100
109
|
|
|
110
|
+
let altKey = getAltKey();
|
|
111
|
+
let metaKey = getMetaKey();
|
|
112
|
+
|
|
101
113
|
if (typeof row === 'string' || typeof row === 'number') {
|
|
102
114
|
row = this.findRow({rowIndexOrText: row});
|
|
103
115
|
}
|
|
@@ -116,9 +128,15 @@ export class GridListTester {
|
|
|
116
128
|
|
|
117
129
|
// this would be better than the check to do nothing in events.ts
|
|
118
130
|
// also, it'd be good to be able to trigger selection on the row instead of having to go to the checkbox directly
|
|
119
|
-
if (interactionType === 'keyboard' && !checkboxSelection) {
|
|
120
|
-
await this.keyboardNavigateToRow({row});
|
|
121
|
-
|
|
131
|
+
if (interactionType === 'keyboard' && (!checkboxSelection || !rowCheckbox)) {
|
|
132
|
+
await this.keyboardNavigateToRow({row, selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'});
|
|
133
|
+
if (selectionBehavior === 'replace') {
|
|
134
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
135
|
+
}
|
|
136
|
+
await this.user.keyboard('[Space]');
|
|
137
|
+
if (selectionBehavior === 'replace') {
|
|
138
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
139
|
+
}
|
|
122
140
|
return;
|
|
123
141
|
}
|
|
124
142
|
if (rowCheckbox && checkboxSelection) {
|
|
@@ -132,9 +150,14 @@ export class GridListTester {
|
|
|
132
150
|
|
|
133
151
|
// Note that long press interactions with rows is strictly touch only for grid rows
|
|
134
152
|
await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
|
|
135
|
-
|
|
136
153
|
} else {
|
|
137
|
-
|
|
154
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
155
|
+
await this.user.keyboard(`[${metaKey}>]`);
|
|
156
|
+
}
|
|
157
|
+
await pressElement(this.user, row, interactionType);
|
|
158
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
159
|
+
await this.user.keyboard(`[/${metaKey}]`);
|
|
160
|
+
}
|
|
138
161
|
}
|
|
139
162
|
}
|
|
140
163
|
}
|
|
@@ -166,7 +189,7 @@ export class GridListTester {
|
|
|
166
189
|
return;
|
|
167
190
|
}
|
|
168
191
|
|
|
169
|
-
await this.keyboardNavigateToRow({row});
|
|
192
|
+
await this.keyboardNavigateToRow({row, selectionOnNav: 'none'});
|
|
170
193
|
await this.user.keyboard('[Enter]');
|
|
171
194
|
} else {
|
|
172
195
|
await pressElement(this.user, row, interactionType);
|
package/src/listbox.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {act, within} from '@testing-library/react';
|
|
14
|
+
import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
|
|
14
15
|
import {ListBoxTesterOpts, UserOpts} from './types';
|
|
15
|
-
import {pressElement, triggerLongPress} from './events';
|
|
16
16
|
|
|
17
17
|
interface ListBoxToggleOptionOpts {
|
|
18
18
|
/**
|
|
@@ -31,7 +31,16 @@ interface ListBoxToggleOptionOpts {
|
|
|
31
31
|
/**
|
|
32
32
|
* Whether the option needs to be long pressed to be selected. Depends on the listbox's implementation.
|
|
33
33
|
*/
|
|
34
|
-
needsLongPress?: boolean
|
|
34
|
+
needsLongPress?: boolean,
|
|
35
|
+
/**
|
|
36
|
+
* Whether the listbox has a selectionBehavior of "toggle" or "replace" (aka highlight selection). This affects the user operations
|
|
37
|
+
* required to toggle option selection by adding modifier keys during user actions, useful when performing multi-option selection in a "selectionBehavior: 'replace'" listbox.
|
|
38
|
+
* If you would like to still simulate user actions (aka press) without these modifiers keys for a "selectionBehavior: replace" listbox, simply omit this option.
|
|
39
|
+
* See the [RAC Listbox docs](https://react-spectrum.adobe.com/react-aria/ListBox.html#selection-behavior) for more info on this behavior.
|
|
40
|
+
*
|
|
41
|
+
* @default 'toggle'
|
|
42
|
+
*/
|
|
43
|
+
selectionBehavior?: 'toggle' | 'replace'
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
interface ListBoxOptionActionOpts extends Omit<ListBoxToggleOptionOpts, 'keyboardActivation' | 'needsLongPress'> {
|
|
@@ -85,44 +94,51 @@ export class ListBoxTester {
|
|
|
85
94
|
|
|
86
95
|
// TODO: this is basically the same as menu except for the error message, refactor later so that they share
|
|
87
96
|
// TODO: this also doesn't support grid layout yet
|
|
88
|
-
private async keyboardNavigateToOption(opts: {option: HTMLElement}) {
|
|
89
|
-
let {option} = opts;
|
|
97
|
+
private async keyboardNavigateToOption(opts: {option: HTMLElement, selectionOnNav?: 'default' | 'none'}) {
|
|
98
|
+
let {option, selectionOnNav = 'default'} = opts;
|
|
99
|
+
let altKey = getAltKey();
|
|
90
100
|
let options = this.options();
|
|
91
101
|
let targetIndex = options.indexOf(option);
|
|
92
102
|
if (targetIndex === -1) {
|
|
93
103
|
throw new Error('Option provided is not in the listbox');
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
if (document.activeElement !== this._listbox
|
|
106
|
+
if (document.activeElement !== this._listbox && !this._listbox.contains(document.activeElement)) {
|
|
97
107
|
act(() => this._listbox.focus());
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await this.user.keyboard('[ArrowDown]');
|
|
101
|
-
|
|
102
|
-
// TODO: not sure about doing same while loop that exists in other implementations of keyboardNavigateToOption,
|
|
103
|
-
// feels like it could break easily
|
|
104
|
-
if (document.activeElement?.getAttribute('role') !== 'option') {
|
|
105
|
-
await act(async () => {
|
|
106
|
-
option.focus();
|
|
107
|
-
});
|
|
108
|
+
await this.user.keyboard(`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`);
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
let currIndex = options.indexOf(document.activeElement as HTMLElement);
|
|
111
112
|
if (currIndex === -1) {
|
|
112
113
|
throw new Error('ActiveElement is not in the listbox');
|
|
113
114
|
}
|
|
114
|
-
let direction = targetIndex > currIndex ? 'down' : 'up';
|
|
115
115
|
|
|
116
|
+
let direction = targetIndex > currIndex ? 'down' : 'up';
|
|
117
|
+
if (selectionOnNav === 'none') {
|
|
118
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
119
|
+
}
|
|
116
120
|
for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
|
|
117
121
|
await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
|
|
118
122
|
}
|
|
123
|
+
if (selectionOnNav === 'none') {
|
|
124
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
125
|
+
}
|
|
119
126
|
};
|
|
120
127
|
|
|
121
128
|
/**
|
|
122
129
|
* Toggles the selection for the specified listbox option. Defaults to using the interaction type set on the listbox tester.
|
|
123
130
|
*/
|
|
124
131
|
async toggleOptionSelection(opts: ListBoxToggleOptionOpts): Promise<void> {
|
|
125
|
-
let {
|
|
132
|
+
let {
|
|
133
|
+
option,
|
|
134
|
+
needsLongPress,
|
|
135
|
+
keyboardActivation = 'Enter',
|
|
136
|
+
interactionType = this._interactionType,
|
|
137
|
+
selectionBehavior = 'toggle'
|
|
138
|
+
} = opts;
|
|
139
|
+
|
|
140
|
+
let altKey = getAltKey();
|
|
141
|
+
let metaKey = getMetaKey();
|
|
126
142
|
|
|
127
143
|
if (typeof option === 'string' || typeof option === 'number') {
|
|
128
144
|
option = this.findOption({optionIndexOrText: option});
|
|
@@ -137,8 +153,14 @@ export class ListBoxTester {
|
|
|
137
153
|
return;
|
|
138
154
|
}
|
|
139
155
|
|
|
140
|
-
await this.keyboardNavigateToOption({option});
|
|
156
|
+
await this.keyboardNavigateToOption({option, selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'});
|
|
157
|
+
if (selectionBehavior === 'replace') {
|
|
158
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
159
|
+
}
|
|
141
160
|
await this.user.keyboard(`[${keyboardActivation}]`);
|
|
161
|
+
if (selectionBehavior === 'replace') {
|
|
162
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
163
|
+
}
|
|
142
164
|
} else {
|
|
143
165
|
if (needsLongPress && interactionType === 'touch') {
|
|
144
166
|
if (this._advanceTimer == null) {
|
|
@@ -147,7 +169,13 @@ export class ListBoxTester {
|
|
|
147
169
|
|
|
148
170
|
await triggerLongPress({element: option, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
|
|
149
171
|
} else {
|
|
172
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
173
|
+
await this.user.keyboard(`[${metaKey}>]`);
|
|
174
|
+
}
|
|
150
175
|
await pressElement(this.user, option, interactionType);
|
|
176
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
177
|
+
await this.user.keyboard(`[/${metaKey}]`);
|
|
178
|
+
}
|
|
151
179
|
}
|
|
152
180
|
}
|
|
153
181
|
}
|
|
@@ -177,7 +205,7 @@ export class ListBoxTester {
|
|
|
177
205
|
return;
|
|
178
206
|
}
|
|
179
207
|
|
|
180
|
-
await this.keyboardNavigateToOption({option});
|
|
208
|
+
await this.keyboardNavigateToOption({option, selectionOnNav: 'none'});
|
|
181
209
|
await this.user.keyboard('[Enter]');
|
|
182
210
|
} else {
|
|
183
211
|
await pressElement(this.user, option, interactionType);
|
package/src/menu.ts
CHANGED
|
@@ -215,7 +215,7 @@ export class MenuTester {
|
|
|
215
215
|
return;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
if (document.activeElement !== menu
|
|
218
|
+
if (document.activeElement !== menu && !menu.contains(document.activeElement)) {
|
|
219
219
|
act(() => menu.focus());
|
|
220
220
|
}
|
|
221
221
|
|
package/src/select.ts
CHANGED
|
@@ -37,11 +37,17 @@ export class SelectTester {
|
|
|
37
37
|
this.user = user;
|
|
38
38
|
this._interactionType = interactionType || 'mouse';
|
|
39
39
|
// Handle case where the wrapper element is provided rather than the Select's button (aka RAC)
|
|
40
|
-
let
|
|
41
|
-
|
|
40
|
+
let buttons = within(root).queryAllByRole('button');
|
|
41
|
+
let triggerButton;
|
|
42
|
+
if (buttons.length === 0) {
|
|
42
43
|
triggerButton = root;
|
|
44
|
+
} else if (buttons.length === 1) {
|
|
45
|
+
triggerButton = buttons[0];
|
|
46
|
+
} else {
|
|
47
|
+
triggerButton = buttons.find(button => button.hasAttribute('aria-haspopup'));
|
|
43
48
|
}
|
|
44
|
-
|
|
49
|
+
|
|
50
|
+
this._trigger = triggerButton ?? root;
|
|
45
51
|
}
|
|
46
52
|
/**
|
|
47
53
|
* Set the interaction type used by the select tester.
|
|
@@ -183,7 +189,7 @@ export class SelectTester {
|
|
|
183
189
|
return;
|
|
184
190
|
}
|
|
185
191
|
|
|
186
|
-
if (document.activeElement !== listbox
|
|
192
|
+
if (document.activeElement !== listbox && !listbox.contains(document.activeElement)) {
|
|
187
193
|
act(() => listbox.focus());
|
|
188
194
|
}
|
|
189
195
|
await this.keyboardNavigateToOption({option});
|
package/src/table.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {act, waitFor, within} from '@testing-library/react';
|
|
14
|
+
import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
|
|
14
15
|
import {GridRowActionOpts, TableTesterOpts, ToggleGridRowOpts, UserOpts} from './types';
|
|
15
|
-
import {pressElement, triggerLongPress} from './events';
|
|
16
16
|
|
|
17
17
|
interface TableToggleRowOpts extends ToggleGridRowOpts {}
|
|
18
18
|
interface TableToggleSortOpts {
|
|
@@ -54,6 +54,55 @@ export class TableTester {
|
|
|
54
54
|
this._interactionType = type;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// TODO: RTL
|
|
58
|
+
private async keyboardNavigateToRow(opts: {row: HTMLElement, selectionOnNav?: 'default' | 'none'}) {
|
|
59
|
+
let {row, selectionOnNav = 'default'} = opts;
|
|
60
|
+
let altKey = getAltKey();
|
|
61
|
+
let rows = this.rows;
|
|
62
|
+
let targetIndex = rows.indexOf(row);
|
|
63
|
+
if (targetIndex === -1) {
|
|
64
|
+
throw new Error('Row provided is not in the table');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Move focus into the table
|
|
68
|
+
if (document.activeElement !== this._table && !this._table.contains(document.activeElement)) {
|
|
69
|
+
act(() => this._table.focus());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (document.activeElement === this._table) {
|
|
73
|
+
await this.user.keyboard('[ArrowDown]');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If focus is currently somewhere in the first row group (aka on a column), we want to keyboard navigate downwards till we reach the rows
|
|
77
|
+
if (this.rowGroups[0].contains(document.activeElement)) {
|
|
78
|
+
do {
|
|
79
|
+
await this.user.keyboard('[ArrowDown]');
|
|
80
|
+
} while (!this.rowGroups[1].contains(document.activeElement));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Move focus onto the row itself
|
|
84
|
+
if (this.rowGroups[1].contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
|
|
85
|
+
do {
|
|
86
|
+
await this.user.keyboard('[ArrowLeft]');
|
|
87
|
+
} while (document.activeElement!.getAttribute('role') !== 'row');
|
|
88
|
+
}
|
|
89
|
+
let currIndex = rows.indexOf(document.activeElement as HTMLElement);
|
|
90
|
+
if (currIndex === -1) {
|
|
91
|
+
throw new Error('Current active element is not on any of the table rows');
|
|
92
|
+
}
|
|
93
|
+
let direction = targetIndex > currIndex ? 'down' : 'up';
|
|
94
|
+
|
|
95
|
+
if (selectionOnNav === 'none') {
|
|
96
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
97
|
+
}
|
|
98
|
+
for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
|
|
99
|
+
await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
|
|
100
|
+
}
|
|
101
|
+
if (selectionOnNav === 'none') {
|
|
102
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
57
106
|
/**
|
|
58
107
|
* Toggles the selection for the specified table row. Defaults to using the interaction type set on the table tester.
|
|
59
108
|
*/
|
|
@@ -62,9 +111,13 @@ export class TableTester {
|
|
|
62
111
|
row,
|
|
63
112
|
needsLongPress,
|
|
64
113
|
checkboxSelection = true,
|
|
65
|
-
interactionType = this._interactionType
|
|
114
|
+
interactionType = this._interactionType,
|
|
115
|
+
selectionBehavior = 'toggle'
|
|
66
116
|
} = opts;
|
|
67
117
|
|
|
118
|
+
let altKey = getMetaKey();
|
|
119
|
+
let metaKey = getMetaKey();
|
|
120
|
+
|
|
68
121
|
if (typeof row === 'string' || typeof row === 'number') {
|
|
69
122
|
row = this.findRow({rowIndexOrText: row});
|
|
70
123
|
}
|
|
@@ -75,12 +128,15 @@ export class TableTester {
|
|
|
75
128
|
|
|
76
129
|
let rowCheckbox = within(row).queryByRole('checkbox');
|
|
77
130
|
|
|
78
|
-
if (interactionType === 'keyboard' && !checkboxSelection) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
await this.user.keyboard('
|
|
131
|
+
if (interactionType === 'keyboard' && (!checkboxSelection || !rowCheckbox)) {
|
|
132
|
+
await this.keyboardNavigateToRow({row, selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'});
|
|
133
|
+
if (selectionBehavior === 'replace') {
|
|
134
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
135
|
+
}
|
|
136
|
+
await this.user.keyboard('[Space]');
|
|
137
|
+
if (selectionBehavior === 'replace') {
|
|
138
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
139
|
+
}
|
|
84
140
|
return;
|
|
85
141
|
}
|
|
86
142
|
if (rowCheckbox && checkboxSelection) {
|
|
@@ -95,7 +151,13 @@ export class TableTester {
|
|
|
95
151
|
// Note that long press interactions with rows is strictly touch only for grid rows
|
|
96
152
|
await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
|
|
97
153
|
} else {
|
|
154
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
155
|
+
await this.user.keyboard(`[${metaKey}>]`);
|
|
156
|
+
}
|
|
98
157
|
await pressElement(this.user, cell, interactionType);
|
|
158
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
159
|
+
await this.user.keyboard(`[/${metaKey}]`);
|
|
160
|
+
}
|
|
99
161
|
}
|
|
100
162
|
}
|
|
101
163
|
};
|
|
@@ -297,8 +359,7 @@ export class TableTester {
|
|
|
297
359
|
if (needsDoubleClick) {
|
|
298
360
|
await this.user.dblClick(row);
|
|
299
361
|
} else if (interactionType === 'keyboard') {
|
|
300
|
-
|
|
301
|
-
act(() => row.focus());
|
|
362
|
+
await this.keyboardNavigateToRow({row, selectionOnNav: 'none'});
|
|
302
363
|
await this.user.keyboard('[Enter]');
|
|
303
364
|
} else {
|
|
304
365
|
await pressElement(this.user, row, interactionType);
|
package/src/tabs.ts
CHANGED
|
@@ -137,7 +137,7 @@ export class TabsTester {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
if (interactionType === 'keyboard') {
|
|
140
|
-
if (document.activeElement !== this._tablist
|
|
140
|
+
if (document.activeElement !== this._tablist && !this._tablist.contains(document.activeElement)) {
|
|
141
141
|
act(() => this._tablist.focus());
|
|
142
142
|
}
|
|
143
143
|
|
package/src/tree.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import {act, within} from '@testing-library/react';
|
|
14
14
|
import {BaseGridRowInteractionOpts, GridRowActionOpts, ToggleGridRowOpts, TreeTesterOpts, UserOpts} from './types';
|
|
15
|
-
import {pressElement, triggerLongPress} from './events';
|
|
15
|
+
import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
|
|
16
16
|
|
|
17
17
|
interface TreeToggleExpansionOpts extends BaseGridRowInteractionOpts {}
|
|
18
18
|
interface TreeToggleRowOpts extends ToggleGridRowOpts {}
|
|
@@ -64,20 +64,21 @@ export class TreeTester {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// TODO: RTL
|
|
67
|
-
private async keyboardNavigateToRow(opts: {row: HTMLElement}) {
|
|
68
|
-
let {row} = opts;
|
|
67
|
+
private async keyboardNavigateToRow(opts: {row: HTMLElement, selectionOnNav?: 'default' | 'none'}) {
|
|
68
|
+
let {row, selectionOnNav = 'default'} = opts;
|
|
69
|
+
let altKey = getAltKey();
|
|
69
70
|
let rows = this.rows;
|
|
70
71
|
let targetIndex = rows.indexOf(row);
|
|
71
72
|
if (targetIndex === -1) {
|
|
72
73
|
throw new Error('Option provided is not in the tree');
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
if (document.activeElement !== this._tree
|
|
76
|
+
if (document.activeElement !== this._tree && !this._tree.contains(document.activeElement)) {
|
|
76
77
|
act(() => this._tree.focus());
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
if (document.activeElement === this.tree) {
|
|
80
|
-
await this.user.keyboard('[ArrowDown]');
|
|
81
|
+
await this.user.keyboard(`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`);
|
|
81
82
|
} else if (this._tree.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
|
|
82
83
|
do {
|
|
83
84
|
await this.user.keyboard('[ArrowLeft]');
|
|
@@ -89,22 +90,33 @@ export class TreeTester {
|
|
|
89
90
|
}
|
|
90
91
|
let direction = targetIndex > currIndex ? 'down' : 'up';
|
|
91
92
|
|
|
93
|
+
if (selectionOnNav === 'none') {
|
|
94
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
95
|
+
}
|
|
92
96
|
for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
|
|
93
97
|
await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
|
|
94
98
|
}
|
|
99
|
+
if (selectionOnNav === 'none') {
|
|
100
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
101
|
+
}
|
|
95
102
|
};
|
|
96
103
|
|
|
97
104
|
/**
|
|
98
105
|
* Toggles the selection for the specified tree row. Defaults to using the interaction type set on the tree tester.
|
|
106
|
+
* Note that this will endevor to always add/remove JUST the provided row to the set of selected rows.
|
|
99
107
|
*/
|
|
100
108
|
async toggleRowSelection(opts: TreeToggleRowOpts): Promise<void> {
|
|
101
109
|
let {
|
|
102
110
|
row,
|
|
103
111
|
needsLongPress,
|
|
104
112
|
checkboxSelection = true,
|
|
105
|
-
interactionType = this._interactionType
|
|
113
|
+
interactionType = this._interactionType,
|
|
114
|
+
selectionBehavior = 'toggle'
|
|
106
115
|
} = opts;
|
|
107
116
|
|
|
117
|
+
let altKey = getAltKey();
|
|
118
|
+
let metaKey = getMetaKey();
|
|
119
|
+
|
|
108
120
|
if (typeof row === 'string' || typeof row === 'number') {
|
|
109
121
|
row = this.findRow({rowIndexOrText: row});
|
|
110
122
|
}
|
|
@@ -123,9 +135,15 @@ export class TreeTester {
|
|
|
123
135
|
|
|
124
136
|
// this would be better than the check to do nothing in events.ts
|
|
125
137
|
// also, it'd be good to be able to trigger selection on the row instead of having to go to the checkbox directly
|
|
126
|
-
if (interactionType === 'keyboard' && !checkboxSelection) {
|
|
127
|
-
await this.keyboardNavigateToRow({row});
|
|
128
|
-
|
|
138
|
+
if (interactionType === 'keyboard' && (!checkboxSelection || !rowCheckbox)) {
|
|
139
|
+
await this.keyboardNavigateToRow({row, selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'});
|
|
140
|
+
if (selectionBehavior === 'replace') {
|
|
141
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
142
|
+
}
|
|
143
|
+
await this.user.keyboard('[Space]');
|
|
144
|
+
if (selectionBehavior === 'replace') {
|
|
145
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
146
|
+
}
|
|
129
147
|
return;
|
|
130
148
|
}
|
|
131
149
|
if (rowCheckbox && checkboxSelection) {
|
|
@@ -140,7 +158,14 @@ export class TreeTester {
|
|
|
140
158
|
// Note that long press interactions with rows is strictly touch only for grid rows
|
|
141
159
|
await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
|
|
142
160
|
} else {
|
|
143
|
-
|
|
161
|
+
// TODO add modifiers here? Maybe move into pressElement if we get more cases for different types of modifier keys
|
|
162
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
163
|
+
await this.user.keyboard(`[${metaKey}>]`);
|
|
164
|
+
}
|
|
165
|
+
await pressElement(this.user, row, interactionType);
|
|
166
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
167
|
+
await this.user.keyboard(`[/${metaKey}]`);
|
|
168
|
+
}
|
|
144
169
|
}
|
|
145
170
|
}
|
|
146
171
|
};
|
|
@@ -177,6 +202,9 @@ export class TreeTester {
|
|
|
177
202
|
return;
|
|
178
203
|
}
|
|
179
204
|
|
|
205
|
+
// TODO: We always Use Option/Ctrl when keyboard navigating so selection isn't changed
|
|
206
|
+
// in selectionmode="replace"/highlight selection when navigating to the row that the user wants
|
|
207
|
+
// to expand. Discuss if this is useful or not
|
|
180
208
|
await this.keyboardNavigateToRow({row});
|
|
181
209
|
if (row.getAttribute('aria-expanded') === 'true') {
|
|
182
210
|
await this.user.keyboard('[ArrowLeft]');
|
|
@@ -211,6 +239,8 @@ export class TreeTester {
|
|
|
211
239
|
return;
|
|
212
240
|
}
|
|
213
241
|
|
|
242
|
+
// TODO: same as above, uses the modifier key to make sure we don't modify selection state on row focus
|
|
243
|
+
// as we keyboard navigate to the row we want activate
|
|
214
244
|
await this.keyboardNavigateToRow({row});
|
|
215
245
|
await this.user.keyboard('[Enter]');
|
|
216
246
|
} else {
|
package/src/types.ts
CHANGED
|
@@ -126,7 +126,17 @@ export interface ToggleGridRowOpts extends BaseGridRowInteractionOpts {
|
|
|
126
126
|
* Whether the checkbox should be used to select the row. If false, will attempt to select the row via press.
|
|
127
127
|
* @default 'true'
|
|
128
128
|
*/
|
|
129
|
-
checkboxSelection?: boolean
|
|
129
|
+
checkboxSelection?: boolean,
|
|
130
|
+
// TODO: this api feels a bit confusing tbh...
|
|
131
|
+
/**
|
|
132
|
+
* Whether the grid has a selectionBehavior of "toggle" or "replace" (aka highlight selection). This affects the user operations
|
|
133
|
+
* required to toggle row selection by adding modifier keys during user actions, useful when performing multi-row selection in a "selectionBehavior: 'replace'" grid.
|
|
134
|
+
* If you would like to still simulate user actions (aka press) without these modifiers keys for a "selectionBehavior: replace" grid, simply omit this option.
|
|
135
|
+
* See the "Selection Behavior" section of the appropriate React Aria Component docs for more information (e.g. https://react-spectrum.adobe.com/react-aria/Tree.html#selection-behavior).
|
|
136
|
+
*
|
|
137
|
+
* @default 'toggle'
|
|
138
|
+
*/
|
|
139
|
+
selectionBehavior?: 'toggle' | 'replace'
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
export interface GridRowActionOpts extends BaseGridRowInteractionOpts {
|
package/src/userEventMaps.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {pointerKey} from '@testing-library/user-event
|
|
1
|
+
import {pointerKey} from '@testing-library/user-event';
|
|
2
2
|
|
|
3
3
|
export let pointerMap: pointerKey[] = [
|
|
4
4
|
{name: 'MouseLeft', pointerType: 'mouse', button: 'primary', height: 1, width: 1, pressure: 0.5},
|