Skip to content

Commit 0049153

Browse files
committed
Add S2 Picker async support, support horizontal scrolling, fix types and data-attributes
1 parent 0bb9f21 commit 0049153

File tree

9 files changed

+161
-54
lines changed

9 files changed

+161
-54
lines changed

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,8 @@ export function useLoadMore(props: LoadMoreProps, ref: RefObject<HTMLElement | n
7373
collectionAwaitingUpdate.current = false;
7474
}
7575

76-
sentinelObserver.current = new IntersectionObserver(triggerLoadMore, {root: ref.current, rootMargin: `0px 0px ${100 * scrollOffset}% 0px`});
76+
sentinelObserver.current = new IntersectionObserver(triggerLoadMore, {root: ref.current, rootMargin: `0px ${100 * scrollOffset}% ${100 * scrollOffset}% ${100 * scrollOffset}%`});
7777
if (sentinelRef?.current) {
78-
// console.log('observing', sentinelRef.current.outerHTML)
7978
sentinelObserver.current.observe(sentinelRef.current);
8079
}
8180

packages/@react-spectrum/s2/src/Picker.tsx

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ import {
1818
SelectRenderProps as AriaSelectRenderProps,
1919
Button,
2020
ButtonRenderProps,
21+
Collection,
2122
ContextValue,
2223
ListBox,
2324
ListBoxItem,
2425
ListBoxItemProps,
2526
ListBoxProps,
2627
Provider,
2728
SectionProps,
28-
SelectValue
29+
SelectValue,
30+
UNSTABLE_ListBoxLoadingIndicator
2931
} from 'react-aria-components';
3032
import {baseColor, edgeToText, focusRing, style} from '../style' with {type: 'macro'};
3133
import {centerBaseline} from './CenterBaseline';
@@ -60,14 +62,14 @@ import {Placement} from 'react-aria';
6062
import {PopoverBase} from './Popover';
6163
import {PressResponder} from '@react-aria/interactions';
6264
import {pressScale} from './pressScale';
65+
import {ProgressCircle} from './ProgressCircle';
6366
import {raw} from '../style/style-macro' with {type: 'macro'};
6467
import React, {createContext, forwardRef, ReactNode, useContext, useRef, useState} from 'react';
6568
import {useFocusableRef} from '@react-spectrum/utils';
6669
import {useGlobalListeners} from '@react-aria/utils';
6770
import {useLocalizedStringFormatter} from '@react-aria/i18n';
6871
import {useSpectrumContextProps} from './useSpectrumContextProps';
6972

70-
7173
export interface PickerStyleProps {
7274
/**
7375
* The size of the Picker.
@@ -226,6 +228,29 @@ const iconStyles = style({
226228
'--iconPrimary': {
227229
type: 'fill',
228230
value: 'currentColor'
231+
},
232+
color: {
233+
isLoading: 'disabled'
234+
}
235+
});
236+
237+
const loadingWrapperStyles = style({
238+
gridColumnStart: '1',
239+
gridColumnEnd: '-1',
240+
display: 'flex',
241+
alignItems: 'center',
242+
justifyContent: 'center',
243+
marginY: 8
244+
});
245+
246+
const progressCircleStyles = style({
247+
size: {
248+
size: {
249+
S: 16,
250+
M: 20,
251+
L: 22,
252+
XL: 26
253+
}
229254
}
230255
});
231256

@@ -259,6 +284,7 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick
259284
UNSAFE_style,
260285
placeholder = stringFormatter.format('picker.placeholder'),
261286
isQuiet,
287+
isLoading,
262288
...pickerProps
263289
} = props;
264290

@@ -289,6 +315,38 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick
289315
}, {once: true, capture: true});
290316
};
291317

318+
// TODO: no designs for the spinner in the listbox that I've seen so will need to double check
319+
let renderer;
320+
let loadingSpinner = (
321+
<UNSTABLE_ListBoxLoadingIndicator
322+
className={loadingWrapperStyles}>
323+
<ProgressCircle
324+
isIndeterminate
325+
size="S"
326+
styles={progressCircleStyles({size})}
327+
// Same loading string as table
328+
aria-label={stringFormatter.format('table.loadingMore')} />
329+
</UNSTABLE_ListBoxLoadingIndicator>
330+
);
331+
332+
if (typeof children === 'function' && items) {
333+
renderer = (
334+
<>
335+
<Collection items={items}>
336+
{children}
337+
</Collection>
338+
{isLoading && loadingSpinner}
339+
</>
340+
);
341+
} else {
342+
renderer = (
343+
<>
344+
{children}
345+
{isLoading && loadingSpinner}
346+
</>
347+
);
348+
}
349+
292350
return (
293351
<AriaSelect
294352
{...pickerProps}
@@ -359,11 +417,18 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick
359417
}}
360418
</SelectValue>
361419
{isInvalid && (
420+
// TODO: in Figma it shows the icon as being disabled when loading, confirm with spectrum
362421
<FieldErrorIcon isDisabled={isDisabled} />
363422
)}
423+
{isLoading && (
424+
<ProgressCircle
425+
isIndeterminate
426+
size="S"
427+
styles={progressCircleStyles({size})} />
428+
)}
364429
<ChevronIcon
365430
size={size}
366-
className={iconStyles} />
431+
className={iconStyles({isLoading})} />
367432
{isFocusVisible && isQuiet && <span className={quietFocusLine} /> }
368433
{isInvalid && !isDisabled && !isQuiet &&
369434
// @ts-ignore known limitation detecting functions from the theme
@@ -414,7 +479,7 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick
414479
<ListBox
415480
items={items}
416481
className={menu({size})}>
417-
{children}
482+
{renderer}
418483
</ListBox>
419484
</Provider>
420485
</PopoverBase>

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import DeviceDesktopIcon from '../s2wf-icons/S2_Icon_DeviceDesktop_20_N.svg';
2929
import DeviceTabletIcon from '../s2wf-icons/S2_Icon_DeviceTablet_20_N.svg';
3030
import type {Meta, StoryObj} from '@storybook/react';
3131
import {style} from '../style' with {type: 'macro'};
32+
import {useAsyncList} from '@react-stately/data';
3233

3334
const meta: Meta<typeof Picker<any>> = {
3435
component: Picker,
@@ -201,3 +202,43 @@ export const ContextualHelpExample = {
201202
label: 'Ice cream flavor'
202203
}
203204
};
205+
206+
interface Character {
207+
name: string,
208+
height: number,
209+
mass: number,
210+
birth_year: number
211+
}
212+
213+
const AsyncPicker = (args: any) => {
214+
let list = useAsyncList<Character>({
215+
async load({signal, cursor}) {
216+
if (cursor) {
217+
cursor = cursor.replace(/^http:\/\//i, 'https://');
218+
}
219+
220+
// Slow down load so progress circle can appear
221+
await new Promise(resolve => setTimeout(resolve, 2000));
222+
let res = await fetch(cursor || 'https://swapi.py4e.com/api/people/?search=', {signal});
223+
let json = await res.json();
224+
return {
225+
items: json.results,
226+
cursor: json.next
227+
};
228+
}
229+
});
230+
231+
return (
232+
<Picker {...args} isLoading={list.isLoading} onLoadMore={list.loadMore} items={list.items}>
233+
{(item: Character) => <PickerItem id={item.name} textValue={item.name}>{item.name}</PickerItem>}
234+
</Picker>
235+
);
236+
};
237+
238+
export const AsyncPickerStory = {
239+
render: AsyncPicker,
240+
args: {
241+
...Example.args
242+
},
243+
name: 'Async loading picker'
244+
};

packages/react-aria-components/src/ComboBox.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212
import {AriaComboBoxProps, useComboBox, useFilter} from 'react-aria';
13+
import {AsyncLoadable, forwardRefType, RefObject} from '@react-types/shared';
1314
import {ButtonContext} from './Button';
1415
import {Collection, ComboBoxState, Node, useComboBoxState} from 'react-stately';
1516
import {CollectionBuilder} from '@react-aria/collections';
1617
import {ContextValue, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
1718
import {FieldErrorContext} from './FieldError';
1819
import {filterDOMProps, useResizeObserver} from '@react-aria/utils';
1920
import {FormContext} from './Form';
20-
import {forwardRefType, RefObject} from '@react-types/shared';
2121
import {GroupContext} from './Group';
2222
import {InputContext} from './Input';
2323
import {LabelContext} from './Label';
@@ -51,13 +51,13 @@ export interface ComboBoxRenderProps {
5151
// TODO: do we want loadingState for RAC Combobox or just S2
5252
// TODO: move types somewhere common later
5353
/**
54-
* Whether the combobox is loading items.
54+
* Whether the combobox is currently loading items.
5555
* @selector [data-loading]
5656
*/
5757
isLoading?: boolean
5858
}
5959

60-
export interface ComboBoxProps<T extends object> extends Omit<AriaComboBoxProps<T>, 'children' | 'placeholder' | 'label' | 'description' | 'errorMessage' | 'validationState' | 'validationBehavior'>, RACValidation, RenderProps<ComboBoxRenderProps>, SlotProps {
60+
export interface ComboBoxProps<T extends object> extends Omit<AriaComboBoxProps<T>, 'children' | 'placeholder' | 'label' | 'description' | 'errorMessage' | 'validationState' | 'validationBehavior'>, RACValidation, RenderProps<ComboBoxRenderProps>, SlotProps, AsyncLoadable {
6161
/** The filter function used to determine if a option should be included in the combo box list. */
6262
defaultFilter?: (textValue: string, inputValue: string) => boolean,
6363
/**
@@ -67,11 +67,7 @@ export interface ComboBoxProps<T extends object> extends Omit<AriaComboBoxProps<
6767
*/
6868
formValue?: 'text' | 'key',
6969
/** Whether the combo box allows the menu to be open when the collection is empty. */
70-
allowsEmptyCollection?: boolean,
71-
/** Whether the combobox is loading items. */
72-
isLoading?: boolean,
73-
/** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */
74-
onLoadMore?: () => void
70+
allowsEmptyCollection?: boolean
7571
}
7672

7773
export const ComboBoxContext = createContext<ContextValue<ComboBoxProps<any>, HTMLDivElement>>(null);

packages/react-aria-components/src/GridList.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212
import {AriaGridListProps, DraggableItemResult, DragPreviewRenderer, DropIndicatorAria, DroppableCollectionResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useGridList, useGridListItem, useGridListSelectionCheckbox, useHover, useLocale, useVisuallyHidden} from 'react-aria';
13+
import {AsyncLoadable, forwardRefType, HoverEvents, Key, LinkDOMProps, RefObject} from '@react-types/shared';
1314
import {ButtonContext} from './Button';
1415
import {CheckboxContext} from './RSPContexts';
1516
import {Collection, CollectionBuilder, createLeafComponent} from '@react-aria/collections';
1617
import {CollectionProps, CollectionRendererContext, DefaultCollectionRenderer, ItemRenderProps} from './Collection';
17-
import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, ScrollableProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps} from './utils';
18+
import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, ScrollableProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps} from './utils';
1819
import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop';
1920
import {DragAndDropHooks} from './useDragAndDrop';
2021
import {DraggableCollectionState, DroppableCollectionState, Collection as ICollection, ListState, Node, SelectionBehavior, useListState} from 'react-stately';
2122
import {filterDOMProps, inertValue, useLoadMore, useObjectRef} from '@react-aria/utils';
22-
import {forwardRefType, HoverEvents, Key, LinkDOMProps, RefObject, StyleProps} from '@react-types/shared';
2323
import {ListStateContext} from './ListBox';
2424
import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
2525
import {TextContext} from './Text';
@@ -53,10 +53,15 @@ export interface GridListRenderProps {
5353
/**
5454
* State of the grid list.
5555
*/
56-
state: ListState<unknown>
56+
state: ListState<unknown>,
57+
/**
58+
* Whether the grid list is currently loading items.
59+
* @selector [data-loading]
60+
*/
61+
isLoading?: boolean
5762
}
5863

59-
export interface GridListProps<T> extends Omit<AriaGridListProps<T>, 'children'>, CollectionProps<T>, StyleRenderProps<GridListRenderProps>, SlotProps, ScrollableProps<HTMLDivElement> {
64+
export interface GridListProps<T> extends Omit<AriaGridListProps<T>, 'children'>, CollectionProps<T>, StyleRenderProps<GridListRenderProps>, SlotProps, ScrollableProps<HTMLDivElement>, AsyncLoadable {
6065
/**
6166
* Whether typeahead navigation is disabled.
6267
* @default false
@@ -72,11 +77,7 @@ export interface GridListProps<T> extends Omit<AriaGridListProps<T>, 'children'>
7277
* Whether the items are arranged in a stack or grid.
7378
* @default 'stack'
7479
*/
75-
layout?: 'stack' | 'grid',
76-
// TODO: move types somewhere common later
77-
// Discuss if we want the RAC components to call the useLoadMore directly (think they have to if we wanna support using <Collection> as child)
78-
isLoading?: boolean,
79-
onLoadMore?: () => void
80+
layout?: 'stack' | 'grid'
8081
}
8182

8283

@@ -200,7 +201,8 @@ function GridListInner<T extends object>({props, collection, gridListRef: ref}:
200201
isFocused,
201202
isFocusVisible,
202203
layout,
203-
state
204+
state,
205+
isLoading: isLoading || false
204206
};
205207
let renderProps = useRenderProps({
206208
className: props.className,
@@ -243,7 +245,8 @@ function GridListInner<T extends object>({props, collection, gridListRef: ref}:
243245
data-empty={state.collection.size === 0 || undefined}
244246
data-focused={isFocused || undefined}
245247
data-focus-visible={isFocusVisible || undefined}
246-
data-layout={layout}>
248+
data-layout={layout}
249+
data-loading={isLoading || undefined}>
247250
<Provider
248251
values={[
249252
[ListStateContext, state],

packages/react-aria-components/src/ListBox.tsx

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

1313
import {AriaListBoxOptions, AriaListBoxProps, DraggableItemResult, DragPreviewRenderer, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useHover, useListBox, useListBoxSection, useLocale, useOption} from 'react-aria';
14+
import {AsyncLoadable, forwardRefType, HoverEvents, Key, LinkDOMProps, RefObject} from '@react-types/shared';
1415
import {Collection, CollectionBuilder, createBranchComponent, createLeafComponent} from '@react-aria/collections';
1516
import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps} from './Collection';
16-
import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, ScrollableProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils';
17+
import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, ScrollableProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils';
1718
import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop';
1819
import {DragAndDropHooks} from './useDragAndDrop';
1920
import {DraggableCollectionState, DroppableCollectionState, ListState, Node, Orientation, SelectionBehavior, UNSTABLE_useFilteredListState, useListState} from 'react-stately';
2021
import {filterDOMProps, inertValue, mergeRefs, useLoadMore, useObjectRef} from '@react-aria/utils';
21-
import {forwardRefType, HoverEvents, Key, LinkDOMProps, RefObject, StyleProps} from '@react-types/shared';
2222
import {HeaderContext} from './Header';
2323
import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
2424
import {SeparatorContext} from './Separator';
@@ -54,10 +54,15 @@ export interface ListBoxRenderProps {
5454
/**
5555
* State of the listbox.
5656
*/
57-
state: ListState<unknown>
57+
state: ListState<unknown>,
58+
/**
59+
* Whether the listbox is currently loading items.
60+
* @selector [data-loading]
61+
*/
62+
isLoading?: boolean
5863
}
5964

60-
export interface ListBoxProps<T> extends Omit<AriaListBoxProps<T>, 'children' | 'label'>, CollectionProps<T>, StyleRenderProps<ListBoxRenderProps>, SlotProps, ScrollableProps<HTMLDivElement> {
65+
export interface ListBoxProps<T> extends Omit<AriaListBoxProps<T>, 'children' | 'label'>, CollectionProps<T>, StyleRenderProps<ListBoxRenderProps>, SlotProps, ScrollableProps<HTMLDivElement>, AsyncLoadable {
6166
/** How multiple selection should behave in the collection. */
6267
selectionBehavior?: SelectionBehavior,
6368
/** The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the ListBox. */
@@ -74,11 +79,7 @@ export interface ListBoxProps<T> extends Omit<AriaListBoxProps<T>, 'children' |
7479
* direction that the collection scrolls.
7580
* @default 'vertical'
7681
*/
77-
orientation?: Orientation,
78-
// TODO: move types somewhere common later
79-
// Discuss if we want the RAC components to call the useLoadMore directly (think they have to if we wanna support using <Collection> as child)
80-
isLoading?: boolean,
81-
onLoadMore?: () => void
82+
orientation?: Orientation
8283
}
8384

8485
export const ListBoxContext = createContext<ContextValue<ListBoxProps<any>, HTMLDivElement>>(null);
@@ -210,7 +211,8 @@ function ListBoxInner<T extends object>({state: inputState, props, listBoxRef}:
210211
isFocused,
211212
isFocusVisible,
212213
layout: props.layout || 'stack',
213-
state
214+
state,
215+
isLoading: props.isLoading || false
214216
};
215217
let renderProps = useRenderProps({
216218
className: props.className,
@@ -261,7 +263,8 @@ function ListBoxInner<T extends object>({state: inputState, props, listBoxRef}:
261263
data-focused={isFocused || undefined}
262264
data-focus-visible={isFocusVisible || undefined}
263265
data-layout={props.layout || 'stack'}
264-
data-orientation={props.orientation || 'vertical'}>
266+
data-orientation={props.orientation || 'vertical'}
267+
data-loading={props.isLoading || undefined}>
265268
<Provider
266269
values={[
267270
[ListBoxContext, props],

0 commit comments

Comments
 (0)