Skip to content
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

feat: Adds table progressive loading empty state #3037

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions pages/table/expandable-rows-test.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export default () => {
</Popover>
</Box>
)}
renderLoaderEmpty={() => <Box>No instances found</Box>}
/>
}
/>
Expand Down Expand Up @@ -320,6 +321,8 @@ function useTableData() {
const pages = loadingState.get(item.name)?.pages ?? 0;
return children.slice(0, pages * NESTED_PAGE_SIZE);
};
// Decorate isItemExpandable to allow expandable items with empty children.
collectionResult.collectionProps.expandableRows.isItemExpandable = item => item.type !== 'instance';
// Decorate onExpandableItemToggle to trigger loading when expanded.
collectionResult.collectionProps.expandableRows.onExpandableItemToggle = event => {
onExpandableItemToggle!(event);
Expand All @@ -344,6 +347,9 @@ function useTableData() {
if (settings.useServerMock && state && (state.status === 'loading' || state.status === 'error')) {
return state.status;
}
if (item && item.type !== 'instance' && item.children === 0) {
return 'empty';
}
const pages = state?.pages ?? 0;
const pageSize = item ? NESTED_PAGE_SIZE : ROOT_PAGE_SIZE;
const totalItems = item ? getItemChildren!(item).length : allItems.length;
Expand Down
11 changes: 9 additions & 2 deletions pages/table/expandable-rows.permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ const itemsMixed: Instance[] = [
},
],
},
{
name: 'Root-3',
description: 'Root item #3',
children: [],
},
];

interface Permutation {
Expand Down Expand Up @@ -250,13 +255,15 @@ export default () => {
expandableRows={{
getItemChildren: item => item.children ?? [],
isItemExpandable: item => !!item.children,
expandedItems: flatten(permutation.items).filter(item => item.children && item.children.length > 0),
expandedItems: flatten(permutation.items).filter(
item => item.children && (item.children.length > 0 || item.name === 'Root-3')
),
onExpandableItemToggle: () => {},
}}
getLoadingStatus={
permutation.progressiveLoading
? item => {
if (!item) {
if (!item || item.name === 'Root-3') {
return 'pending';
}
if (item.name === 'Root-1') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15809,6 +15809,7 @@ table with \`item=null\` and then for each expanded item. The function result is
* \`pending\` - Indicates that no request in progress, but more options may be loaded.
* \`loading\` - Indicates that data fetching is in progress.
* \`finished\` - Indicates that loading has finished and no more requests are expected.
* \`empty\` - Indicates that the loading has successfully finished but no items were returned.
* \`error\` - Indicates that an error occurred during fetch.",
"inlineType": {
"name": "TableProps.GetLoadingStatus",
Expand Down Expand Up @@ -15883,6 +15884,12 @@ Important: in tables with expandable rows the \`firstIndex\`, \`lastIndex\`, and
"optional": true,
"type": "(data: TableProps.LiveAnnouncement) => string",
},
{
"description": "Defines loader properties for empty state.",
"name": "renderLoaderEmpty",
"optional": true,
"type": "(detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode",
},
{
"description": "Defines loader properties for error state.",
"name": "renderLoaderError",
Expand Down
72 changes: 40 additions & 32 deletions src/table/__tests__/progressive-loading.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function renderTable(tableProps: Partial<TableProps<Instance>>) {
renderLoaderPending={({ item }) => `[pending] Loader for ${item?.name ?? 'TABLE ROOT'}`}
renderLoaderLoading={({ item }) => `[loading] Loader for ${item?.name ?? 'TABLE ROOT'}`}
renderLoaderError={({ item }) => `[error] Loader for ${item?.name ?? 'TABLE ROOT'}`}
renderLoaderEmpty={({ item }) => `[empty] Loader for ${item?.name ?? 'TABLE ROOT'}`}
{...tableProps}
/>
);
Expand Down Expand Up @@ -141,7 +142,7 @@ describe('Progressive loading', () => {
]);
});

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

test.each(['loading', 'error'] as const)('loader content for status="%s" is announced with aria-live', status => {
const { table } = renderTable({
expandableRows: {
...defaultExpandableRows,
expandedItems: [{ name: 'Root-1' }, { name: 'Nested-1.2' }],
},
getLoadingStatus: () => status,
});
test.each(['empty', 'loading', 'error'] as const)(
'loader content for status="%s" is announced with aria-live',
status => {
const { table } = renderTable({
expandableRows: {
...defaultExpandableRows,
expandedItems: [{ name: 'Root-1' }, { name: 'Nested-1.2' }],
},
getLoadingStatus: () => status,
});

expect(getAriaLive(table.findRootItemsLoader()!)).toBe(`[${status}] Loader for TABLE ROOT`);
expect(getAriaLive(table.findItemsLoaderByItemId('Root-1')!)).toBe(`[${status}] Loader for Root-1`);
expect(getAriaLive(table.findItemsLoaderByItemId('Nested-1.2')!)).toBe(`[${status}] Loader for Nested-1.2`);
});
expect(getAriaLive(table.findRootItemsLoader()!)).toBe(`[${status}] Loader for TABLE ROOT`);
expect(getAriaLive(table.findItemsLoaderByItemId('Root-1')!)).toBe(`[${status}] Loader for Root-1`);
expect(getAriaLive(table.findItemsLoaderByItemId('Nested-1.2')!)).toBe(`[${status}] Loader for Nested-1.2`);
}
);

test.each(['pending', 'loading', 'error'] as const)(
test.each(['empty', 'pending', 'loading', 'error'] as const)(
'warns when table requires a loader but the render function is missing',
status => {
render(
Expand All @@ -214,11 +218,12 @@ describe('Progressive loading', () => {
renderLoaderPending={status === 'pending' ? undefined : () => ({ buttonLabel: 'Load more' })}
renderLoaderLoading={status === 'loading' ? undefined : () => ({ loadingText: 'Loading' })}
renderLoaderError={status === 'error' ? undefined : () => ({ cellContent: 'Error' })}
renderLoaderEmpty={status === 'empty' ? undefined : () => ({ cellContent: 'Empty' })}
/>
);
expect(warnOnce).toHaveBeenCalledWith(
'Table',
'Must define `renderLoaderPending`, `renderLoaderLoading`, or `renderLoaderError` when using corresponding loading status.'
'Must define `renderLoaderPending`, `renderLoaderLoading`, `renderLoaderError`, or `renderLoaderEmpty` when using corresponding loading status.'
);
}
);
Expand Down Expand Up @@ -246,25 +251,28 @@ describe('Progressive loading', () => {
}
);

test.each(['loading', 'error'] as const)('loader row with status="%s" is added after empty expanded item', status => {
const { table } = renderTable({
items: [
{
name: 'Root-1',
children: [],
test.each(['empty', 'pending', 'loading', 'error'] as const)(
'loader row with status="%s" is added after empty expanded item',
status => {
const { table } = renderTable({
items: [
{
name: 'Root-1',
children: [],
},
],
expandableRows: {
...defaultExpandableRows,
expandedItems: [{ name: 'Root-1' }],
},
],
expandableRows: {
...defaultExpandableRows,
expandedItems: [{ name: 'Root-1' }],
},
getLoadingStatus: () => status,
});
getLoadingStatus: () => status,
});

expect(getTextContent(table.findItemsLoaderByItemId('Root-1')!)).toBe(`[${status}] Loader for Root-1`);
});
expect(getTextContent(table.findItemsLoaderByItemId('Root-1')!)).toBe(`[${status}] Loader for Root-1`);
}
);

test.each([undefined, 'pending', 'finished'] as const)(
test.each([undefined, 'finished'] as const)(
'loader row with status="%s" is not added after empty expanded item and a warning is shown',
status => {
const { table } = renderTable({
Expand All @@ -284,7 +292,7 @@ describe('Progressive loading', () => {
expect(table.findItemsLoaderByItemId('Root-1')).toBe(null);
expect(warnOnce).toHaveBeenCalledWith(
'Table',
'Expanded items without children must have "loading" or "error" loading status.'
'Expanded items without children must not have "finished" loading status.'
);
}
);
Expand Down
7 changes: 6 additions & 1 deletion src/table/interfaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ export interface TableProps<T = any> extends BaseComponentProps {
* * `pending` - Indicates that no request in progress, but more options may be loaded.
* * `loading` - Indicates that data fetching is in progress.
* * `finished` - Indicates that loading has finished and no more requests are expected.
* * `empty` - Indicates that the loading has successfully finished but no items were returned.
* * `error` - Indicates that an error occurred during fetch.
**/
getLoadingStatus?: TableProps.GetLoadingStatus<T>;
Expand All @@ -378,6 +379,10 @@ export interface TableProps<T = any> extends BaseComponentProps {
* Defines loader properties for error state.
*/
renderLoaderError?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
/**
* Defines loader properties for empty state.
*/
renderLoaderEmpty?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
}

export namespace TableProps {
Expand Down Expand Up @@ -552,7 +557,7 @@ export namespace TableProps {

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

export type LoadingStatus = 'pending' | 'loading' | 'error' | 'finished';
export type LoadingStatus = 'pending' | 'loading' | 'error' | 'empty' | 'finished';

export interface RenderLoaderDetail<T> {
item: null | T;
Expand Down
2 changes: 2 additions & 0 deletions src/table/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const InternalTable = React.forwardRef(
renderLoaderPending,
renderLoaderLoading,
renderLoaderError,
renderLoaderEmpty,
__funnelSubStepProps,
...rest
}: InternalTableProps<T>,
Expand Down Expand Up @@ -678,6 +679,7 @@ const InternalTable = React.forwardRef(
renderLoaderPending={renderLoaderPending}
renderLoaderLoading={renderLoaderLoading}
renderLoaderError={renderLoaderError}
renderLoaderEmpty={renderLoaderEmpty}
trackBy={trackBy}
/>
))}
Expand Down
6 changes: 5 additions & 1 deletion src/table/progressive-loading/items-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ItemsLoaderProps<T> {
renderLoaderPending?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
renderLoaderLoading?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
renderLoaderError?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
renderLoaderEmpty?: (detail: TableProps.RenderLoaderDetail<T>) => React.ReactNode;
trackBy?: TableProps.TrackBy<T>;
}

Expand All @@ -25,6 +26,7 @@ export function ItemsLoader<T>({
renderLoaderPending,
renderLoaderLoading,
renderLoaderError,
renderLoaderEmpty,
trackBy,
}: ItemsLoaderProps<T>) {
let content: React.ReactNode = null;
Expand All @@ -34,10 +36,12 @@ export function ItemsLoader<T>({
content = <InternalLiveRegion tagName="span">{renderLoaderLoading({ item })}</InternalLiveRegion>;
} else if (loadingStatus === 'error' && renderLoaderError) {
content = <InternalLiveRegion tagName="span">{renderLoaderError({ item })}</InternalLiveRegion>;
} else if (loadingStatus === 'empty' && renderLoaderEmpty) {
content = <InternalLiveRegion tagName="span">{renderLoaderEmpty({ item })}</InternalLiveRegion>;
} else {
warnOnce(
'Table',
'Must define `renderLoaderPending`, `renderLoaderLoading`, or `renderLoaderError` when using corresponding loading status.'
'Must define `renderLoaderPending`, `renderLoaderLoading`, `renderLoaderError`, or `renderLoaderEmpty` when using corresponding loading status.'
);
}

Expand Down
2 changes: 2 additions & 0 deletions src/table/progressive-loading/loader-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function TableLoaderCell<ItemType>({
renderLoaderPending,
renderLoaderLoading,
renderLoaderError,
renderLoaderEmpty,
trackBy,
...props
}: TableLoaderCellProps<ItemType>) {
Expand All @@ -28,6 +29,7 @@ export function TableLoaderCell<ItemType>({
renderLoaderPending={renderLoaderPending}
renderLoaderLoading={renderLoaderLoading}
renderLoaderError={renderLoaderError}
renderLoaderEmpty={renderLoaderEmpty}
trackBy={trackBy}
/>
) : null}
Expand Down
4 changes: 2 additions & 2 deletions src/table/progressive-loading/progressive-loading-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ export function useProgressiveLoadingProps<T>({
// Insert empty expandable item loader
if (isItemExpanded(items[i]) && getItemChildren(items[i]).length === 0) {
const status = getLoadingStatus?.(items[i]);
if (status && (status === 'loading' || status === 'error')) {
if (status && status !== 'finished') {
allRows.push({ type: 'loader', item: items[i], level: getItemLevel(items[i]), status, from: 0 });
} else {
warnOnce('Table', 'Expanded items without children must have "loading" or "error" loading status.');
warnOnce('Table', 'Expanded items without children must not have "finished" loading status.');
}
}

Expand Down
Loading