Skip to content

Commit 4b0114e

Browse files
committed
feat: Adds table progressive loading empty state
1 parent ad36547 commit 4b0114e

9 files changed

+79
-38
lines changed

pages/table/expandable-rows-test.page.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ export default () => {
195195
</Popover>
196196
</Box>
197197
)}
198+
renderLoaderEmpty={() => <Box>No instances found</Box>}
198199
/>
199200
}
200201
/>
@@ -320,6 +321,8 @@ function useTableData() {
320321
const pages = loadingState.get(item.name)?.pages ?? 0;
321322
return children.slice(0, pages * NESTED_PAGE_SIZE);
322323
};
324+
// Decorate isItemExpandable to allow expandable items with empty children.
325+
collectionResult.collectionProps.expandableRows.isItemExpandable = item => item.type !== 'instance';
323326
// Decorate onExpandableItemToggle to trigger loading when expanded.
324327
collectionResult.collectionProps.expandableRows.onExpandableItemToggle = event => {
325328
onExpandableItemToggle!(event);
@@ -344,6 +347,9 @@ function useTableData() {
344347
if (settings.useServerMock && state && (state.status === 'loading' || state.status === 'error')) {
345348
return state.status;
346349
}
350+
if (item && item.type !== 'instance' && item.children === 0) {
351+
return 'empty';
352+
}
347353
const pages = state?.pages ?? 0;
348354
const pageSize = item ? NESTED_PAGE_SIZE : ROOT_PAGE_SIZE;
349355
const totalItems = item ? getItemChildren!(item).length : allItems.length;

pages/table/expandable-rows.permutations.page.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ const itemsMixed: Instance[] = [
6666
},
6767
],
6868
},
69+
{
70+
name: 'Root-3',
71+
description: 'Root item #3',
72+
children: [],
73+
},
6974
];
7075

7176
interface Permutation {
@@ -250,13 +255,15 @@ export default () => {
250255
expandableRows={{
251256
getItemChildren: item => item.children ?? [],
252257
isItemExpandable: item => !!item.children,
253-
expandedItems: flatten(permutation.items).filter(item => item.children && item.children.length > 0),
258+
expandedItems: flatten(permutation.items).filter(
259+
item => item.children && (item.children.length > 0 || item.name === 'Root-3')
260+
),
254261
onExpandableItemToggle: () => {},
255262
}}
256263
getLoadingStatus={
257264
permutation.progressiveLoading
258265
? item => {
259-
if (!item) {
266+
if (!item || item.name === 'Root-3') {
260267
return 'pending';
261268
}
262269
if (item.name === 'Root-1') {

src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap

+7
Original file line numberDiff line numberDiff line change
@@ -15809,6 +15809,7 @@ table with \`item=null\` and then for each expanded item. The function result is
1580915809
* \`pending\` - Indicates that no request in progress, but more options may be loaded.
1581015810
* \`loading\` - Indicates that data fetching is in progress.
1581115811
* \`finished\` - Indicates that loading has finished and no more requests are expected.
15812+
* \`empty\` - Indicates that the loading has successfully finished but no items were returned.
1581215813
* \`error\` - Indicates that an error occurred during fetch.",
1581315814
"inlineType": {
1581415815
"name": "TableProps.GetLoadingStatus",
@@ -15883,6 +15884,12 @@ Important: in tables with expandable rows the \`firstIndex\`, \`lastIndex\`, and
1588315884
"optional": true,
1588415885
"type": "(data: TableProps.LiveAnnouncement) => string",
1588515886
},
15887+
{
15888+
"description": "Defines loader properties for empty state.",
15889+
"name": "renderLoaderEmpty",
15890+
"optional": true,
15891+
"type": "(detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode",
15892+
},
1588615893
{
1588715894
"description": "Defines loader properties for error state.",
1588815895
"name": "renderLoaderError",

src/table/__tests__/progressive-loading.test.tsx

+40-32
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ function renderTable(tableProps: Partial<TableProps<Instance>>) {
7575
renderLoaderPending={({ item }) => `[pending] Loader for ${item?.name ?? 'TABLE ROOT'}`}
7676
renderLoaderLoading={({ item }) => `[loading] Loader for ${item?.name ?? 'TABLE ROOT'}`}
7777
renderLoaderError={({ item }) => `[error] Loader for ${item?.name ?? 'TABLE ROOT'}`}
78+
renderLoaderEmpty={({ item }) => `[empty] Loader for ${item?.name ?? 'TABLE ROOT'}`}
7879
{...tableProps}
7980
/>
8081
);
@@ -141,7 +142,7 @@ describe('Progressive loading', () => {
141142
]);
142143
});
143144

144-
test.each(['pending', 'loading', 'error'] as const)(
145+
test.each(['pending', 'loading', 'error', 'empty'] as const)(
145146
'renders loaders with correct level offset for status="%s"',
146147
status => {
147148
const { table } = renderTable({
@@ -189,21 +190,24 @@ describe('Progressive loading', () => {
189190
expect(table.findItemsLoaderByItemId('Nested-1.2.2')).toBe(null);
190191
});
191192

192-
test.each(['loading', 'error'] as const)('loader content for status="%s" is announced with aria-live', status => {
193-
const { table } = renderTable({
194-
expandableRows: {
195-
...defaultExpandableRows,
196-
expandedItems: [{ name: 'Root-1' }, { name: 'Nested-1.2' }],
197-
},
198-
getLoadingStatus: () => status,
199-
});
193+
test.each(['empty', 'loading', 'error'] as const)(
194+
'loader content for status="%s" is announced with aria-live',
195+
status => {
196+
const { table } = renderTable({
197+
expandableRows: {
198+
...defaultExpandableRows,
199+
expandedItems: [{ name: 'Root-1' }, { name: 'Nested-1.2' }],
200+
},
201+
getLoadingStatus: () => status,
202+
});
200203

201-
expect(getAriaLive(table.findRootItemsLoader()!)).toBe(`[${status}] Loader for TABLE ROOT`);
202-
expect(getAriaLive(table.findItemsLoaderByItemId('Root-1')!)).toBe(`[${status}] Loader for Root-1`);
203-
expect(getAriaLive(table.findItemsLoaderByItemId('Nested-1.2')!)).toBe(`[${status}] Loader for Nested-1.2`);
204-
});
204+
expect(getAriaLive(table.findRootItemsLoader()!)).toBe(`[${status}] Loader for TABLE ROOT`);
205+
expect(getAriaLive(table.findItemsLoaderByItemId('Root-1')!)).toBe(`[${status}] Loader for Root-1`);
206+
expect(getAriaLive(table.findItemsLoaderByItemId('Nested-1.2')!)).toBe(`[${status}] Loader for Nested-1.2`);
207+
}
208+
);
205209

206-
test.each(['pending', 'loading', 'error'] as const)(
210+
test.each(['empty', 'pending', 'loading', 'error'] as const)(
207211
'warns when table requires a loader but the render function is missing',
208212
status => {
209213
render(
@@ -214,11 +218,12 @@ describe('Progressive loading', () => {
214218
renderLoaderPending={status === 'pending' ? undefined : () => ({ buttonLabel: 'Load more' })}
215219
renderLoaderLoading={status === 'loading' ? undefined : () => ({ loadingText: 'Loading' })}
216220
renderLoaderError={status === 'error' ? undefined : () => ({ cellContent: 'Error' })}
221+
renderLoaderEmpty={status === 'empty' ? undefined : () => ({ cellContent: 'Empty' })}
217222
/>
218223
);
219224
expect(warnOnce).toHaveBeenCalledWith(
220225
'Table',
221-
'Must define `renderLoaderPending`, `renderLoaderLoading`, or `renderLoaderError` when using corresponding loading status.'
226+
'Must define `renderLoaderPending`, `renderLoaderLoading`, `renderLoaderError`, or `renderLoaderEmpty` when using corresponding loading status.'
222227
);
223228
}
224229
);
@@ -246,25 +251,28 @@ describe('Progressive loading', () => {
246251
}
247252
);
248253

249-
test.each(['loading', 'error'] as const)('loader row with status="%s" is added after empty expanded item', status => {
250-
const { table } = renderTable({
251-
items: [
252-
{
253-
name: 'Root-1',
254-
children: [],
254+
test.each(['empty', 'pending', 'loading', 'error'] as const)(
255+
'loader row with status="%s" is added after empty expanded item',
256+
status => {
257+
const { table } = renderTable({
258+
items: [
259+
{
260+
name: 'Root-1',
261+
children: [],
262+
},
263+
],
264+
expandableRows: {
265+
...defaultExpandableRows,
266+
expandedItems: [{ name: 'Root-1' }],
255267
},
256-
],
257-
expandableRows: {
258-
...defaultExpandableRows,
259-
expandedItems: [{ name: 'Root-1' }],
260-
},
261-
getLoadingStatus: () => status,
262-
});
268+
getLoadingStatus: () => status,
269+
});
263270

264-
expect(getTextContent(table.findItemsLoaderByItemId('Root-1')!)).toBe(`[${status}] Loader for Root-1`);
265-
});
271+
expect(getTextContent(table.findItemsLoaderByItemId('Root-1')!)).toBe(`[${status}] Loader for Root-1`);
272+
}
273+
);
266274

267-
test.each([undefined, 'pending', 'finished'] as const)(
275+
test.each([undefined, 'finished'] as const)(
268276
'loader row with status="%s" is not added after empty expanded item and a warning is shown',
269277
status => {
270278
const { table } = renderTable({
@@ -284,7 +292,7 @@ describe('Progressive loading', () => {
284292
expect(table.findItemsLoaderByItemId('Root-1')).toBe(null);
285293
expect(warnOnce).toHaveBeenCalledWith(
286294
'Table',
287-
'Expanded items without children must have "loading" or "error" loading status.'
295+
'Expanded items without children must not have "finished" loading status.'
288296
);
289297
}
290298
);

src/table/interfaces.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ export interface TableProps<T = any> extends BaseComponentProps {
363363
* * `pending` - Indicates that no request in progress, but more options may be loaded.
364364
* * `loading` - Indicates that data fetching is in progress.
365365
* * `finished` - Indicates that loading has finished and no more requests are expected.
366+
* * `empty` - Indicates that the loading has successfully finished but no items were returned.
366367
* * `error` - Indicates that an error occurred during fetch.
367368
**/
368369
getLoadingStatus?: TableProps.GetLoadingStatus<T>;
@@ -378,6 +379,10 @@ export interface TableProps<T = any> extends BaseComponentProps {
378379
* Defines loader properties for error state.
379380
*/
380381
renderLoaderError?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
382+
/**
383+
* Defines loader properties for empty state.
384+
*/
385+
renderLoaderEmpty?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
381386
}
382387

383388
export namespace TableProps {
@@ -552,7 +557,7 @@ export namespace TableProps {
552557

553558
export type GetLoadingStatus<T> = (item: null | T) => TableProps.LoadingStatus;
554559

555-
export type LoadingStatus = 'pending' | 'loading' | 'error' | 'finished';
560+
export type LoadingStatus = 'pending' | 'loading' | 'error' | 'empty' | 'finished';
556561

557562
export interface RenderLoaderDetail<T> {
558563
item: null | T;

src/table/internal.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ const InternalTable = React.forwardRef(
133133
renderLoaderPending,
134134
renderLoaderLoading,
135135
renderLoaderError,
136+
renderLoaderEmpty,
136137
__funnelSubStepProps,
137138
...rest
138139
}: InternalTableProps<T>,
@@ -678,6 +679,7 @@ const InternalTable = React.forwardRef(
678679
renderLoaderPending={renderLoaderPending}
679680
renderLoaderLoading={renderLoaderLoading}
680681
renderLoaderError={renderLoaderError}
682+
renderLoaderEmpty={renderLoaderEmpty}
681683
trackBy={trackBy}
682684
/>
683685
))}

src/table/progressive-loading/items-loader.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface ItemsLoaderProps<T> {
1616
renderLoaderPending?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
1717
renderLoaderLoading?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
1818
renderLoaderError?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
19+
renderLoaderEmpty?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
1920
trackBy?: TableProps.TrackBy<T>;
2021
}
2122

@@ -25,6 +26,7 @@ export function ItemsLoader<T>({
2526
renderLoaderPending,
2627
renderLoaderLoading,
2728
renderLoaderError,
29+
renderLoaderEmpty,
2830
trackBy,
2931
}: ItemsLoaderProps<T>) {
3032
let content: React.ReactNode = null;
@@ -34,10 +36,12 @@ export function ItemsLoader<T>({
3436
content = <InternalLiveRegion tagName="span">{renderLoaderLoading({ item })}</InternalLiveRegion>;
3537
} else if (loadingStatus === 'error' && renderLoaderError) {
3638
content = <InternalLiveRegion tagName="span">{renderLoaderError({ item })}</InternalLiveRegion>;
39+
} else if (loadingStatus === 'empty' && renderLoaderEmpty) {
40+
content = <InternalLiveRegion tagName="span">{renderLoaderEmpty({ item })}</InternalLiveRegion>;
3741
} else {
3842
warnOnce(
3943
'Table',
40-
'Must define `renderLoaderPending`, `renderLoaderLoading`, or `renderLoaderError` when using corresponding loading status.'
44+
'Must define `renderLoaderPending`, `renderLoaderLoading`, `renderLoaderError`, or `renderLoaderEmpty` when using corresponding loading status.'
4145
);
4246
}
4347

src/table/progressive-loading/loader-cell.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function TableLoaderCell<ItemType>({
1616
renderLoaderPending,
1717
renderLoaderLoading,
1818
renderLoaderError,
19+
renderLoaderEmpty,
1920
trackBy,
2021
...props
2122
}: TableLoaderCellProps<ItemType>) {
@@ -28,6 +29,7 @@ export function TableLoaderCell<ItemType>({
2829
renderLoaderPending={renderLoaderPending}
2930
renderLoaderLoading={renderLoaderLoading}
3031
renderLoaderError={renderLoaderError}
32+
renderLoaderEmpty={renderLoaderEmpty}
3133
trackBy={trackBy}
3234
/>
3335
) : null}

src/table/progressive-loading/progressive-loading-utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ export function useProgressiveLoadingProps<T>({
3131
// Insert empty expandable item loader
3232
if (isItemExpanded(items[i]) && getItemChildren(items[i]).length === 0) {
3333
const status = getLoadingStatus?.(items[i]);
34-
if (status && (status === 'loading' || status === 'error')) {
34+
if (status && status !== 'finished') {
3535
allRows.push({ type: 'loader', item: items[i], level: getItemLevel(items[i]), status, from: 0 });
3636
} else {
37-
warnOnce('Table', 'Expanded items without children must have "loading" or "error" loading status.');
37+
warnOnce('Table', 'Expanded items without children must not have "finished" loading status.');
3838
}
3939
}
4040

0 commit comments

Comments
 (0)