Skip to content

Commit dfe72ee

Browse files
authored
ListView loading spinner height fix (#2795)
* fixing loading spinner height in ListView and adding loading support * adding tests and fixing lint * fixing css * updating test to use less parentNode
1 parent 4809747 commit dfe72ee

File tree

4 files changed

+98
-13
lines changed

4 files changed

+98
-13
lines changed

packages/@react-spectrum/list/src/ListView.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212
import {
1313
AriaLabelingProps,
14+
AsyncLoadable,
1415
CollectionBase,
1516
DOMProps,
1617
DOMRef,
@@ -55,20 +56,22 @@ const ROW_HEIGHTS = {
5556
export function useListLayout<T>(state: ListState<T>, density: ListViewProps<T>['density']) {
5657
let {scale} = useProvider();
5758
let collator = useCollator({usage: 'search', sensitivity: 'base'});
59+
let isEmpty = state.collection.size === 0;
5860
let layout = useMemo(() =>
5961
new ListLayout<T>({
6062
estimatedRowHeight: ROW_HEIGHTS[density][scale],
6163
padding: 0,
62-
collator
64+
collator,
65+
loaderHeight: isEmpty ? null : ROW_HEIGHTS[density][scale]
6366
})
64-
, [collator, scale, density]);
67+
, [collator, scale, density, isEmpty]);
6568

6669
layout.collection = state.collection;
6770
layout.disabledKeys = state.disabledKeys;
6871
return layout;
6972
}
7073

71-
interface ListViewProps<T> extends CollectionBase<T>, DOMProps, AriaLabelingProps, StyleProps, MultipleSelection, SpectrumSelectionProps {
74+
interface ListViewProps<T> extends CollectionBase<T>, DOMProps, AriaLabelingProps, StyleProps, MultipleSelection, SpectrumSelectionProps, Omit<AsyncLoadable, 'isLoading'> {
7275
/**
7376
* Sets the amount of vertical padding within each cell.
7477
* @default 'regular'
@@ -84,6 +87,7 @@ interface ListViewProps<T> extends CollectionBase<T>, DOMProps, AriaLabelingProp
8487
function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDivElement>) {
8588
let {
8689
density = 'regular',
90+
onLoadMore,
8791
loadingState,
8892
isQuiet,
8993
transitionDuration = 0,
@@ -92,6 +96,7 @@ function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDiv
9296
let domRef = useDOMRef(ref);
9397
let {collection} = useListState(props);
9498
let formatMessage = useMessageFormatter(intlMessages);
99+
let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
95100

96101
let {styleProps} = useStyleProps(props);
97102
let {direction} = useLocale();
@@ -138,7 +143,7 @@ function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDiv
138143
}, state, domRef);
139144

140145
// Sync loading state into the layout.
141-
layout.isLoading = loadingState === 'loading';
146+
layout.isLoading = isLoading;
142147

143148
let focusedKey = state.selectionManager.focusedKey;
144149
let focusedItem = gridCollection.getItem(state.selectionManager.focusedKey);
@@ -151,6 +156,8 @@ function ListView<T extends object>(props: ListViewProps<T>, ref: DOMRef<HTMLDiv
151156
<Virtualizer
152157
{...gridProps}
153158
{...styleProps}
159+
isLoading={isLoading}
160+
onLoadMore={onLoadMore}
154161
ref={domRef}
155162
focusedKey={focusedKey}
156163
scrollDirection="vertical"
@@ -208,7 +215,14 @@ function CenteredWrapper({children}) {
208215
<div
209216
role="row"
210217
aria-rowindex={state.collection.size + 1}
211-
className={classNames(listStyles, 'react-spectrum-ListView-centeredWrapper')}>
218+
className={
219+
classNames(
220+
listStyles,
221+
'react-spectrum-ListView-centeredWrapper',
222+
{
223+
'react-spectrum-ListView-centeredWrapper--loadingMore': state.collection.size > 0
224+
}
225+
)}>
212226
<div role="gridcell">
213227
{children}
214228
</div>

packages/@react-spectrum/list/src/listview.css

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
}
6161

6262
/* avoid double border for adjacent selected items */
63-
&.is-previous-selected {
63+
&.is-previous-selected {
6464
&:not(.is-focused) {
6565
border-top-color: transparent;
6666
}
@@ -108,7 +108,7 @@
108108
;
109109
align-items: center;
110110
}
111-
111+
112112
.react-spectrum-ListViewItem-checkbox {
113113
grid-area: checkbox;
114114
align-items: center;
@@ -161,7 +161,7 @@
161161
grid-area: chevron;
162162
padding-inline-start: var(--spectrum-global-dimension-size-75);
163163
}
164-
164+
165165
/* give first and last items border-radius to match listview container */
166166
div:first-child > div[role="row"] > & {
167167
border-start-start-radius: var(--spectrum-listview-item-start-end-border-radius);
@@ -191,5 +191,7 @@
191191
justify-content: center;
192192
width: 100%;
193193
height: 100%;
194+
&.react-spectrum-ListView-centeredWrapper--loadingMore {
195+
padding-top: var(--spectrum-global-dimension-size-50);
196+
}
194197
}
195-

packages/@react-spectrum/list/stories/ListView.stories.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import MoreSmall from '@spectrum-icons/workflow/MoreSmall';
1818
import NoSearchResults from '@spectrum-icons/illustrations/src/NoSearchResults';
1919
import React, {useEffect, useState} from 'react';
2020
import {storiesOf} from '@storybook/react';
21-
21+
import {useAsyncList} from '@react-stately/data';
2222

2323
function renderEmptyState() {
2424
return (
@@ -118,6 +118,16 @@ storiesOf('ListView', module)
118118
{[]}
119119
</ListView>
120120
))
121+
.add('loadingMore', () => (
122+
<ListView width="300px" height="300px" loadingState="loadingMore">
123+
<Item textValue="row 1">row 1</Item>
124+
<Item textValue="row 2">row 2</Item>
125+
<Item textValue="row 3">row 3</Item>
126+
</ListView>
127+
))
128+
.add('async listview loading', () => (
129+
<AsyncList />
130+
))
121131
.add('density: compact', () => (
122132
<ListView width="250px" density="compact">
123133
<Item textValue="row 1">row 1</Item>
@@ -350,3 +360,41 @@ function EmptyTest() {
350360
</div>
351361
);
352362
}
363+
364+
function AsyncList() {
365+
interface StarWarsChar {
366+
name: string,
367+
url: string
368+
}
369+
370+
let list = useAsyncList<StarWarsChar>({
371+
async load({signal, cursor}) {
372+
if (cursor) {
373+
cursor = cursor.replace(/^http:\/\//i, 'https://');
374+
}
375+
376+
// Slow down load so progress circle can appear
377+
await new Promise(resolve => setTimeout(resolve, 1500));
378+
let res = await fetch(cursor || 'https://swapi.py4e.com/api/people/?search=', {signal});
379+
let json = await res.json();
380+
return {
381+
items: json.results,
382+
cursor: json.next
383+
};
384+
}
385+
});
386+
return (
387+
<ListView
388+
selectionMode="multiple"
389+
aria-label="example async loading list"
390+
width="size-6000"
391+
height="size-3000"
392+
items={list.items}
393+
loadingState={list.loadingState}
394+
onLoadMore={list.loadMore}>
395+
{(item) => (
396+
<Item key={item.name} textValue={item.name}>{item.name}</Item>
397+
)}
398+
</ListView>
399+
);
400+
}

packages/@react-spectrum/list/test/ListView.test.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,30 @@ describe('ListView', function () {
282282
});
283283
});
284284

285-
it('should display loading affordance', function () {
286-
let {getByRole} = render(<ListView aria-label="List" loadingState="loading">{[]}</ListView>);
287-
expect(getByRole('progressbar')).toBeTruthy();
285+
it('should display loading affordance with proper height (isLoading)', function () {
286+
let {getAllByRole} = render(<ListView aria-label="List" loadingState="loading">{[]}</ListView>);
287+
let row = getAllByRole('row')[0];
288+
expect(row.parentNode.style.height).toBe('1000px');
289+
let progressbar = within(row).getByRole('progressbar');
290+
expect(progressbar).toBeTruthy();
291+
});
292+
293+
it('should display loading affordance with proper height (isLoadingMore)', function () {
294+
let items = [
295+
{key: 'foo', label: 'Foo'},
296+
{key: 'bar', label: 'Bar'},
297+
{key: 'baz', label: 'Baz'}
298+
];
299+
let {getByRole} = render(
300+
<ListView items={items} aria-label="List" loadingState="loadingMore">
301+
{item =>
302+
<Item textValue={item.key}>{item.label}</Item>
303+
}
304+
</ListView>
305+
);
306+
let progressbar = getByRole('progressbar');
307+
expect(progressbar).toBeTruthy();
308+
expect(progressbar.parentNode.parentNode.parentNode.style.height).toBe('40px');
288309
});
289310

290311
it('should render empty state', function () {

0 commit comments

Comments
 (0)