From 690786f84d0d85272002e1e4076ada3fb015236e Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Thu, 12 Mar 2026 15:48:18 +0000 Subject: [PATCH 1/9] feat(ui): replace Table loading text with skeleton rows Add a skeleton loading state to the BUI Table component that shows the table header with animated skeleton rows instead of plain "Loading..." text. Includes accessibility support via aria-busy and live region announcements. BUCKS-2919 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Jonathan Roebuck --- .changeset/bui-table-skeleton-loading.md | 5 + .../Table/components/Table.test.tsx | 107 ++++++++++++++++++ .../src/components/Table/components/Table.tsx | 85 +++++++------- .../Table/components/TableBodySkeleton.tsx | 59 ++++++++++ packages/ui/src/setupTests.ts | 16 +++ 5 files changed, 228 insertions(+), 44 deletions(-) create mode 100644 .changeset/bui-table-skeleton-loading.md create mode 100644 packages/ui/src/components/Table/components/Table.test.tsx create mode 100644 packages/ui/src/components/Table/components/TableBodySkeleton.tsx create mode 100644 packages/ui/src/setupTests.ts diff --git a/.changeset/bui-table-skeleton-loading.md b/.changeset/bui-table-skeleton-loading.md new file mode 100644 index 0000000000..a2e050e6b5 --- /dev/null +++ b/.changeset/bui-table-skeleton-loading.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Improved the `Table` component loading state to show a skeleton UI with visible headers instead of plain "Loading..." text. The table now renders its full structure during loading, with animated skeleton rows in place of data. The loading state includes proper accessibility support with `aria-busy` on the table and screen reader announcements. diff --git a/packages/ui/src/components/Table/components/Table.test.tsx b/packages/ui/src/components/Table/components/Table.test.tsx new file mode 100644 index 0000000000..355c780413 --- /dev/null +++ b/packages/ui/src/components/Table/components/Table.test.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render, screen } from '@testing-library/react'; +import { Table } from './Table'; +import { CellText } from './CellText'; +import type { ColumnConfig } from '../types'; + +type TestItem = { id: number; name: string; type: string }; + +const testColumns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, +]; + +describe('Table', () => { + describe('loading state', () => { + it('renders a table with header and skeleton rows when loading with no data', async () => { + render( + , + ); + + const table = screen.getByRole('grid'); + expect(table).toBeInTheDocument(); + + // Header columns should be visible + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + + // Should render 5 skeleton rows (plus 1 header row = 6 total) + const rows = screen.getAllByRole('row'); + expect(rows).toHaveLength(6); + + // Table should indicate it's in a loading/stale state via data-stale + // (react-aria-components Table renders this as a data attribute rather + // than aria-busy on the DOM element) + expect(table).toHaveAttribute('data-stale', 'true'); + + // Each skeleton row should contain Skeleton placeholder elements + // (5 rows * 2 columns = 10 skeleton divs) + const skeletonElements = table.querySelectorAll('.bui-Skeleton'); + expect(skeletonElements).toHaveLength(10); + + // Skeleton elements should have varying widths for visual variety + const widths = Array.from(skeletonElements).map( + el => (el as HTMLElement).style.width, + ); + expect(new Set(widths).size).toBeGreaterThan(1); + + // Should NOT render "Loading..." text - skeleton is purely visual + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + + it('renders data rows normally when not loading', async () => { + const data: TestItem[] = [ + { id: 1, name: 'Service A', type: 'service' }, + { id: 2, name: 'Library B', type: 'library' }, + ]; + + render( +
, + ); + + expect(screen.getByText('Service A')).toBeInTheDocument(); + expect(screen.getByText('Library B')).toBeInTheDocument(); + + const table = screen.getByRole('grid'); + // When not loading, data-stale should be "false" + expect(table).toHaveAttribute('data-stale', 'false'); + + // Should not contain skeleton elements + const skeletonElements = table.querySelectorAll('.bui-Skeleton'); + expect(skeletonElements).toHaveLength(0); + }); + }); +}); diff --git a/packages/ui/src/components/Table/components/Table.tsx b/packages/ui/src/components/Table/components/Table.tsx index 523e5bd6c3..af9ff3f40c 100644 --- a/packages/ui/src/components/Table/components/Table.tsx +++ b/packages/ui/src/components/Table/components/Table.tsx @@ -32,6 +32,7 @@ import type { import { useMemo } from 'react'; import { VisuallyHidden } from '../../VisuallyHidden'; import { Flex } from '../../Flex'; +import { TableBodySkeleton } from './TableBodySkeleton'; function isRowRenderFn( rowConfig: RowConfig | RowRenderFn | undefined, @@ -115,13 +116,7 @@ export function Table({ onSelectionChange, } = selection || {}; - if (loading && !data) { - return ( -
- Loading... -
- ); - } + const isInitialLoading = loading && !data; if (error) { return ( @@ -131,11 +126,9 @@ export function Table({ ); } - const liveRegionLabel = useLiveRegionLabel( - pagination, - isStale, - data !== undefined, - ); + const liveRegionLabel = isInitialLoading + ? 'Loading table data.' + : useLiveRegionLabel(pagination, isStale, data !== undefined); const manualColumnSizing = columnConfig.some( col => @@ -165,7 +158,7 @@ export function Table({ sortDescriptor={sort?.descriptor ?? undefined} onSortChange={sort?.onSortChange} disabledKeys={disabledRows} - stale={isStale} + stale={isStale || isInitialLoading} aria-describedby={liveRegionId} > @@ -187,39 +180,43 @@ export function Table({ ) } - {emptyState} : undefined - } - > - {item => { - const itemIndex = data?.indexOf(item) ?? -1; - - if (isRowRenderFn(rowConfig)) { - return rowConfig({ - item, - index: itemIndex, - }); + {isInitialLoading ? ( + + ) : ( + {emptyState} : undefined } + > + {item => { + const itemIndex = data?.indexOf(item) ?? -1; - return ( - rowConfig?.onClick?.(item) - : undefined - } - > - {column => column.cell(item)} - - ); - }} - + if (isRowRenderFn(rowConfig)) { + return rowConfig({ + item, + index: itemIndex, + }); + } + + return ( + rowConfig?.onClick?.(item) + : undefined + } + > + {column => column.cell(item)} + + ); + }} + + )} , )} {pagination.type === 'page' && ( diff --git a/packages/ui/src/components/Table/components/TableBodySkeleton.tsx b/packages/ui/src/components/Table/components/TableBodySkeleton.tsx new file mode 100644 index 0000000000..1623fdc9cf --- /dev/null +++ b/packages/ui/src/components/Table/components/TableBodySkeleton.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TableBody } from './TableBody'; +import { Row } from './Row'; +import { Cell } from './Cell'; +import { Skeleton } from '../../Skeleton'; +import type { ColumnConfig, TableItem } from '../types'; + +const SKELETON_ROW_COUNT = 5; +const SKELETON_WIDTHS = ['75%', '50%', '60%', '45%', '70%']; + +const skeletonItems = Array.from({ length: SKELETON_ROW_COUNT }, (_, i) => ({ + id: `skeleton-${i}`, +})); + +/** @internal */ +export function TableBodySkeleton({ + columns, +}: { + columns: readonly ColumnConfig[]; +}) { + return ( + + {item => { + const rowIndex = Number(item.id.split('-')[1]); + return ( + + {column => ( + + )} + + ); + }} + + ); +} diff --git a/packages/ui/src/setupTests.ts b/packages/ui/src/setupTests.ts new file mode 100644 index 0000000000..5f11bab1c2 --- /dev/null +++ b/packages/ui/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; From 6f56a0c0b05d49c2cf8df9f055e996323dc6bcbe Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Thu, 12 Mar 2026 16:40:29 +0000 Subject: [PATCH 2/9] fix(ui): address review feedback for Table skeleton loading - Add @testing-library/jest-dom and @testing-library/react as explicit devDependencies for @backstage/ui - Disable selection on TableRoot during initial loading to prevent interaction with skeleton rows - Clarify test comment about aria-busy vs data-stale behavior Co-Authored-By: Claude Opus 4.6 Signed-off-by: Jonathan Roebuck --- packages/ui/package.json | 2 ++ .../ui/src/components/Table/components/Table.test.tsx | 4 ++-- packages/ui/src/components/Table/components/Table.tsx | 8 ++++---- yarn.lock | 2 ++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index a1df4bcf8a..ce979d58d2 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -55,6 +55,8 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^16.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/use-sync-external-store": "^0.0.6", diff --git a/packages/ui/src/components/Table/components/Table.test.tsx b/packages/ui/src/components/Table/components/Table.test.tsx index 355c780413..f10d672af4 100644 --- a/packages/ui/src/components/Table/components/Table.test.tsx +++ b/packages/ui/src/components/Table/components/Table.test.tsx @@ -59,8 +59,8 @@ describe('Table', () => { expect(rows).toHaveLength(6); // Table should indicate it's in a loading/stale state via data-stale - // (react-aria-components Table renders this as a data attribute rather - // than aria-busy on the DOM element) + // (react-aria-components' Table does not forward aria-busy to the DOM, + // but the stale prop produces a data-stale attribute for CSS targeting) expect(table).toHaveAttribute('data-stale', 'true'); // Each skeleton row should contain Skeleton placeholder elements diff --git a/packages/ui/src/components/Table/components/Table.tsx b/packages/ui/src/components/Table/components/Table.tsx index af9ff3f40c..e5f519b8e5 100644 --- a/packages/ui/src/components/Table/components/Table.tsx +++ b/packages/ui/src/components/Table/components/Table.tsx @@ -151,10 +151,10 @@ export function Table({ {wrapResizable( Date: Thu, 12 Mar 2026 16:58:28 +0000 Subject: [PATCH 3/9] chore(ui): remove Table tests and test setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed per reviewer feedback — BUI Table relies on Storybook visual tests rather than unit tests. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Jonathan Roebuck --- packages/ui/package.json | 2 - .../Table/components/Table.test.tsx | 107 ------------------ packages/ui/src/setupTests.ts | 16 --- yarn.lock | 2 - 4 files changed, 127 deletions(-) delete mode 100644 packages/ui/src/components/Table/components/Table.test.tsx delete mode 100644 packages/ui/src/setupTests.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index ce979d58d2..a1df4bcf8a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -55,8 +55,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@testing-library/jest-dom": "^6.0.0", - "@testing-library/react": "^16.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/use-sync-external-store": "^0.0.6", diff --git a/packages/ui/src/components/Table/components/Table.test.tsx b/packages/ui/src/components/Table/components/Table.test.tsx deleted file mode 100644 index f10d672af4..0000000000 --- a/packages/ui/src/components/Table/components/Table.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { render, screen } from '@testing-library/react'; -import { Table } from './Table'; -import { CellText } from './CellText'; -import type { ColumnConfig } from '../types'; - -type TestItem = { id: number; name: string; type: string }; - -const testColumns: ColumnConfig[] = [ - { - id: 'name', - label: 'Name', - isRowHeader: true, - cell: item => , - }, - { - id: 'type', - label: 'Type', - cell: item => , - }, -]; - -describe('Table', () => { - describe('loading state', () => { - it('renders a table with header and skeleton rows when loading with no data', async () => { - render( -
, - ); - - const table = screen.getByRole('grid'); - expect(table).toBeInTheDocument(); - - // Header columns should be visible - expect(screen.getByText('Name')).toBeInTheDocument(); - expect(screen.getByText('Type')).toBeInTheDocument(); - - // Should render 5 skeleton rows (plus 1 header row = 6 total) - const rows = screen.getAllByRole('row'); - expect(rows).toHaveLength(6); - - // Table should indicate it's in a loading/stale state via data-stale - // (react-aria-components' Table does not forward aria-busy to the DOM, - // but the stale prop produces a data-stale attribute for CSS targeting) - expect(table).toHaveAttribute('data-stale', 'true'); - - // Each skeleton row should contain Skeleton placeholder elements - // (5 rows * 2 columns = 10 skeleton divs) - const skeletonElements = table.querySelectorAll('.bui-Skeleton'); - expect(skeletonElements).toHaveLength(10); - - // Skeleton elements should have varying widths for visual variety - const widths = Array.from(skeletonElements).map( - el => (el as HTMLElement).style.width, - ); - expect(new Set(widths).size).toBeGreaterThan(1); - - // Should NOT render "Loading..." text - skeleton is purely visual - expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); - }); - - it('renders data rows normally when not loading', async () => { - const data: TestItem[] = [ - { id: 1, name: 'Service A', type: 'service' }, - { id: 2, name: 'Library B', type: 'library' }, - ]; - - render( -
, - ); - - expect(screen.getByText('Service A')).toBeInTheDocument(); - expect(screen.getByText('Library B')).toBeInTheDocument(); - - const table = screen.getByRole('grid'); - // When not loading, data-stale should be "false" - expect(table).toHaveAttribute('data-stale', 'false'); - - // Should not contain skeleton elements - const skeletonElements = table.querySelectorAll('.bui-Skeleton'); - expect(skeletonElements).toHaveLength(0); - }); - }); -}); diff --git a/packages/ui/src/setupTests.ts b/packages/ui/src/setupTests.ts deleted file mode 100644 index 5f11bab1c2..0000000000 --- a/packages/ui/src/setupTests.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2026 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import '@testing-library/jest-dom'; diff --git a/yarn.lock b/yarn.lock index 52709955fb..fbc65dcfdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7652,8 +7652,6 @@ __metadata: "@backstage/version-bridge": "workspace:^" "@remixicon/react": "npm:^4.6.0" "@tanstack/react-table": "npm:^8.21.3" - "@testing-library/jest-dom": "npm:^6.0.0" - "@testing-library/react": "npm:^16.0.0" "@types/react": "npm:^18.0.0" "@types/react-dom": "npm:^18.0.0" "@types/use-sync-external-store": "npm:^0.0.6" From 4bd75da4cf0b2f80ad76bd915c0c5d1db28e6ac4 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Thu, 12 Mar 2026 17:18:18 +0000 Subject: [PATCH 4/9] docs(ui): fix changeset wording for Table loading state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove inaccurate aria-busy claim from changeset — react-aria-components does not forward it to the DOM. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Jonathan Roebuck --- .changeset/bui-table-skeleton-loading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/bui-table-skeleton-loading.md b/.changeset/bui-table-skeleton-loading.md index a2e050e6b5..4a2d8aaea4 100644 --- a/.changeset/bui-table-skeleton-loading.md +++ b/.changeset/bui-table-skeleton-loading.md @@ -2,4 +2,4 @@ '@backstage/ui': patch --- -Improved the `Table` component loading state to show a skeleton UI with visible headers instead of plain "Loading..." text. The table now renders its full structure during loading, with animated skeleton rows in place of data. The loading state includes proper accessibility support with `aria-busy` on the table and screen reader announcements. +Improved the `Table` component loading state to show a skeleton UI with visible headers instead of plain "Loading..." text. The table now renders its full structure during loading, with animated skeleton rows in place of data. The loading state includes accessibility support via a screen reader live-region announcement. From 36d9e1b56e462f08735f505db839a1317d168a97 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Fri, 13 Mar 2026 09:44:05 +0000 Subject: [PATCH 5/9] docs(ui): update changeset with affected component Signed-off-by: Jonathan Roebuck --- .changeset/bui-table-skeleton-loading.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/bui-table-skeleton-loading.md b/.changeset/bui-table-skeleton-loading.md index 4a2d8aaea4..df20393c50 100644 --- a/.changeset/bui-table-skeleton-loading.md +++ b/.changeset/bui-table-skeleton-loading.md @@ -2,4 +2,6 @@ '@backstage/ui': patch --- -Improved the `Table` component loading state to show a skeleton UI with visible headers instead of plain "Loading..." text. The table now renders its full structure during loading, with animated skeleton rows in place of data. The loading state includes accessibility support via a screen reader live-region announcement. +Improved the `Table` component loading state to show a skeleton UI with visible headers instead of plain "Loading..." text. The table now renders its full structure during loading, with animated skeleton rows in place of data. The loading state includes proper accessibility support with `aria-busy` on the table and screen reader announcements. + +Affected components: Table From a688c806bcae348f65f54103d371db629a2d676e Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Fri, 13 Mar 2026 09:57:25 +0000 Subject: [PATCH 6/9] refactor(ui): consolidate live region label logic Move initial loading label into useLiveRegionLabel so all live region logic is in one place. Signed-off-by: Jonathan Roebuck --- .../ui/src/components/Table/components/Table.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/Table/components/Table.tsx b/packages/ui/src/components/Table/components/Table.tsx index e5f519b8e5..6ad1fa96ae 100644 --- a/packages/ui/src/components/Table/components/Table.tsx +++ b/packages/ui/src/components/Table/components/Table.tsx @@ -62,8 +62,13 @@ function useDisabledRows({ function useLiveRegionLabel( pagination: TablePaginationType, isStale: boolean, + isLoading: boolean, hasData: boolean, ): string { + if (isLoading) { + return 'Loading table data.'; + } + if (!hasData || pagination.type === 'none') { return ''; } @@ -126,9 +131,12 @@ export function Table({ ); } - const liveRegionLabel = isInitialLoading - ? 'Loading table data.' - : useLiveRegionLabel(pagination, isStale, data !== undefined); + const liveRegionLabel = useLiveRegionLabel( + pagination, + isStale, + isInitialLoading, + data !== undefined, + ); const manualColumnSizing = columnConfig.some( col => From 3ace9c54db6dca0d0a114b0782bba7cd14a90fe2 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Fri, 13 Mar 2026 09:57:41 +0000 Subject: [PATCH 7/9] feat(ui): add separate loading prop to TableRoot Use spread for conditional selection props. Add a dedicated loading prop and data-loading data attribute to TableRoot, distinct from stale. Both set aria-busy and default to the same opacity, but consumers can now style them independently. Signed-off-by: Jonathan Roebuck --- packages/ui/report.api.md | 4 ++++ packages/ui/src/components/Table/Table.module.css | 3 ++- .../ui/src/components/Table/components/Table.tsx | 15 ++++++++++----- .../src/components/Table/components/TableRoot.tsx | 2 +- packages/ui/src/components/Table/definition.ts | 1 + packages/ui/src/components/Table/types.ts | 1 + 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 834251f088..f2d326dc30 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -2332,6 +2332,9 @@ export const TableDefinition: { readonly stale: { readonly dataAttribute: true; }; + readonly loading: { + readonly dataAttribute: true; + }; }; }; @@ -2449,6 +2452,7 @@ export const TableRoot: (props: TableRootProps) => JSX_2.Element; // @public (undocumented) export type TableRootOwnProps = { stale?: boolean; + loading?: boolean; }; // @public (undocumented) diff --git a/packages/ui/src/components/Table/Table.module.css b/packages/ui/src/components/Table/Table.module.css index 55cf235e56..472e0733ac 100644 --- a/packages/ui/src/components/Table/Table.module.css +++ b/packages/ui/src/components/Table/Table.module.css @@ -24,7 +24,8 @@ table-layout: fixed; transition: opacity 0.2s ease-in-out; - &[data-stale='true'] { + &[data-stale='true'], + &[data-loading='true'] { opacity: 0.6; } } diff --git a/packages/ui/src/components/Table/components/Table.tsx b/packages/ui/src/components/Table/components/Table.tsx index 6ad1fa96ae..96e7cfe166 100644 --- a/packages/ui/src/components/Table/components/Table.tsx +++ b/packages/ui/src/components/Table/components/Table.tsx @@ -159,14 +159,19 @@ export function Table({ {wrapResizable( diff --git a/packages/ui/src/components/Table/components/TableRoot.tsx b/packages/ui/src/components/Table/components/TableRoot.tsx index af96d1809e..223e9d648b 100644 --- a/packages/ui/src/components/Table/components/TableRoot.tsx +++ b/packages/ui/src/components/Table/components/TableRoot.tsx @@ -30,7 +30,7 @@ export const TableRoot = (props: TableRootProps) => { diff --git a/packages/ui/src/components/Table/definition.ts b/packages/ui/src/components/Table/definition.ts index eb41617b93..1c3953a19a 100644 --- a/packages/ui/src/components/Table/definition.ts +++ b/packages/ui/src/components/Table/definition.ts @@ -38,6 +38,7 @@ export const TableDefinition = defineComponent()({ }, propDefs: { stale: { dataAttribute: true }, + loading: { dataAttribute: true }, }, }); diff --git a/packages/ui/src/components/Table/types.ts b/packages/ui/src/components/Table/types.ts index 099a855d33..0736afa05b 100644 --- a/packages/ui/src/components/Table/types.ts +++ b/packages/ui/src/components/Table/types.ts @@ -42,6 +42,7 @@ export interface SortState { /** @public */ export type TableRootOwnProps = { stale?: boolean; + loading?: boolean; }; /** @public */ From b838cc97b0f42b98d05b12e9eaa72be7cca3af92 Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Fri, 13 Mar 2026 11:52:08 +0000 Subject: [PATCH 8/9] docs(ui): add changeset and docs for TableRoot loading prop Signed-off-by: Jonathan Roebuck --- .changeset/bui-table-root-loading-prop.md | 7 +++++++ docs-ui/src/app/components/table/props-definition.tsx | 11 +++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .changeset/bui-table-root-loading-prop.md diff --git a/.changeset/bui-table-root-loading-prop.md b/.changeset/bui-table-root-loading-prop.md new file mode 100644 index 0000000000..b5728154cc --- /dev/null +++ b/.changeset/bui-table-root-loading-prop.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Added a `loading` prop and `data-loading` data attribute to `TableRoot`, allowing consumers to distinguish between stale data and initial loading states. Both `stale` and `loading` set `aria-busy` on the table. + +Affected components: Table diff --git a/docs-ui/src/app/components/table/props-definition.tsx b/docs-ui/src/app/components/table/props-definition.tsx index abfdd45549..6d35cb3350 100644 --- a/docs-ui/src/app/components/table/props-definition.tsx +++ b/docs-ui/src/app/components/table/props-definition.tsx @@ -430,6 +430,17 @@ export const tableRootPropDefs: Record = { ), }, + loading: { + type: 'boolean', + default: 'false', + description: ( + <> + Whether the table is in a loading state (e.g., initial data fetch). Adds{' '} + aria-busy attribute and data-loading data + attribute for styling. + + ), + }, }; export const columnPropDefs: Record = { From 5d4fc274a2ac7e5029669ad066a0fbc59dced21b Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Fri, 13 Mar 2026 16:27:26 +0000 Subject: [PATCH 9/9] Update .changeset/bui-table-root-loading-prop.md Co-authored-by: Johan Persson Signed-off-by: Jonathan Roebuck --- .changeset/bui-table-root-loading-prop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/bui-table-root-loading-prop.md b/.changeset/bui-table-root-loading-prop.md index b5728154cc..7c3f453c82 100644 --- a/.changeset/bui-table-root-loading-prop.md +++ b/.changeset/bui-table-root-loading-prop.md @@ -4,4 +4,4 @@ Added a `loading` prop and `data-loading` data attribute to `TableRoot`, allowing consumers to distinguish between stale data and initial loading states. Both `stale` and `loading` set `aria-busy` on the table. -Affected components: Table +Affected components: TableRoot