Skip to content

feat: Async loading support for S2 ComboBox/Picker #7938

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 86 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
c4ca76d
initial support for async loading in Combobox/picker/listbox in RAC
LFDanLu Mar 8, 2025
8ee20f9
test against Listbox standalone and put on content size change issue
LFDanLu Mar 10, 2025
c2a1cf9
update S2 CardView/RAC GridList for new useLoadMore
LFDanLu Mar 10, 2025
765b757
fix v3 load more stories and tests
LFDanLu Mar 10, 2025
7708b81
update table to call useLoadMore internally
LFDanLu Mar 11, 2025
2da7d04
first attempt at refactoring useLoadmore
LFDanLu Mar 12, 2025
0bb9f21
refactor useLoadMore to get rid of scroll handlers
LFDanLu Mar 14, 2025
0049153
Add S2 Picker async support, support horizontal scrolling, fix types …
LFDanLu Mar 14, 2025
76e1a05
async support for S2 combobox
LFDanLu Mar 14, 2025
68033b2
Merge branch 'main' of github.com:adobe/react-spectrum into loadmore_rac
LFDanLu Mar 14, 2025
54fcbaa
fix lint and add horizontal scrolling story
LFDanLu Mar 15, 2025
4d3bb6c
hack together async listbox virtualized example
LFDanLu Mar 17, 2025
f6ba68a
Merge branch 'main' of github.com:adobe/react-spectrum into loadmore_rac
LFDanLu Mar 18, 2025
3fe09f6
add loading spinners to RAC stories
LFDanLu Mar 18, 2025
0b06824
fix FF load more resizable wrapping container table, make s2 picker l…
LFDanLu Mar 18, 2025
da166f2
update S2 Picker/Combobox so they are described by loading spinner
LFDanLu Mar 18, 2025
0ef52ef
fix Talkback and NVDA announcements for loading spinner
LFDanLu Mar 19, 2025
51580d7
Merge branch 'main' of github.com:adobe/react-spectrum into loadmore_rac
LFDanLu Apr 9, 2025
76dc08a
clean up some todos
LFDanLu Apr 9, 2025
27260f4
set overflow to visible on ListLayout
yihuiliao Apr 11, 2025
41a28cb
add useLoadMoreSentinel instead of changing useLoadMore
LFDanLu Apr 14, 2025
4fb8aeb
refactor useLoadMore and update RAC components load more
LFDanLu Apr 15, 2025
f8b1745
add separator height to list layout
yihuiliao Apr 15, 2025
b272469
change css for picker and combobox
yihuiliao Apr 15, 2025
7a2877c
Merge branch 'main' into s2-combobox-picker-virtualizer
yihuiliao Apr 15, 2025
4c5bc69
fix s2 combobox and picker
LFDanLu Apr 16, 2025
5c423d4
fix separator height
yihuiliao Apr 17, 2025
fdf423f
fix picker's separator
yihuiliao Apr 17, 2025
711dff6
cleanup
yihuiliao Apr 17, 2025
286d8b8
Merge branch 'main' into s2-combobox-picker-virtualizer
yihuiliao Apr 17, 2025
0938fb9
update yarn lock
yihuiliao Apr 17, 2025
1eeb798
update S2 CardView and TableView for new loading sentinel refactor
LFDanLu Apr 17, 2025
5fc224f
fix lint
yihuiliao Apr 17, 2025
18c65b2
remove workflow dependency
yihuiliao Apr 17, 2025
85b7edb
remove style from s1 theme oops
yihuiliao Apr 17, 2025
7f57506
fix lint
yihuiliao Apr 17, 2025
d493fc4
picker fixes
yihuiliao Apr 17, 2025
a69a794
picker cleanup
yihuiliao Apr 17, 2025
4523888
fix lint
yihuiliao Apr 17, 2025
3052001
fix line height in header
yihuiliao Apr 17, 2025
6a98ac5
properly persist table loading spinner in virtualized case
LFDanLu Apr 17, 2025
6fb2c01
fix listbox and gridlist persisted sentinel and fix double spinners
LFDanLu Apr 18, 2025
97b21a2
stray console log
LFDanLu Apr 18, 2025
e1bd8f3
Merge branch 'main' of github.com:adobe/react-spectrum into loadmore_rac
LFDanLu Apr 18, 2025
d4bec98
fix react 19 tests
LFDanLu Apr 18, 2025
7f062b8
fix lint?
yihuiliao Apr 18, 2025
561d914
persist sentinel in card layouts
LFDanLu Apr 19, 2025
dc6e80a
forgot to fix waterfall empty state
LFDanLu Apr 19, 2025
0c3a3b4
Merge branch 'main' into s2-combobox-picker-virtualizer
yihuiliao Apr 21, 2025
2085f8c
get rid of extranous space when listbox/table loaded all of the avail…
LFDanLu Apr 22, 2025
a9b7ea6
fix empty state for S2 ComboBox and make sure S2 Picker doesnt open w…
LFDanLu Apr 22, 2025
f60cbe3
Merge branch 's2-combobox-picker-virtualizer' of github.com:adobe/rea…
LFDanLu Apr 23, 2025
0fa17cc
fix scroll offset issue after loadMore operations finish in virtualiz…
LFDanLu Apr 23, 2025
95beec7
dont reserve room for the isLoadingMore spinner if performing initial…
LFDanLu Apr 23, 2025
fc193c5
add translations and clean up
LFDanLu Apr 24, 2025
cbd91b5
get rid of flex: none since loader is part of virtualized collection
LFDanLu Apr 24, 2025
4a62993
fix lint
LFDanLu Apr 24, 2025
f664da0
Merge branch 'main' of github.com:adobe/react-spectrum into loadmore_rac
LFDanLu Apr 24, 2025
85440fc
update grid areas and fix edgeToText
yihuiliao Apr 24, 2025
09825f4
prevent the empty w/ loading sentinel select from opening on arrow down
LFDanLu Apr 25, 2025
d98691f
adding chromatic tests for S2 Combobox/Picker async loading
LFDanLu Apr 25, 2025
f34fc0c
making sure sentinel is rendered even when empty
LFDanLu Apr 25, 2025
abfcde0
update gridlist stories so that it is easier to see useLoadMore is on…
LFDanLu Apr 25, 2025
5258d8b
add gridlist tests for loadmore
LFDanLu Apr 28, 2025
48caf94
Merge branch 'main' into s2-combobox-picker-virtualizer
yihuiliao Apr 28, 2025
379fba7
fix install?
yihuiliao Apr 28, 2025
160e9a0
fix sizes
yihuiliao Apr 28, 2025
fac1920
fix lint
yihuiliao Apr 29, 2025
f87438b
update ScrollView to fix ComboBox tests
LFDanLu Apr 29, 2025
7c5f1e8
refactor to use collection instead of isLoading in useLoadMoreSentinel
LFDanLu Apr 29, 2025
63db21e
update getItemCount so it doesnt include loaders in custom announcements
LFDanLu Apr 30, 2025
1eb421e
make sure listbox doesnt add extranous padding above the empty state …
LFDanLu Apr 30, 2025
15bc7d1
add listbox and table tests
LFDanLu May 1, 2025
77b4b62
fix delay when opening many items S2 select
LFDanLu May 1, 2025
f7a1e20
fix picker tests
LFDanLu May 2, 2025
53f9158
fix collection index incrementing when performing insertBefore
LFDanLu May 2, 2025
4755b91
fix tests and lint
LFDanLu May 5, 2025
5e8b6ae
Merge branch 's2-combobox-picker-virtualizer' of github.com:adobe/rea…
LFDanLu May 5, 2025
e2b8c00
update test-util dev dep in S2 so 16/17 tests pass
LFDanLu May 5, 2025
a0b9e5d
forgot yarn lock change
LFDanLu May 5, 2025
be7d233
fix rowindex calculation when filtering async s2 combobox
LFDanLu May 5, 2025
69b7db1
make S2 picker button not throw warning when rending in fake DOM
LFDanLu May 6, 2025
a2049ab
clean up and add row index tests for GridList and Table
LFDanLu May 6, 2025
65c2aca
small improvements from testing session
LFDanLu May 6, 2025
32c3792
Merge branch 'main' of github.com:adobe/react-spectrum into loadmore_rac
LFDanLu May 7, 2025
1680885
review comments
LFDanLu May 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"@storybook/addon-themes": "^7.6.19",
"@storybook/api": "^7.6.19",
"@storybook/components": "^7.6.19",
"@storybook/jest": "^0.2.3",
"@storybook/manager-api": "^7.6.19",
"@storybook/preview": "^7.6.19",
"@storybook/preview-api": "^7.6.19",
Expand Down
23 changes: 16 additions & 7 deletions packages/@react-aria/collections/src/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class BaseNode<T> {
}

private invalidateChildIndices(child: ElementNode<T>): void {
if (this._minInvalidChildIndex == null || child.index < this._minInvalidChildIndex.index) {
if (this._minInvalidChildIndex == null || !this._minInvalidChildIndex.isConnected || child.index < this._minInvalidChildIndex.index) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found cases where the minInvalidChildIndex set on the Document was pointing to nodes that didn't exist in the collection any more so added this check so we'd always have an up to date one

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds maybe related to #8127?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah a bit, definitely solves a part of that issue

this._minInvalidChildIndex = child;
}
}
Expand Down Expand Up @@ -154,8 +154,11 @@ export class BaseNode<T> {

newNode.nextSibling = referenceNode;
newNode.previousSibling = referenceNode.previousSibling;
newNode.index = referenceNode.index;

// Ensure that the newNode's index is less than that of the reference node so that
// invalidateChildIndices will properly use the newNode as the _minInvalidChildIndex, thus making sure
// we will properly update the indexes of all sibiling nodes after the newNode. The value here doesn't matter
// since updateChildIndices should calculate the proper indexes.
newNode.index = referenceNode.index - 1;
if (this.firstChild === referenceNode) {
this.firstChild = newNode;
} else if (referenceNode.previousSibling) {
Expand All @@ -165,15 +168,15 @@ export class BaseNode<T> {
referenceNode.previousSibling = newNode;
newNode.parentNode = referenceNode.parentNode;

this.invalidateChildIndices(referenceNode);
this.invalidateChildIndices(newNode);
this.ownerDocument.queueUpdate();
}

removeChild(child: ElementNode<T>): void {
if (child.parentNode !== this || !this.ownerDocument.isMounted) {
return;
}

if (child.nextSibling) {
this.invalidateChildIndices(child.nextSibling);
child.nextSibling.previousSibling = child.previousSibling;
Expand Down Expand Up @@ -279,7 +282,7 @@ export class ElementNode<T> extends BaseNode<T> {
this.node = this.node.clone();
this.isMutated = true;
}

this.ownerDocument.markDirty(this);
return this.node;
}
Expand Down Expand Up @@ -470,6 +473,12 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
}

// Next, update dirty collection nodes.
// TODO: when updateCollection is called here, shouldn't we be calling this.updateChildIndicies as well? Theoretically it should only update
// nodes from _minInvalidChildIndex onwards so the increase in dirtyNodes should be minimal.
// Is element.updateNode supposed to handle that (it currently assumes the index stored on the node is correct already).
// At the moment, without this call to updateChildIndicies, filtering an async combobox doesn't actually update the index values of the
// updated collection...
this.updateChildIndices();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change along with the above were mainly to fix an issue I noticed where the collection indexes weren't updating when filtering the combobox. Above, we properly update the childIndices of the Document's dirty nodes (aka the children's children) but not for the main children. Not sure if the call to element.updateNode below is supposed to handle this but this felt the most correct

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the document be in the list of dirtyNodes then and therefore get processed by the above loop?

also is the todo still relevant?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The todo is part of the comment above, basically just noting that I'm not 100% sure about the change. From what I observed the document never marks itself as dirty when performing insertBefore operations, only when calling appendChild or if setFirstChild gets hit in insertBefore. Instead of this change, I could add this.ownerDocument.markDirty(this); to BaseNode's insertBefore and removeChild but it felt safer to isolate it here to updateCollection as a "end step" of sorts

for (let element of this.dirtyNodes) {
if (element instanceof ElementNode) {
if (element.isConnected && !element.isHidden) {
Expand Down Expand Up @@ -497,7 +506,7 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
if (this.dirtyNodes.size === 0 || this.queuedRender) {
return;
}

// Only trigger subscriptions once during an update, when the first item changes.
// React's useSyncExternalStore will call getCollection immediately, to check whether the snapshot changed.
// If so, React will queue a render to happen after the current commit to our fake DOM finishes.
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-aria/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ export {useEffectEvent} from './useEffectEvent';
export {useDeepMemo} from './useDeepMemo';
export {useFormReset} from './useFormReset';
export {useLoadMore} from './useLoadMore';
export {UNSTABLE_useLoadMoreSentinel} from './useLoadMoreSentinel';
export {inertValue} from './inertValue';
export {CLEAR_FOCUS_EVENT, FOCUS_EVENT} from './constants';
export {isCtrlKeyPressed} from './keyboard';
export {useEnterAnimation, useExitAnimation} from './animation';
export {isFocusable, isTabbable} from './isFocusable';

export type {LoadMoreSentinelProps} from './useLoadMoreSentinel';
64 changes: 64 additions & 0 deletions packages/@react-aria/utils/src/useLoadMoreSentinel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import type {AsyncLoadable, Collection, Node} from '@react-types/shared';
import {getScrollParent} from './getScrollParent';
import {RefObject, useRef} from 'react';
import {useEffectEvent} from './useEffectEvent';
import {useLayoutEffect} from './useLayoutEffect';

export interface LoadMoreSentinelProps extends Omit<AsyncLoadable, 'isLoading'> {
collection: Collection<Node<unknown>>,
/**
* The amount of offset from the bottom of your scrollable region that should trigger load more.
* Uses a percentage value relative to the scroll body's client height. Load more is then triggered
* when your current scroll position's distance from the bottom of the currently loaded list of items is less than
* or equal to the provided value. (e.g. 1 = 100% of the scroll region's height).
* @default 1
*/
scrollOffset?: number
// TODO: Maybe include a scrollRef option so the user can provide the scrollParent to compare against instead of having us look it up
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaning towards not adding it for now, the hook is unstable as of now so we could always add it later. Open to opinions if anyone feels strongly about the approach

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would that be different than the ref already provided? a ref to the scrollable region seems like a good idea so that eventually people can use something like body element scrolling to drive the virtualizer

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ref that is provided is the sentinel ref so not quite the same, but the general idea would be for the case you mentioned. Figured we could omit it until we actually try that use case

}

export function UNSTABLE_useLoadMoreSentinel(props: LoadMoreSentinelProps, ref: RefObject<HTMLElement | null>): void {
let {collection, onLoadMore, scrollOffset = 1} = props;

let sentinelObserver = useRef<IntersectionObserver>(null);

let triggerLoadMore = useEffectEvent((entries: IntersectionObserverEntry[]) => {
// Use "isIntersecting" over an equality check of 0 since it seems like there is cases where
// a intersection ratio of 0 can be reported when isIntersecting is actually true
for (let entry of entries) {
// Note that this will be called if the collection changes, even if onLoadMore was already called and is being processed.
// Up to user discretion as to how to handle these multiple onLoadMore calls
if (entry.isIntersecting && onLoadMore) {
onLoadMore();
}
}
});

useLayoutEffect(() => {
if (ref.current) {
// Tear down and set up a new IntersectionObserver when the collection changes so that we can properly trigger additional loadMores if there is room for more items
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fine, certainly more readable than separating it into some other effect and has the benefit of being queued in the same way as the rest of the IntersectionObserver updates

// Need to do this tear down and set up since using a large rootMargin will mean the observer's callback isn't called even when scrolling the item into view beause its visibility hasn't actually changed
// https://codesandbox.io/p/sandbox/magical-swanson-dhgp89?file=%2Fsrc%2FApp.js%3A21%2C21
sentinelObserver.current = new IntersectionObserver(triggerLoadMore, {root: getScrollParent(ref?.current) as HTMLElement, rootMargin: `0px ${100 * scrollOffset}% ${100 * scrollOffset}% ${100 * scrollOffset}%`});
sentinelObserver.current.observe(ref.current);
}

return () => {
if (sentinelObserver.current) {
sentinelObserver.current.disconnect();
}
};
}, [collection, triggerLoadMore, ref, scrollOffset]);
}
13 changes: 11 additions & 2 deletions packages/@react-aria/virtualizer/src/ScrollView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
// Prevent rubber band scrolling from shaking when scrolling out of bounds
state.scrollTop = Math.max(0, Math.min(scrollTop, contentSize.height - state.height));
state.scrollLeft = Math.max(0, Math.min(scrollLeft, contentSize.width - state.width));

onVisibleRectChange(new Rect(state.scrollLeft, state.scrollTop, state.width, state.height));

if (!state.isScrolling) {
Expand Down Expand Up @@ -199,6 +198,7 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement

// Update visible rect when the content size changes, in case scrollbars need to appear or disappear.
let lastContentSize = useRef<Size | null>(null);
let [update, setUpdate] = useState({});
useLayoutEffect(() => {
if (!isUpdatingSize.current && (lastContentSize.current == null || !contentSize.equals(lastContentSize.current))) {
// React doesn't allow flushSync inside effects, so queue a microtask.
Expand All @@ -209,7 +209,11 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
// https://github.com/reactwg/react-18/discussions/102
// @ts-ignore
if (typeof IS_REACT_ACT_ENVIRONMENT === 'boolean' ? IS_REACT_ACT_ENVIRONMENT : typeof jest !== 'undefined') {
updateSize(fn => fn());
// This is so we update size in a separate render but within the same act. Needs to be setState instead of refs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why? is there some event we could hook into instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to specifically fix the tests where the order in which things run (such as when the ref is connected to the ScrollView and when we perform rect measurements) vs how focus is moved by simulated clicks/presses from userEvent was causing odd behaviors (i.e. focus would move to the trigger button -> the dropdown -> back to the button specifically due to the order in which the dropdown becomes available/focusable + preventFocus's logic thinking it should then restore focus to the button vs how it happens in the browser with the dropdown becoming available after that preventFocus logic runs).

The only way to fix this was to make sure the size of the ScrollView updates in a separate render but within the same act, but ideally the high level flow should be that focus moves to the Picker trigger on click -> all press handling finishes -> collection is fully formed -> dropdown opens and focus moves into it.

// due to strict mode.
setUpdate({});
lastContentSize.current = contentSize;
return;
} else {
queueMicrotask(() => updateSize(flushSync));
}
Expand All @@ -218,6 +222,11 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
lastContentSize.current = contentSize;
});

// Will only run in tests, needs to be in separate effect so it is properly run in the next render in strict mode.
useLayoutEffect(() => {
updateSize(fn => fn());
}, [update]);

let onResize = useCallback(() => {
updateSize(flushSync);
}, [updateSize]);
Expand Down
95 changes: 92 additions & 3 deletions packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
* governing permissions and limitations under the License.
*/

import {AsyncComboBoxStory, ContextualHelpExample, CustomWidth, Dynamic, EmptyCombobox, Example, Sections, WithIcons} from '../stories/ComboBox.stories';
import {ComboBox} from '../src';
import {ContextualHelpExample, CustomWidth, Dynamic, Example, Sections, WithIcons} from '../stories/ComboBox.stories';
import {expect} from '@storybook/jest';
import type {Meta, StoryObj} from '@storybook/react';
import {userEvent, within} from '@storybook/testing-library';
import {userEvent, waitFor, within} from '@storybook/testing-library';

const meta: Meta<typeof ComboBox<any>> = {
component: ComboBox,
parameters: {
chromaticProvider: {colorSchemes: ['light'], backgrounds: ['base'], locales: ['en-US'], disableAnimations: true}
chromaticProvider: {colorSchemes: ['light'], backgrounds: ['base'], locales: ['en-US'], disableAnimations: true},
chromatic: {ignoreSelectors: ['[role="progressbar"]']}
},
tags: ['autodocs'],
title: 'S2 Chromatic/ComboBox'
Expand Down Expand Up @@ -69,3 +71,90 @@ export const WithCustomWidth = {
...CustomWidth,
play: async (context) => await Static.play!(context)
} as StoryObj;

export const WithEmptyState = {
...EmptyCombobox,
play: async ({canvasElement}) => {
await userEvent.tab();
await userEvent.keyboard('{ArrowDown}');
let body = canvasElement.ownerDocument.body;
let listbox = await within(body).findByRole('listbox');
await within(listbox).findByText('No results');
}
};

// TODO: this one is probably not great for chromatic since it has the spinner, check if ignoreSelectors works for it
export const WithInitialLoading = {
...EmptyCombobox,
args: {
loadingState: 'loading',
label: 'Initial loading'
},
play: async ({canvasElement}) => {
await userEvent.tab();
await userEvent.keyboard('{ArrowDown}');
let body = canvasElement.ownerDocument.body;
let listbox = await within(body).findByRole('listbox');
await within(listbox).findByText('Loading', {exact: false});
}
};

export const WithLoadMore = {
...Example,
args: {
loadingState: 'loadingMore',
label: 'Loading more'
},
play: async ({canvasElement}) => {
await userEvent.tab();
await userEvent.keyboard('{ArrowDown}');
let body = canvasElement.ownerDocument.body;
let listbox = await within(body).findByRole('listbox');
await within(listbox).findByRole('progressbar');
}
};

export const AsyncResults = {
...AsyncComboBoxStory,
args: {
...AsyncComboBoxStory.args,
delay: 2000
},
play: async ({canvasElement}) => {
await userEvent.tab();
await userEvent.keyboard('{ArrowDown}');
let body = canvasElement.ownerDocument.body;
let listbox = await within(body).findByRole('listbox');
await waitFor(() => {
expect(within(listbox).getByText('Luke', {exact: false})).toBeInTheDocument();
}, {timeout: 5000});
}
};

export const Filtering = {
...AsyncComboBoxStory,
args: {
...AsyncComboBoxStory.args,
delay: 2000
},
play: async ({canvasElement}) => {
await userEvent.tab();
await userEvent.keyboard('{ArrowDown}');
let body = canvasElement.ownerDocument.body;
let listbox = await within(body).findByRole('listbox');
await waitFor(() => {
expect(within(listbox).getByText('Luke', {exact: false})).toBeInTheDocument();
}, {timeout: 5000});

let combobox = await within(body).findByRole('combobox');
await userEvent.type(combobox, 'R2');

await waitFor(() => {
expect(within(body).getByRole('progressbar', {hidden: true})).toBeInTheDocument();
}, {timeout: 5000});

await waitFor(() => {
expect(within(listbox).queryByRole('progressbar', {hidden: true})).toBeFalsy();
}, {timeout: 5000});
}
};
63 changes: 60 additions & 3 deletions packages/@react-spectrum/s2/chromatic/Picker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
* governing permissions and limitations under the License.
*/

import {ContextualHelpExample, CustomWidth, Dynamic, Example, Sections, WithIcons} from '../stories/Picker.stories';
import {AsyncPickerStory, ContextualHelpExample, CustomWidth, Dynamic, Example, Sections, WithIcons} from '../stories/Picker.stories';
import {expect} from '@storybook/jest';
import type {Meta, StoryObj} from '@storybook/react';
import {Picker} from '../src';
import {userEvent, within} from '@storybook/testing-library';
import {userEvent, waitFor, within} from '@storybook/testing-library';

const meta: Meta<typeof Picker<any>> = {
component: Picker,
parameters: {
chromaticProvider: {colorSchemes: ['light'], backgrounds: ['base'], locales: ['en-US'], disableAnimations: true}
chromaticProvider: {colorSchemes: ['light'], backgrounds: ['base'], locales: ['en-US'], disableAnimations: true},
chromatic: {ignoreSelectors: ['[role="progressbar"]']}
},
tags: ['autodocs'],
title: 'S2 Chromatic/Picker'
Expand Down Expand Up @@ -68,3 +70,58 @@ export const ContextualHelp = {
}
};

export const EmptyAndLoading = {
render: () => (
<Picker label="loading" isLoading>
{[]}
</Picker>
),
play: async ({canvasElement}) => {
let body = canvasElement.ownerDocument.body;
await waitFor(() => {
expect(within(body).getByRole('progressbar', {hidden: true})).toBeInTheDocument();
}, {timeout: 5000});
await userEvent.tab();
await userEvent.keyboard('{ArrowDown}');
expect(within(body).queryByRole('listbox')).toBeFalsy();
}
};

export const AsyncResults = {
...AsyncPickerStory,
args: {
...AsyncPickerStory.args,
delay: 2000
},
play: async ({canvasElement}) => {
let body = canvasElement.ownerDocument.body;
await waitFor(() => {
expect(within(body).getByRole('progressbar', {hidden: true})).toBeInTheDocument();
}, {timeout: 5000});
await userEvent.tab();

await waitFor(() => {
expect(within(body).queryByRole('progressbar', {hidden: true})).toBeFalsy();
}, {timeout: 5000});

await userEvent.keyboard('{ArrowDown}');
let listbox = await within(body).findByRole('listbox');
await waitFor(() => {
expect(within(listbox).getByText('Luke', {exact: false})).toBeInTheDocument();
}, {timeout: 5000});

await waitFor(() => {
expect(within(listbox).getByRole('progressbar', {hidden: true})).toBeInTheDocument();
}, {timeout: 5000});

await waitFor(() => {
expect(within(listbox).queryByRole('progressbar', {hidden: true})).toBeFalsy();
}, {timeout: 5000});

await userEvent.keyboard('{PageDown}');

await waitFor(() => {
expect(within(listbox).getByText('Greedo', {exact: false})).toBeInTheDocument();
}, {timeout: 5000});
}
};
1 change: 1 addition & 0 deletions packages/@react-spectrum/s2/intl/ar-AE.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"actionbar.selectedAll": "تم تحديد الكل",
"breadcrumbs.more": "المزيد من العناصر",
"button.pending": "قيد الانتظار",
"combobox.noResults": "لا توجد نتائج",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From S1

"contextualhelp.help": "مساعدة",
"contextualhelp.info": "معلومات",
"dialog.alert": "تنبيه",
Expand Down
Loading