Skip to content

Commit bc5e9c9

Browse files
committed
Merge branch 'main' into upgrade-sb-8-2
# Conflicts: # package.json
2 parents 576f0c4 + 21d1950 commit bc5e9c9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3604
-228
lines changed

.circleci/comment.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ async function run() {
5353
[RAC Tailwind Example](https://reactspectrum.blob.core.windows.net/reactspectrum/${process.env.CIRCLE_SHA1}/verdaccio/rac-tailwind/index.html)
5454
[RAC Spectrum + Tailwind Example](https://reactspectrum.blob.core.windows.net/reactspectrum/${process.env.CIRCLE_SHA1}/verdaccio/rac-spectrum-tailwind/index.html)
5555
[S2 Parcel Example](https://reactspectrum.blob.core.windows.net/reactspectrum/${process.env.CIRCLE_SHA1}/verdaccio/s2-parcel-example/index.html)
56+
[S2 Custom Icons](https://reactspectrum.blob.core.windows.net/reactspectrum/${process.env.CIRCLE_SHA1}/verdaccio/icon-builder-fixture/index.html)
5657
[S2 Webpack Example](https://reactspectrum.blob.core.windows.net/reactspectrum/${process.env.CIRCLE_SHA1}/verdaccio/s2-webpack-5-example/index.html)
5758
[CRA Test App Size](https://reactspectrum.blob.core.windows.net/reactspectrum/${process.env.CIRCLE_SHA1}/verdaccio/publish-stats/build-stats.txt)
5859
[NextJS App Size](https://reactspectrum.blob.core.windows.net/reactspectrum/${process.env.CIRCLE_SHA1}/verdaccio/publish-stats/next-build-stats.txt)

.circleci/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,11 @@ jobs:
685685
environment:
686686
VERDACCIO_STORAGE_PATH: /tmp/verdaccio-workspace/storage
687687

688+
- persist_to_workspace:
689+
root: verdaccio_dist
690+
paths:
691+
- '*/verdaccio/icon-builder-fixture'
692+
688693
v-publish-stats:
689694
executor: rsp
690695
steps:

.parcelrc-build

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@parcel/transformer-react-refresh-wrap"
1717
]
1818
},
19-
"namers": ["parcel-namer-s2", "parcel-namer-intl", "..."],
19+
"namers": ["@react-spectrum/parcel-namer-s2", "parcel-namer-intl", "..."],
2020
"optimizers": {
2121
"**/spectrum-theme.cjs": ["parcel-optimizer-strict-mode"]
2222
}

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export default [{
5252
"packages/dev/optimize-locales-plugin/LocalesPlugin.d.ts",
5353
"examples/**/*",
5454
"starters/**/*",
55+
"scripts/icon-builder-fixture/**/*",
5556
"packages/@react-spectrum/s2/icon.d.ts",
5657
"packages/@react-spectrum/s2/spectrum-illustrations",
5758
"packages/dev/parcel-config-storybook/*",

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"@parcel/transformer-inline-string": "^2.14.0",
9999
"@parcel/transformer-svg-react": "^2.14.0",
100100
"@parcel/transformer-typescript-types": "^2.14.0",
101+
"@react-spectrum/parcel-namer-s2": "^1.0.0",
101102
"@react-spectrum/s2-icon-builder": "^0.2.0",
102103
"@spectrum-css/component-builder": "workspace:^",
103104
"@spectrum-css/vars": "^2.3.0",
@@ -220,6 +221,9 @@
220221
"@babel/traverse": "7.24.1",
221222
"@babel/types": "7.24.0",
222223
"@mdx-js/react": "2.0.0-rc.2",
224+
"@parcel/transformer-react-refresh-wrap": "2.14.0",
225+
"@parcel/transformer-js": "2.14.0",
226+
"@parcel/codeframe": "2.14.0",
223227
"postcss": "8.4.24",
224228
"postcss-custom-properties": "13.2.0",
225229
"postcss-import": "15.1.0",
@@ -231,8 +235,6 @@
231235
"recast": "0.23.6",
232236
"ast-types": "0.16.1",
233237
"svgo": "^3",
234-
"react": "18.3.1",
235-
"react-dom": "18.3.1",
236238
"@testing-library/user-event": "patch:@testing-library/user-event@npm%3A14.6.1#~/.yarn/patches/@testing-library-user-event-npm-14.6.1-5da7e1d4e2.patch",
237239
"@types/node@npm:*": "^22",
238240
"@types/node@npm:^18.0.0": "^22",

packages/@react-aria/selection/src/ListKeyboardDelegate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
7878
let nextKey = key;
7979
while (nextKey != null) {
8080
let item = this.collection.getItem(nextKey);
81-
if (item?.type === 'loader' || (item?.type === 'item' && !this.isDisabled(item))) {
81+
if (item?.type === 'item' && !this.isDisabled(item)) {
8282
return nextKey;
8383
}
8484

packages/@react-aria/test-utils/src/combobox.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,17 @@ export class ComboBoxTester {
5555
if (trigger) {
5656
this._trigger = trigger;
5757
} else {
58-
let trigger = within(root).queryByRole('button', {hidden: true});
59-
if (trigger) {
60-
this._trigger = trigger;
61-
} else {
62-
// For cases like https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ where the combobox
63-
// is also the trigger button
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'));
6564
}
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;
6669
}
6770
}
6871

packages/@react-aria/test-utils/src/events.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,37 @@ import {act, fireEvent} from '@testing-library/react';
1414
import {UserOpts} from './types';
1515

1616
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+
}
1748

1849
/**
1950
* Simulates a "long press" event on a element.
@@ -58,9 +89,10 @@ export async function triggerLongPress(opts: {element: HTMLElement, advanceTimer
5889
}
5990

6091
// 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> {
6293
if (interactionType === 'mouse') {
63-
await user.click(element);
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}});
6496
} else if (interactionType === 'keyboard') {
6597
// 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
6698
// stick to simulting an actual user's keyboard operations as closely as possible

packages/@react-aria/test-utils/src/gridlist.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
*/
1212

1313
import {act, within} from '@testing-library/react';
14+
import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
1415
import {GridListTesterOpts, GridRowActionOpts, ToggleGridRowOpts, UserOpts} from './types';
15-
import {pressElement, triggerLongPress} from './events';
1616

1717
interface GridListToggleRowOpts extends ToggleGridRowOpts {}
1818
interface GridListRowActionOpts extends GridRowActionOpts {}
@@ -57,20 +57,21 @@ export class GridListTester {
5757
}
5858

5959
// 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();
6263
let rows = this.rows;
6364
let targetIndex = rows.indexOf(row);
6465
if (targetIndex === -1) {
6566
throw new Error('Option provided is not in the gridlist');
6667
}
6768

68-
if (document.activeElement !== this._gridlist || !this._gridlist.contains(document.activeElement)) {
69+
if (document.activeElement !== this._gridlist && !this._gridlist.contains(document.activeElement)) {
6970
act(() => this._gridlist.focus());
7071
}
7172

7273
if (document.activeElement === this._gridlist) {
73-
await this.user.keyboard('[ArrowDown]');
74+
await this.user.keyboard(`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`);
7475
} else if (this._gridlist.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
7576
do {
7677
await this.user.keyboard('[ArrowLeft]');
@@ -82,22 +83,33 @@ export class GridListTester {
8283
}
8384
let direction = targetIndex > currIndex ? 'down' : 'up';
8485

86+
if (selectionOnNav === 'none') {
87+
await this.user.keyboard(`[${altKey}>]`);
88+
}
8589
for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
8690
await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
8791
}
92+
if (selectionOnNav === 'none') {
93+
await this.user.keyboard(`[/${altKey}]`);
94+
}
8895
};
8996

9097
/**
9198
* 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.
92100
*/
93101
async toggleRowSelection(opts: GridListToggleRowOpts): Promise<void> {
94102
let {
95103
row,
96104
needsLongPress,
97105
checkboxSelection = true,
98-
interactionType = this._interactionType
106+
interactionType = this._interactionType,
107+
selectionBehavior = 'toggle'
99108
} = opts;
100109

110+
let altKey = getAltKey();
111+
let metaKey = getMetaKey();
112+
101113
if (typeof row === 'string' || typeof row === 'number') {
102114
row = this.findRow({rowIndexOrText: row});
103115
}
@@ -116,9 +128,15 @@ export class GridListTester {
116128

117129
// this would be better than the check to do nothing in events.ts
118130
// 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-
await this.user.keyboard('{Space}');
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+
}
122140
return;
123141
}
124142
if (rowCheckbox && checkboxSelection) {
@@ -132,9 +150,14 @@ export class GridListTester {
132150

133151
// Note that long press interactions with rows is strictly touch only for grid rows
134152
await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
135-
136153
} else {
137-
await pressElement(this.user, cell, interactionType);
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+
}
138161
}
139162
}
140163
}
@@ -166,7 +189,7 @@ export class GridListTester {
166189
return;
167190
}
168191

169-
await this.keyboardNavigateToRow({row});
192+
await this.keyboardNavigateToRow({row, selectionOnNav: 'none'});
170193
await this.user.keyboard('[Enter]');
171194
} else {
172195
await pressElement(this.user, row, interactionType);

packages/@react-aria/test-utils/src/listbox.ts

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
*/
1212

1313
import {act, within} from '@testing-library/react';
14+
import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
1415
import {ListBoxTesterOpts, UserOpts} from './types';
15-
import {pressElement, triggerLongPress} from './events';
1616

1717
interface ListBoxToggleOptionOpts {
1818
/**
@@ -31,7 +31,16 @@ interface ListBoxToggleOptionOpts {
3131
/**
3232
* Whether the option needs to be long pressed to be selected. Depends on the listbox's implementation.
3333
*/
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'
3544
}
3645

3746
interface ListBoxOptionActionOpts extends Omit<ListBoxToggleOptionOpts, 'keyboardActivation' | 'needsLongPress'> {
@@ -85,44 +94,51 @@ export class ListBoxTester {
8594

8695
// TODO: this is basically the same as menu except for the error message, refactor later so that they share
8796
// 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();
90100
let options = this.options();
91101
let targetIndex = options.indexOf(option);
92102
if (targetIndex === -1) {
93103
throw new Error('Option provided is not in the listbox');
94104
}
95105

96-
if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) {
106+
if (document.activeElement !== this._listbox && !this._listbox.contains(document.activeElement)) {
97107
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}]` : ''}`);
108109
}
109110

110111
let currIndex = options.indexOf(document.activeElement as HTMLElement);
111112
if (currIndex === -1) {
112113
throw new Error('ActiveElement is not in the listbox');
113114
}
114-
let direction = targetIndex > currIndex ? 'down' : 'up';
115115

116+
let direction = targetIndex > currIndex ? 'down' : 'up';
117+
if (selectionOnNav === 'none') {
118+
await this.user.keyboard(`[${altKey}>]`);
119+
}
116120
for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
117121
await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
118122
}
123+
if (selectionOnNav === 'none') {
124+
await this.user.keyboard(`[/${altKey}]`);
125+
}
119126
};
120127

121128
/**
122129
* Toggles the selection for the specified listbox option. Defaults to using the interaction type set on the listbox tester.
123130
*/
124131
async toggleOptionSelection(opts: ListBoxToggleOptionOpts): Promise<void> {
125-
let {option, needsLongPress, keyboardActivation = 'Enter', interactionType = this._interactionType} = opts;
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();
126142

127143
if (typeof option === 'string' || typeof option === 'number') {
128144
option = this.findOption({optionIndexOrText: option});
@@ -137,8 +153,14 @@ export class ListBoxTester {
137153
return;
138154
}
139155

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+
}
141160
await this.user.keyboard(`[${keyboardActivation}]`);
161+
if (selectionBehavior === 'replace') {
162+
await this.user.keyboard(`[/${altKey}]`);
163+
}
142164
} else {
143165
if (needsLongPress && interactionType === 'touch') {
144166
if (this._advanceTimer == null) {
@@ -147,7 +169,13 @@ export class ListBoxTester {
147169

148170
await triggerLongPress({element: option, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
149171
} else {
172+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
173+
await this.user.keyboard(`[${metaKey}>]`);
174+
}
150175
await pressElement(this.user, option, interactionType);
176+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
177+
await this.user.keyboard(`[/${metaKey}]`);
178+
}
151179
}
152180
}
153181
}
@@ -177,7 +205,7 @@ export class ListBoxTester {
177205
return;
178206
}
179207

180-
await this.keyboardNavigateToOption({option});
208+
await this.keyboardNavigateToOption({option, selectionOnNav: 'none'});
181209
await this.user.keyboard('[Enter]');
182210
} else {
183211
await pressElement(this.user, option, interactionType);

0 commit comments

Comments
 (0)