-
Notifications
You must be signed in to change notification settings - Fork 240
feat: Per-column table skeleton placeholders via renderCell #4879
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
base: main
Are you sure you want to change the base?
Changes from all commits
ca8e568
8a7ebb1
c1cfd06
a0cb1c8
4b5464a
78564fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| import React from 'react'; | ||
|
|
||
| import Box from '~components/box'; | ||
| import Button from '~components/button'; | ||
| import Header from '~components/header'; | ||
| import Skeleton from '~components/skeleton'; | ||
| import SpaceBetween from '~components/space-between'; | ||
| import StatusIndicator from '~components/status-indicator'; | ||
| import Table, { TableProps } from '~components/table'; | ||
|
|
||
| interface Item { | ||
| name: string; | ||
| summary: string; | ||
| detail: string; | ||
| status: 'available' | 'error'; | ||
| } | ||
|
|
||
| // Columns whose settled content is not a single line of text: a two-line cell, | ||
| // a status indicator, and an actions button. A single one-line skeleton bar | ||
| // mismatches these and causes a layout jump when data lands. | ||
| const columnDefinitions: TableProps.ColumnDefinition<Item>[] = [ | ||
| { id: 'name', header: 'Name', cell: item => item.name }, | ||
| { | ||
| id: 'description', | ||
| header: 'Description', | ||
| cell: item => ( | ||
| <SpaceBetween size="xxs"> | ||
| <Box>{item.summary}</Box> | ||
| <Box color="text-body-secondary" fontSize="body-s"> | ||
| {item.detail} | ||
| </Box> | ||
| </SpaceBetween> | ||
| ), | ||
| }, | ||
| { | ||
| id: 'status', | ||
| header: 'Status', | ||
| cell: item => <StatusIndicator type={item.status === 'error' ? 'error' : 'success'}>{item.status}</StatusIndicator>, | ||
| }, | ||
| { id: 'actions', header: '', cell: () => <Button>Edit</Button> }, | ||
| ]; | ||
|
|
||
| // A single central render function keyed on the column definition. Return | ||
| // `undefined` to fall back to the default single-line skeleton (here, the Name column). | ||
| const renderCell: NonNullable<TableProps.SkeletonConfig<Item>['renderCell']> = column => { | ||
| switch (column.id) { | ||
| case 'description': | ||
| return ( | ||
| <SpaceBetween size="xxs"> | ||
| <Skeleton variant="text-body-m" width="90%" /> | ||
| <Skeleton variant="text-body-s" width="60%" /> | ||
| </SpaceBetween> | ||
| ); | ||
| case 'status': | ||
| return <Skeleton variant="text-body-m" width="80px" />; | ||
| case 'actions': | ||
| return <Skeleton variant="text-body-m" display="inline-block" width="64px" height="2rem" />; | ||
| default: | ||
| return undefined; | ||
| } | ||
| }; | ||
|
|
||
| export default function SkeletonRenderCellPage() { | ||
| return ( | ||
| <Box padding="l"> | ||
| <SpaceBetween size="xl"> | ||
| <h1>Table skeleton — per-column renderCell</h1> | ||
|
|
||
| <Table | ||
| items={[]} | ||
| loading={true} | ||
| loadingText="Loading resources" | ||
| columnDefinitions={columnDefinitions} | ||
| skeleton={{ totalRows: 5 }} | ||
| header={<Header>Default single-line skeleton</Header>} | ||
| /> | ||
|
|
||
| <Table | ||
| items={[]} | ||
| loading={true} | ||
| loadingText="Loading resources" | ||
| columnDefinitions={columnDefinitions} | ||
| skeleton={{ totalRows: 5, renderCell }} | ||
| header={<Header>Column-shaped skeleton (renderCell)</Header>} | ||
| /> | ||
| </SpaceBetween> | ||
| </Box> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,8 +72,11 @@ export interface TableProps<T = any> extends BaseComponentProps { | |
| * - `minAutoRows` (number) - Sets the minimum number of skeleton rows rendered when `totalRows` is set to `'auto'`. | ||
| * Defaults to 1. Useful for tables rendered off-screen, where the calculated available height would | ||
| * otherwise yield a single row. | ||
| * - `renderCell` ((column) => ReactNode) - Renders a custom skeleton placeholder per column, for cells whose | ||
| * final content is not a single line of text (for example, multi-line cells, status indicators, or actions). | ||
| * Return `undefined` for a column to fall back to the default single-line skeleton. | ||
| */ | ||
| skeleton?: TableProps.SkeletonConfig; | ||
| skeleton?: TableProps.SkeletonConfig<T>; | ||
|
|
||
| /** | ||
| * Specifies a property that uniquely identifies an individual item. | ||
|
|
@@ -775,19 +778,33 @@ export namespace TableProps { | |
| item: T; | ||
| } | ||
|
|
||
| export interface FixedSkeletonConfig { | ||
| interface BaseSkeletonConfig<T> { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaving as-is intentionally. The declaration-emit concern doesn't apply here: |
||
| /** | ||
| * Renders a custom skeleton placeholder for each cell of the given column while data is loading. | ||
| * Use for columns whose final content is not a single line of text, so the placeholder matches the | ||
| * settled cell shape and the load-to-settle transition stays stable. Compose the returned content | ||
| * from the `Skeleton` component. Return `undefined` for a column to use the default single-line | ||
| * skeleton; return `null` to render an empty placeholder for that column. | ||
| * | ||
| * The returned content is rendered inside an `aria-hidden` row, so it is not announced to screen | ||
| * readers; do not render focusable or interactive elements. | ||
| */ | ||
| renderCell?: (column: TableProps.ColumnDefinition<T>) => React.ReactNode; | ||
| } | ||
|
|
||
| export interface FixedSkeletonConfig<T = any> extends BaseSkeletonConfig<T> { | ||
| totalRows: number; | ||
| maxAutoRows?: never; | ||
| minAutoRows?: never; | ||
| } | ||
|
|
||
| export interface AutoSkeletonConfig { | ||
| export interface AutoSkeletonConfig<T = any> extends BaseSkeletonConfig<T> { | ||
| totalRows: 'auto'; | ||
| maxAutoRows?: number; | ||
| minAutoRows?: number; | ||
| } | ||
|
|
||
| export type SkeletonConfig = FixedSkeletonConfig | AutoSkeletonConfig; | ||
| export type SkeletonConfig<T = any> = FixedSkeletonConfig<T> | AutoSkeletonConfig<T>; | ||
| } | ||
|
|
||
| export type TableRow<T> = TableDataRow<T> | TableLoaderRow<T>; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add a similar test for automatic rows too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've taken another look at what tests there were, and updated to make them a bit more meaningful in terms of combinations they test.