@react-aria/test-utils 1.0.0-alpha.7 → 1.0.0-beta.0
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 +29 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/user.main.js.map +1 -1
- package/dist/user.module.js.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/user.ts +10 -1
- package/src/userEventMaps.ts +1 -1
package/dist/user.module.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":";;;;;;;;;;;AAAA;;;;;;;;;;CAUC;;;;;;;;;;AAwBD,IAAI,
|
|
1
|
+
{"mappings":";;;;;;;;;;;AAAA;;;;;;;;;;CAUC;;;;;;;;;;AAwBD,IAAI,kCASA;IACF,UAAU,CAAA,GAAA,yCAAW;IACrB,SAAS,CAAA,GAAA,wCAAU;IACnB,QAAQ,CAAA,GAAA,yCAAS;IACjB,YAAY,CAAA,GAAA,yCAAa;IACzB,YAAY,CAAA,GAAA,yCAAa;IACzB,WAAW,CAAA,GAAA,yCAAY;IACvB,QAAQ,CAAA,GAAA,yCAAS;IACjB,QAAQ,CAAA,GAAA,yCAAS;AACnB;AA0BA,IAAI,4CAAsB,CAAC,WAAiC,IAAI,QAAQ,CAAC,UAAY,WAAW,SAAS;AAElG,MAAM;IAoBX;;GAEC,GACD,aAAqC,WAAc,EAAE,IAAmB,EAAa;QACnF,OAAO,IAAI,AAAC,+BAAU,CAAC,YAAY,CAAC;YAAC,iBAAiB,IAAI,CAAC,eAAe;YAAE,cAAc,IAAI,CAAC,YAAY;YAAE,GAAG,IAAI;YAAE,MAAM,IAAI,CAAC,IAAI;QAAA;IACvI;IAZA,YAAY,OAAiB,CAAC,CAAC,CAAE;QAC/B,IAAI,mBAAC,eAAe,gBAAE,YAAY,EAAC,GAAG;QACtC,IAAI,CAAC,IAAI,GAAG,CAAA,GAAA,8BAAQ,EAAE,KAAK,CAAC;YAAC,OAAO;wBAAM;QAAU;QACpD,IAAI,CAAC,eAAe,GAAG;QACvB,IAAI,CAAC,YAAY,GAAG,gBAAgB;IACtC;AAQF","sources":["packages/@react-aria/test-utils/src/user.ts"],"sourcesContent":["/*\n * Copyright 2024 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {ComboBoxTester} from './combobox';\nimport {\n ComboBoxTesterOpts,\n GridListTesterOpts,\n ListBoxTesterOpts,\n MenuTesterOpts,\n SelectTesterOpts,\n TableTesterOpts,\n TabsTesterOpts,\n TreeTesterOpts,\n UserOpts\n} from './types';\nimport {GridListTester} from './gridlist';\nimport {ListBoxTester} from './listbox';\nimport {MenuTester} from './menu';\nimport {pointerMap} from './';\nimport {SelectTester} from './select';\nimport {TableTester} from './table';\nimport {TabsTester} from './tabs';\nimport {TreeTester} from './tree';\nimport userEvent from '@testing-library/user-event';\n\nlet keyToUtil: {\n 'Select': typeof SelectTester,\n 'Table': typeof TableTester,\n 'Menu': typeof MenuTester,\n 'ComboBox': typeof ComboBoxTester,\n 'GridList': typeof GridListTester,\n 'ListBox': typeof ListBoxTester,\n 'Tabs': typeof TabsTester,\n 'Tree': typeof TreeTester\n} = {\n 'Select': SelectTester,\n 'Table': TableTester,\n 'Menu': MenuTester,\n 'ComboBox': ComboBoxTester,\n 'GridList': GridListTester,\n 'ListBox': ListBoxTester,\n 'Tabs': TabsTester,\n 'Tree': TreeTester\n} as const;\nexport type PatternNames = keyof typeof keyToUtil;\n\n// Conditional type: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html\ntype Tester<T> =\n T extends 'ComboBox' ? ComboBoxTester :\n T extends 'GridList' ? GridListTester :\n T extends 'ListBox' ? ListBoxTester :\n T extends 'Menu' ? MenuTester :\n T extends 'Select' ? SelectTester :\n T extends 'Table' ? TableTester :\n T extends 'Tabs' ? TabsTester :\n T extends 'Tree' ? TreeTester :\n never;\n\ntype TesterOpts<T> =\n T extends 'ComboBox' ? ComboBoxTesterOpts :\n T extends 'GridList' ? GridListTesterOpts :\n T extends 'ListBox' ? ListBoxTesterOpts :\n T extends 'Menu' ? MenuTesterOpts :\n T extends 'Select' ? SelectTesterOpts :\n T extends 'Table' ? TableTesterOpts :\n T extends 'Tabs' ? TabsTesterOpts :\n T extends 'Tree' ? TreeTesterOpts :\n never;\n\nlet defaultAdvanceTimer = (waitTime: number | undefined) => new Promise((resolve) => setTimeout(resolve, waitTime));\n\nexport class User {\n private user;\n /**\n * The interaction type (mouse, touch, keyboard) that the test util user will use when interacting with a component. This can be overridden\n * at the aria pattern util level if needed.\n * @default mouse\n */\n interactionType: UserOpts['interactionType'];\n /**\n * A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table).\n */\n advanceTimer: UserOpts['advanceTimer'];\n\n constructor(opts: UserOpts = {}) {\n let {interactionType, advanceTimer} = opts;\n this.user = userEvent.setup({delay: null, pointerMap});\n this.interactionType = interactionType;\n this.advanceTimer = advanceTimer || defaultAdvanceTimer;\n }\n\n /**\n * Creates an aria pattern tester, inheriting the options provided to the original user.\n */\n createTester<T extends PatternNames>(patternName: T, opts: TesterOpts<T>): Tester<T> {\n return new (keyToUtil)[patternName]({interactionType: this.interactionType, advanceTimer: this.advanceTimer, ...opts, user: this.user}) as Tester<T>;\n }\n}\n"],"names":[],"version":3,"file":"user.module.js.map"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":";;;;;;AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event
|
|
1
|
+
{"mappings":";;;;;;AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event';\n\nexport let pointerMap: pointerKey[] = [\n {name: 'MouseLeft', pointerType: 'mouse', button: 'primary', height: 1, width: 1, pressure: 0.5},\n {name: 'MouseRight', pointerType: 'mouse', button: 'secondary'},\n {name: 'MouseMiddle', pointerType: 'mouse', button: 'auxiliary'},\n {name: 'TouchA', pointerType: 'touch', height: 1, width: 1},\n {name: 'TouchB', pointerType: 'touch'},\n {name: 'TouchC', pointerType: 'touch'}\n] as unknown as pointerKey[];\n"],"names":[],"version":3,"file":"userEventMaps.main.js.map"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event
|
|
1
|
+
{"mappings":"AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event';\n\nexport let pointerMap: pointerKey[] = [\n {name: 'MouseLeft', pointerType: 'mouse', button: 'primary', height: 1, width: 1, pressure: 0.5},\n {name: 'MouseRight', pointerType: 'mouse', button: 'secondary'},\n {name: 'MouseMiddle', pointerType: 'mouse', button: 'auxiliary'},\n {name: 'TouchA', pointerType: 'touch', height: 1, width: 1},\n {name: 'TouchB', pointerType: 'touch'},\n {name: 'TouchC', pointerType: 'touch'}\n] as unknown as pointerKey[];\n"],"names":[],"version":3,"file":"userEventMaps.module.js.map"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-aria/test-utils",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-beta.0",
|
|
4
4
|
"description": "Testing utils for react-aria patterns",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
7
7
|
"module": "dist/module.js",
|
|
8
8
|
"exports": {
|
|
9
|
-
"
|
|
9
|
+
"source": "./src/index.ts",
|
|
10
|
+
"types": [
|
|
11
|
+
"./dist/types.d.ts",
|
|
12
|
+
"./src/index.ts"
|
|
13
|
+
],
|
|
10
14
|
"import": "./dist/import.mjs",
|
|
11
15
|
"require": "./dist/main.js"
|
|
12
16
|
},
|
|
@@ -33,5 +37,5 @@
|
|
|
33
37
|
"publishConfig": {
|
|
34
38
|
"access": "public"
|
|
35
39
|
},
|
|
36
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "8b9348ff255e018b2dd9b27e2a45507cadfa1d35"
|
|
37
41
|
}
|
package/src/combobox.ts
CHANGED
|
@@ -55,14 +55,17 @@ export class ComboBoxTester {
|
|
|
55
55
|
if (trigger) {
|
|
56
56
|
this._trigger = trigger;
|
|
57
57
|
} else {
|
|
58
|
-
let
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this._trigger = this._combobox;
|
|
58
|
+
let buttons = within(root).queryAllByRole('button', {hidden: true});
|
|
59
|
+
|
|
60
|
+
if (buttons.length === 1) {
|
|
61
|
+
trigger = buttons[0];
|
|
62
|
+
} else if (buttons.length > 1) {
|
|
63
|
+
trigger = buttons.find(button => button.hasAttribute('aria-haspopup'));
|
|
65
64
|
}
|
|
65
|
+
|
|
66
|
+
// For cases like https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ where the combobox
|
|
67
|
+
// is also the trigger button
|
|
68
|
+
this._trigger = trigger || this._combobox;
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
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
|
|