Skip to content

Commit e9bd3a3

Browse files
LFDanLuyihuiliao
andauthored
feat: Async loading support for S2 ComboBox/Picker (#7938)
* initial support for async loading in Combobox/picker/listbox in RAC * test against Listbox standalone and put on content size change issue * update S2 CardView/RAC GridList for new useLoadMore * fix v3 load more stories and tests * update table to call useLoadMore internally * first attempt at refactoring useLoadmore * refactor useLoadMore to get rid of scroll handlers * Add S2 Picker async support, support horizontal scrolling, fix types and data-attributes * async support for S2 combobox * fix lint and add horizontal scrolling story * hack together async listbox virtualized example * add loading spinners to RAC stories * fix FF load more resizable wrapping container table, make s2 picker load more like v3 * update S2 Picker/Combobox so they are described by loading spinner * fix Talkback and NVDA announcements for loading spinner * clean up some todos * set overflow to visible on ListLayout * add useLoadMoreSentinel instead of changing useLoadMore revert v3 components to still use old useLoadMore * refactor useLoadMore and update RAC components load more render a sentinel in the loading indicator always and call useLoadMoreSentinel from there instead. This allows the user to theoretically render as many sentinels as they want with each having its own loading state/logic * add separator height to list layout * change css for picker and combobox * fix s2 combobox and picker * fix separator height * fix picker's separator * cleanup * update yarn lock * update S2 CardView and TableView for new loading sentinel refactor * fix lint * remove workflow dependency * remove style from s1 theme oops * fix lint * picker fixes * picker cleanup * fix lint * fix line height in header * properly persist table loading spinner in virtualized case * fix listbox and gridlist persisted sentinel and fix double spinners * stray console log * fix react 19 tests * fix lint? * persist sentinel in card layouts * forgot to fix waterfall empty state * get rid of extranous space when listbox/table loaded all of the available items * fix empty state for S2 ComboBox and make sure S2 Picker doesnt open when empty * fix scroll offset issue after loadMore operations finish in virtualized components works for the most part but is problematic if you dont want the loadingRow to appear with performing initial load (aka loadingState = loading). Due to how useLoadMoreSentinel work, we need to reobserve the list when we go from loading to loadingMore, but is isLoading is true the layout willpreserve room for the loading row... * dont reserve room for the isLoadingMore spinner if performing initial load also fixes RAC examples by properly applying a height to the tablebody if performing initial load * add translations and clean up * get rid of flex: none since loader is part of virtualized collection was only needed when rendering the loader after the virutalizer div * fix lint * update grid areas and fix edgeToText * prevent the empty w/ loading sentinel select from opening on arrow down * adding chromatic tests for S2 Combobox/Picker async loading * making sure sentinel is rendered even when empty for cases like Listbox in Combobox, the content area is actually 0 since we dont have a height on the listbox nor does it reserve room like table layout * update gridlist stories so that it is easier to see useLoadMore is only called once per load due to the number of items returned by the star wars api and the height of the rows, making the gridlist too tall causes the loadmore to be called multiple times in order to fill enough content for a single page * add gridlist tests for loadmore * fix install? * fix sizes * fix lint * update ScrollView to fix ComboBox tests the issue was that updateSize was being called before the listboxref was attached due to it being immediatelly called in the test. The change makes it so the updateSize is called in the next render instead * refactor to use collection instead of isLoading in useLoadMoreSentinel this allows us to only have one prop in the sentinel to control visiblility of the loader while still calling loadMore when the collection changes. Also gives the added benefit of causing load more to happen if items are deleted from the collection * update getItemCount so it doesnt include loaders in custom announcements * make sure listbox doesnt add extranous padding above the empty state when empty or loading since we are now rendering the virtualizer body if there is a loading sentinel, we dont want to add padding to the body rect calc since that will push the renderEmpty node down. Not a problem in TableLayout it seems * add listbox and table tests * fix delay when opening many items S2 select * sorta get selected item to scroll into view virtualized * fix tests * cleanup fix lint * more cleanup * fix picker tests * fix collection index incrementing when performing insertBefore previously was only checking elements in the collection after the loading spinner node, thus items loaded async into the collection all remained with incorrect index, resulting in incorrect aria-rowIndex * fix tests and lint * update test-util dev dep in S2 so 16/17 tests pass * forgot yarn lock change * fix overflow on windows potentially... * fix rowindex calculation when filtering async s2 combobox the document didnt seem to be both updating its _minInvalidChildIndex nor updating its child indicies properly when the combobox list was filtered async (aka the collection got new items added and removed via insertBefore and/or removeChild. Additionally, we didnt see to ever call updateChildIndices on the Document other than the first time the collection loaded * make S2 picker button not throw warning when rending in fake DOM * fix s2 picker scroll selected item into view * fix lint * clean up and add row index tests for GridList and Table * small improvements from testing session * update yarn lock * remove comment * review comments * missed yarn.lock conflict * fix picker test * mixed up which react-dom dep to remove derp * fix Combobox test so it properly catches previous Document bug tested with the Document changes removed and verified that the test fails properly. Swore it failed when I first wrote it... * fix cases where extra separators appeared on combobox filter/if load sentinel is present * marking the document as dirty when _minInvalidChildIndex is changing * get rid of extra todos and obsolte test --------- Co-authored-by: Yihui Liao <[email protected]>
1 parent acecfe2 commit e9bd3a3

Some content is hidden

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

78 files changed

+3057
-547
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"@storybook/addon-themes": "^7.6.19",
114114
"@storybook/api": "^7.6.19",
115115
"@storybook/components": "^7.6.19",
116+
"@storybook/jest": "^0.2.3",
116117
"@storybook/manager-api": "^7.6.19",
117118
"@storybook/preview": "^7.6.19",
118119
"@storybook/preview-api": "^7.6.19",

packages/@react-aria/collections/src/CollectionBuilder.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,9 @@ function useSSRCollectionNode<T extends Element>(Type: string, props: object, re
157157
return <Type ref={itemRef}>{children}</Type>;
158158
}
159159

160-
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>) => ReactElement): (props: P & React.RefAttributes<T>) => ReactElement | null;
161-
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement): (props: P & React.RefAttributes<T>) => ReactElement | null;
162-
export function createLeafComponent<P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node?: any) => ReactElement): (props: P & React.RefAttributes<any>) => ReactElement | null {
160+
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
161+
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
162+
export function createLeafComponent<P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node?: any) => ReactElement | null): (props: P & React.RefAttributes<any>) => ReactElement | null {
163163
let Component = ({node}) => render(node.props, node.props.ref, node);
164164
let Result = (forwardRef as forwardRefType)((props: P, ref: ForwardedRef<E>) => {
165165
let focusableProps = useContext(FocusableContext);
@@ -190,7 +190,7 @@ export function createLeafComponent<P extends object, E extends Element>(type: s
190190
return Result;
191191
}
192192

193-
export function createBranchComponent<T extends object, P extends {children?: any}, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement, useChildren: (props: P) => ReactNode = useCollectionChildren): (props: P & React.RefAttributes<E>) => ReactElement | null {
193+
export function createBranchComponent<T extends object, P extends {children?: any}, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null, useChildren: (props: P) => ReactNode = useCollectionChildren): (props: P & React.RefAttributes<E>) => ReactElement | null {
194194
let Component = ({node}) => render(node.props, node.props.ref, node);
195195
let Result = (forwardRef as forwardRefType)((props: P, ref: ForwardedRef<E>) => {
196196
let children = useChildren(props);

packages/@react-aria/collections/src/Document.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ export class BaseNode<T> {
103103
}
104104

105105
private invalidateChildIndices(child: ElementNode<T>): void {
106-
if (this._minInvalidChildIndex == null || child.index < this._minInvalidChildIndex.index) {
106+
if (this._minInvalidChildIndex == null || !this._minInvalidChildIndex.isConnected || child.index < this._minInvalidChildIndex.index) {
107107
this._minInvalidChildIndex = child;
108+
this.ownerDocument.markDirty(this);
108109
}
109110
}
110111

@@ -156,8 +157,11 @@ export class BaseNode<T> {
156157

157158
newNode.nextSibling = referenceNode;
158159
newNode.previousSibling = referenceNode.previousSibling;
159-
newNode.index = referenceNode.index;
160-
160+
// Ensure that the newNode's index is less than that of the reference node so that
161+
// invalidateChildIndices will properly use the newNode as the _minInvalidChildIndex, thus making sure
162+
// we will properly update the indexes of all sibiling nodes after the newNode. The value here doesn't matter
163+
// since updateChildIndices should calculate the proper indexes.
164+
newNode.index = referenceNode.index - 1;
161165
if (this.firstChild === referenceNode) {
162166
this.firstChild = newNode;
163167
} else if (referenceNode.previousSibling) {
@@ -167,7 +171,7 @@ export class BaseNode<T> {
167171
referenceNode.previousSibling = newNode;
168172
newNode.parentNode = referenceNode.parentNode;
169173

170-
this.invalidateChildIndices(referenceNode);
174+
this.invalidateChildIndices(newNode);
171175
if (this.isConnected) {
172176
this.ownerDocument.queueUpdate();
173177
}
@@ -177,7 +181,7 @@ export class BaseNode<T> {
177181
if (child.parentNode !== this || !this.ownerDocument.isMounted) {
178182
return;
179183
}
180-
184+
181185
if (child.nextSibling) {
182186
this.invalidateChildIndices(child.nextSibling);
183187
child.nextSibling.previousSibling = child.previousSibling;
@@ -285,7 +289,7 @@ export class ElementNode<T> extends BaseNode<T> {
285289
this.node = this.node.clone();
286290
this.isMutated = true;
287291
}
288-
292+
289293
this.ownerDocument.markDirty(this);
290294
return this.node;
291295
}
@@ -505,7 +509,7 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
505509
if (this.dirtyNodes.size === 0 || this.queuedRender) {
506510
return;
507511
}
508-
512+
509513
// Only trigger subscriptions once during an update, when the first item changes.
510514
// React's useSyncExternalStore will call getCollection immediately, to check whether the snapshot changed.
511515
// If so, React will queue a render to happen after the current commit to our fake DOM finishes.

packages/@react-aria/utils/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ export {useEffectEvent} from './useEffectEvent';
4545
export {useDeepMemo} from './useDeepMemo';
4646
export {useFormReset} from './useFormReset';
4747
export {useLoadMore} from './useLoadMore';
48+
export {UNSTABLE_useLoadMoreSentinel} from './useLoadMoreSentinel';
4849
export {inertValue} from './inertValue';
4950
export {CLEAR_FOCUS_EVENT, FOCUS_EVENT} from './constants';
5051
export {isCtrlKeyPressed} from './keyboard';
5152
export {useEnterAnimation, useExitAnimation} from './animation';
5253
export {isFocusable, isTabbable} from './isFocusable';
54+
55+
export type {LoadMoreSentinelProps} from './useLoadMoreSentinel';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2024 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import type {AsyncLoadable, Collection, Node} from '@react-types/shared';
14+
import {getScrollParent} from './getScrollParent';
15+
import {RefObject, useRef} from 'react';
16+
import {useEffectEvent} from './useEffectEvent';
17+
import {useLayoutEffect} from './useLayoutEffect';
18+
19+
export interface LoadMoreSentinelProps extends Omit<AsyncLoadable, 'isLoading'> {
20+
collection: Collection<Node<unknown>>,
21+
/**
22+
* The amount of offset from the bottom of your scrollable region that should trigger load more.
23+
* Uses a percentage value relative to the scroll body's client height. Load more is then triggered
24+
* when your current scroll position's distance from the bottom of the currently loaded list of items is less than
25+
* or equal to the provided value. (e.g. 1 = 100% of the scroll region's height).
26+
* @default 1
27+
*/
28+
scrollOffset?: number
29+
}
30+
31+
export function UNSTABLE_useLoadMoreSentinel(props: LoadMoreSentinelProps, ref: RefObject<HTMLElement | null>): void {
32+
let {collection, onLoadMore, scrollOffset = 1} = props;
33+
34+
let sentinelObserver = useRef<IntersectionObserver>(null);
35+
36+
let triggerLoadMore = useEffectEvent((entries: IntersectionObserverEntry[]) => {
37+
// Use "isIntersecting" over an equality check of 0 since it seems like there is cases where
38+
// a intersection ratio of 0 can be reported when isIntersecting is actually true
39+
for (let entry of entries) {
40+
// Note that this will be called if the collection changes, even if onLoadMore was already called and is being processed.
41+
// Up to user discretion as to how to handle these multiple onLoadMore calls
42+
if (entry.isIntersecting && onLoadMore) {
43+
onLoadMore();
44+
}
45+
}
46+
});
47+
48+
useLayoutEffect(() => {
49+
if (ref.current) {
50+
// 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
51+
// 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
52+
// https://codesandbox.io/p/sandbox/magical-swanson-dhgp89?file=%2Fsrc%2FApp.js%3A21%2C21
53+
sentinelObserver.current = new IntersectionObserver(triggerLoadMore, {root: getScrollParent(ref?.current) as HTMLElement, rootMargin: `0px ${100 * scrollOffset}% ${100 * scrollOffset}% ${100 * scrollOffset}%`});
54+
sentinelObserver.current.observe(ref.current);
55+
}
56+
57+
return () => {
58+
if (sentinelObserver.current) {
59+
sentinelObserver.current.disconnect();
60+
}
61+
};
62+
}, [collection, triggerLoadMore, ref, scrollOffset]);
63+
}

packages/@react-aria/virtualizer/src/ScrollView.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
9898
// Prevent rubber band scrolling from shaking when scrolling out of bounds
9999
state.scrollTop = Math.max(0, Math.min(scrollTop, contentSize.height - state.height));
100100
state.scrollLeft = Math.max(0, Math.min(scrollLeft, contentSize.width - state.width));
101-
102101
onVisibleRectChange(new Rect(state.scrollLeft, state.scrollTop, state.width, state.height));
103102

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

200199
// Update visible rect when the content size changes, in case scrollbars need to appear or disappear.
201200
let lastContentSize = useRef<Size | null>(null);
201+
let [update, setUpdate] = useState({});
202202
useLayoutEffect(() => {
203203
if (!isUpdatingSize.current && (lastContentSize.current == null || !contentSize.equals(lastContentSize.current))) {
204204
// React doesn't allow flushSync inside effects, so queue a microtask.
@@ -209,7 +209,11 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
209209
// https://github.com/reactwg/react-18/discussions/102
210210
// @ts-ignore
211211
if (typeof IS_REACT_ACT_ENVIRONMENT === 'boolean' ? IS_REACT_ACT_ENVIRONMENT : typeof jest !== 'undefined') {
212-
updateSize(fn => fn());
212+
// This is so we update size in a separate render but within the same act. Needs to be setState instead of refs
213+
// due to strict mode.
214+
setUpdate({});
215+
lastContentSize.current = contentSize;
216+
return;
213217
} else {
214218
queueMicrotask(() => updateSize(flushSync));
215219
}
@@ -218,6 +222,11 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
218222
lastContentSize.current = contentSize;
219223
});
220224

225+
// Will only run in tests, needs to be in separate effect so it is properly run in the next render in strict mode.
226+
useLayoutEffect(() => {
227+
updateSize(fn => fn());
228+
}, [update]);
229+
221230
let onResize = useCallback(() => {
222231
updateSize(flushSync);
223232
}, [updateSize]);

packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx

+91-3
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13+
import {AsyncComboBoxStory, ContextualHelpExample, CustomWidth, Dynamic, EmptyCombobox, Example, Sections, WithIcons} from '../stories/ComboBox.stories';
1314
import {ComboBox} from '../src';
14-
import {ContextualHelpExample, CustomWidth, Dynamic, Example, Sections, WithIcons} from '../stories/ComboBox.stories';
15+
import {expect} from '@storybook/jest';
1516
import type {Meta, StoryObj} from '@storybook/react';
16-
import {userEvent, within} from '@storybook/testing-library';
17+
import {userEvent, waitFor, within} from '@storybook/testing-library';
1718

1819
const meta: Meta<typeof ComboBox<any>> = {
1920
component: ComboBox,
2021
parameters: {
21-
chromaticProvider: {colorSchemes: ['light'], backgrounds: ['base'], locales: ['en-US'], disableAnimations: true}
22+
chromaticProvider: {colorSchemes: ['light'], backgrounds: ['base'], locales: ['en-US'], disableAnimations: true},
23+
chromatic: {ignoreSelectors: ['[role="progressbar"]']}
2224
},
2325
tags: ['autodocs'],
2426
title: 'S2 Chromatic/ComboBox'
@@ -69,3 +71,89 @@ export const WithCustomWidth = {
6971
...CustomWidth,
7072
play: async (context) => await Static.play!(context)
7173
} as StoryObj;
74+
75+
export const WithEmptyState = {
76+
...EmptyCombobox,
77+
play: async ({canvasElement}) => {
78+
await userEvent.tab();
79+
await userEvent.keyboard('{ArrowDown}');
80+
let body = canvasElement.ownerDocument.body;
81+
let listbox = await within(body).findByRole('listbox');
82+
await within(listbox).findByText('No results');
83+
}
84+
};
85+
86+
export const WithInitialLoading = {
87+
...EmptyCombobox,
88+
args: {
89+
loadingState: 'loading',
90+
label: 'Initial loading'
91+
},
92+
play: async ({canvasElement}) => {
93+
await userEvent.tab();
94+
await userEvent.keyboard('{ArrowDown}');
95+
let body = canvasElement.ownerDocument.body;
96+
let listbox = await within(body).findByRole('listbox');
97+
await within(listbox).findByText('Loading', {exact: false});
98+
}
99+
};
100+
101+
export const WithLoadMore = {
102+
...Example,
103+
args: {
104+
loadingState: 'loadingMore',
105+
label: 'Loading more'
106+
},
107+
play: async ({canvasElement}) => {
108+
await userEvent.tab();
109+
await userEvent.keyboard('{ArrowDown}');
110+
let body = canvasElement.ownerDocument.body;
111+
let listbox = await within(body).findByRole('listbox');
112+
await within(listbox).findByRole('progressbar');
113+
}
114+
};
115+
116+
export const AsyncResults = {
117+
...AsyncComboBoxStory,
118+
args: {
119+
...AsyncComboBoxStory.args,
120+
delay: 2000
121+
},
122+
play: async ({canvasElement}) => {
123+
await userEvent.tab();
124+
await userEvent.keyboard('{ArrowDown}');
125+
let body = canvasElement.ownerDocument.body;
126+
let listbox = await within(body).findByRole('listbox');
127+
await waitFor(() => {
128+
expect(within(listbox).getByText('Luke', {exact: false})).toBeInTheDocument();
129+
}, {timeout: 5000});
130+
}
131+
};
132+
133+
export const Filtering = {
134+
...AsyncComboBoxStory,
135+
args: {
136+
...AsyncComboBoxStory.args,
137+
delay: 2000
138+
},
139+
play: async ({canvasElement}) => {
140+
await userEvent.tab();
141+
await userEvent.keyboard('{ArrowDown}');
142+
let body = canvasElement.ownerDocument.body;
143+
let listbox = await within(body).findByRole('listbox');
144+
await waitFor(() => {
145+
expect(within(listbox).getByText('Luke', {exact: false})).toBeInTheDocument();
146+
}, {timeout: 5000});
147+
148+
let combobox = await within(body).findByRole('combobox');
149+
await userEvent.type(combobox, 'R2');
150+
151+
await waitFor(() => {
152+
expect(within(body).getByRole('progressbar', {hidden: true})).toBeInTheDocument();
153+
}, {timeout: 5000});
154+
155+
await waitFor(() => {
156+
expect(within(listbox).queryByRole('progressbar', {hidden: true})).toBeFalsy();
157+
}, {timeout: 5000});
158+
}
159+
};

packages/@react-spectrum/s2/chromatic/Picker.stories.tsx

+60-3
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

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

1819
const meta: Meta<typeof Picker<any>> = {
1920
component: Picker,
2021
parameters: {
21-
chromaticProvider: {colorSchemes: ['light'], backgrounds: ['base'], locales: ['en-US'], disableAnimations: true}
22+
chromaticProvider: {colorSchemes: ['light'], backgrounds: ['base'], locales: ['en-US'], disableAnimations: true},
23+
chromatic: {ignoreSelectors: ['[role="progressbar"]']}
2224
},
2325
tags: ['autodocs'],
2426
title: 'S2 Chromatic/Picker'
@@ -68,3 +70,58 @@ export const ContextualHelp = {
6870
}
6971
};
7072

73+
export const EmptyAndLoading = {
74+
render: () => (
75+
<Picker label="loading" isLoading>
76+
{[]}
77+
</Picker>
78+
),
79+
play: async ({canvasElement}) => {
80+
let body = canvasElement.ownerDocument.body;
81+
await waitFor(() => {
82+
expect(within(body).getByRole('progressbar', {hidden: true})).toBeInTheDocument();
83+
}, {timeout: 5000});
84+
await userEvent.tab();
85+
await userEvent.keyboard('{ArrowDown}');
86+
expect(within(body).queryByRole('listbox')).toBeFalsy();
87+
}
88+
};
89+
90+
export const AsyncResults = {
91+
...AsyncPickerStory,
92+
args: {
93+
...AsyncPickerStory.args,
94+
delay: 2000
95+
},
96+
play: async ({canvasElement}) => {
97+
let body = canvasElement.ownerDocument.body;
98+
await waitFor(() => {
99+
expect(within(body).getByRole('progressbar', {hidden: true})).toBeInTheDocument();
100+
}, {timeout: 5000});
101+
await userEvent.tab();
102+
103+
await waitFor(() => {
104+
expect(within(body).queryByRole('progressbar', {hidden: true})).toBeFalsy();
105+
}, {timeout: 5000});
106+
107+
await userEvent.keyboard('{ArrowDown}');
108+
let listbox = await within(body).findByRole('listbox');
109+
await waitFor(() => {
110+
expect(within(listbox).getByText('Luke', {exact: false})).toBeInTheDocument();
111+
}, {timeout: 5000});
112+
113+
await waitFor(() => {
114+
expect(within(listbox).getByRole('progressbar', {hidden: true})).toBeInTheDocument();
115+
}, {timeout: 5000});
116+
117+
await waitFor(() => {
118+
expect(within(listbox).queryByRole('progressbar', {hidden: true})).toBeFalsy();
119+
}, {timeout: 5000});
120+
121+
await userEvent.keyboard('{PageDown}');
122+
123+
await waitFor(() => {
124+
expect(within(listbox).getByText('Greedo', {exact: false})).toBeInTheDocument();
125+
}, {timeout: 5000});
126+
}
127+
};

0 commit comments

Comments
 (0)