Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0aa1793
refactor(table): extract internal TableCell substrate from TableTdEle…
gethinwebster Sep 4, 2026
5b211c4
refactor(table): extract internal TableHeaderCell substrate from Tabl…
gethinwebster Sep 4, 2026
a9dcf4e
feat(table): extract internal TableRow element from inline <tr> rende…
gethinwebster Sep 4, 2026
16e3491
refactor(table): extract internal TableBody/TableHead substrates (Inc3b)
gethinwebster Sep 7, 2026
aeefcb0
feat(table): add public table atomic components unified onto carved s…
gethinwebster Sep 7, 2026
6fdb7ea
feat(table): add table-root demo pages (Inc4b)
gethinwebster Sep 7, 2026
164df9c
test(table): unit tests for public table atomic components (Inc5)
gethinwebster Sep 7, 2026
435c3f6
fix(table): keep selected disablePaddings control cells centred (Inc6)
gethinwebster Sep 7, 2026
cb9f3fa
refactor(table): trim public API (drop event props, nativeAttributes …
gethinwebster Sep 7, 2026
74e09e6
fix(table): standalone atomic parity -- is-visual-refresh first-col p…
gethinwebster Sep 7, 2026
d3025c6
test(table): classic-Table reference demo pages for atomic VR (table-…
gethinwebster Sep 7, 2026
2c95cd0
test(table): borderless classic reference pages for atomic VR
gethinwebster Sep 7, 2026
768eba8
fix(table): atomic header + edge-row height parity (a1/a2)
gethinwebster Sep 7, 2026
0ac7a36
refactor(table): route internal TableRow row events via nativeAttribu…
gethinwebster Sep 7, 2026
871cd37
fix(table): atomic parity -- control column, radio centering, striped…
gethinwebster Sep 7, 2026
d137819
fix(table): don't clip focus ring in disablePaddings control cells
gethinwebster Sep 7, 2026
947259a
fix(table): atomic header-cell inline padding to align header text wi…
gethinwebster Sep 7, 2026
4c649a8
fix(table): merge consecutive-selected outline + enclose control cell…
gethinwebster Sep 7, 2026
fbf8135
test(table): faithful classic twin pages (match atomic feature set)
gethinwebster Sep 7, 2026
78b8658
fix(table): selection/single-selection demo column widths to match cl…
gethinwebster Sep 7, 2026
433d877
test(table): faithful loading-and-empty twin
gethinwebster Sep 8, 2026
325ffd4
fix(table): suppress header divider on control column
gethinwebster Sep 8, 2026
090d4ef
fix(table): center control cell on all rows, darker striped divider v…
gethinwebster Sep 8, 2026
81bbc2d
fix(table-atomic): match classic on selection outline, seam, control-…
gethinwebster Sep 8, 2026
ff8428b
fix(table-atomic): constant-height selected rows (zero shift on toggle)
gethinwebster Sep 8, 2026
1fbd133
fix(table-atomic): repaint selection ring as layout-neutral overlay (…
gethinwebster Sep 8, 2026
6001ea0
chore(table-atomic): trim comments, drop dead class, de-ambiguate 'cl…
gethinwebster Sep 8, 2026
a54a224
chore(table-atomic): remove table-root-classic VR-oracle demo pages
gethinwebster Sep 8, 2026
7fc7d15
test(table-atomic): regenerate test-utils/documenter snapshots + fix …
gethinwebster Sep 8, 2026
434718a
refactor(table-atomic): manually type Style props instead of Pick<CSS…
gethinwebster Sep 9, 2026
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
7 changes: 7 additions & 0 deletions build-tools/utils/pluralize.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ const pluralizationMap = {
StatusIndicator: 'StatusIndicators',
Steps: 'Steps',
Table: 'Tables',
TableBody: 'TableBodies',
TableCell: 'TableCells',
TableHead: 'TableHeads',
TableHeaderCell: 'TableHeaderCells',
TableHeaderRow: 'TableHeaderRows',
TableRoot: 'TableRoots',
TableRow: 'TableRows',
Tabs: 'Tabs',
TagEditor: 'TagEditors',
TextContent: 'TextContents',
Expand Down
67 changes: 67 additions & 0 deletions pages/table-root/common.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import {
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableHeaderRow,
TableRootProps,
TableRow,
} from '~components';

export interface Item {
id: string;
name: string;
type: string;
size: string;
status: string;
}

export const makeItems = (n: number): Item[] =>
Array.from({ length: n }, (_, index) => ({
id: `resource-${index}`,
name: `Resource ${index}`,
type: index % 3 === 0 ? 'Compute' : index % 3 === 1 ? 'Storage' : 'Network',
size: `${(index % 8) + 1} GiB`,
status: index % 2 === 0 ? 'Available' : 'Pending',
}));

// A 4-column grid layout (no control column): Name fixed, Type/Size flexible-with-min, Status flexible.
export const DATA_COLUMNS: ReadonlyArray<TableRootProps.ColumnDefinition> = [
{ size: 220 },
{ minWidth: 140 },
{ minWidth: 120 },
{},
];

// Renders the standard header row for the shared 4-column item shape: a TableRow of TableHeaderCells.
export function DataHeader() {
return (
<TableHead>
<TableHeaderRow>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Type</TableHeaderCell>
<TableHeaderCell>Size</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
</TableHeaderRow>
</TableHead>
);
}

export function DataBody({ items }: { items: Item[] }) {
return (
<TableBody>
{items.map(item => (
<TableRow key={item.id}>
<TableCell>{item.name}</TableCell>
<TableCell>{item.type}</TableCell>
<TableCell>{item.size}</TableCell>
<TableCell>{item.status}</TableCell>
</TableRow>
))}
</TableBody>
);
}
75 changes: 75 additions & 0 deletions pages/table-root/loading-and-empty.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from 'react';

import Box from '~components/box';
import Header from '~components/header';
import SegmentedControl from '~components/segmented-control';
import SpaceBetween from '~components/space-between';
import StatusIndicator from '~components/status-indicator';
import TableBody from '~components/table-body';
import TableRoot from '~components/table-root';
import TableRow from '~components/table-row';

import { DataBody, DataHeader, makeItems } from './common';

type State = 'loaded' | 'loading' | 'empty';

const COLUMN_COUNT = 4;

// Loading and empty states are composed by the consumer. In auto layout the table is a native
// `<table>`, so a single full-width status row is a plain `<td colSpan>` the consumer renders inside
// a `TableRow`. The consumer owns the data and the state; the status content is wrapped in a `Box`
// so its centered padding comes from spacing design tokens, not a standard data `TableCell`.
export default function TableLoadingEmptyPage() {
const [state, setState] = useState<State>('loaded');
const items = state === 'loaded' ? makeItems(20) : [];

return (
<Box padding="l">
<SpaceBetween size="l">
<Box variant="h1">Table atomics — loading & empty states</Box>

<SegmentedControl
selectedId={state}
onChange={event => setState(event.detail.selectedId as State)}
label="Data state"
options={[
{ id: 'loaded', text: 'Loaded' },
{ id: 'loading', text: 'Loading' },
{ id: 'empty', text: 'Empty' },
]}
/>

<SpaceBetween size="s">
<Header counter={`(${items.length})`}>Resources</Header>
<TableRoot ariaLabel="Resources">
<DataHeader />
{state === 'loaded' ? (
<DataBody items={items} />
) : (
<TableBody>
<TableRow>
<td colSpan={COLUMN_COUNT}>
<Box padding="m" textAlign="center" color="inherit">
{state === 'loading' ? (
<StatusIndicator type="loading">Loading resources</StatusIndicator>
) : (
<SpaceBetween size="xxs">
<b>No resources</b>
<Box variant="p" color="inherit">
No resources to display.
</Box>
</SpaceBetween>
)}
</Box>
</td>
</TableRow>
</TableBody>
)}
</TableRoot>
</SpaceBetween>
</SpaceBetween>
</Box>
);
}
119 changes: 119 additions & 0 deletions pages/table-root/selection.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useMemo, useState } from 'react';

import Box from '~components/box';
import Checkbox from '~components/checkbox';
import Header from '~components/header';
import Icon from '~components/icon';
import SpaceBetween from '~components/space-between';
import TableBody from '~components/table-body';
import TableCell from '~components/table-cell';
import TableHead from '~components/table-head';
import TableHeaderCell from '~components/table-header-cell';
import TableHeaderRow from '~components/table-header-row';
import TableRoot, { TableRootProps } from '~components/table-root';
import TableRow from '~components/table-row';

import { Item, makeItems } from './common';

import styles from './styles.scss';

// A selectable + sortable table (grid layout). Selection and sorting are composed
// by the consumer — the atomic parts contribute `variant='selected'` (row surface + aria-selected)
// and `ariaSort` (the header semantic). The control column uses `disablePaddings` cells and a
// centered checkbox to match the classic Table selection column; the name column flexes.
// Selection control column is a fixed 40px; the Name and Status columns share the remaining width
// with proportional flex weights (~53:47), reproducing classic Table's balanced auto-layout split at
// the demo viewport. (Flexing Name to fill and pinning Status to a fixed width would shove Status to
// the far right with a large gap, unlike classic.)
const COLUMNS: ReadonlyArray<TableRootProps.ColumnDefinition> = [
{ size: 40 },
{ size: { flex: 53 } },
{ size: { flex: 47 } },
];
const ITEM_COUNT = 10;

type SortDirection = 'ascending' | 'descending';

export default function TableSelectionPage() {
const allItems = makeItems(ITEM_COUNT);
const [selectedIds, setSelectedIds] = useState<ReadonlySet<string>>(new Set([allItems[1].id, allItems[2].id]));
const [direction, setDirection] = useState<SortDirection>('ascending');

const items = useMemo(() => {
const sorted = [...allItems].sort((a, b) => a.name.localeCompare(b.name));
return direction === 'ascending' ? sorted : sorted.reverse();
}, [allItems, direction]);

const allSelected = items.length > 0 && items.every(item => selectedIds.has(item.id));
const someSelected = items.some(item => selectedIds.has(item.id));
const toggleAll = () => setSelectedIds(allSelected ? new Set() : new Set(items.map(item => item.id)));
const toggleRow = (id: string) =>
setSelectedIds(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
const toggleSort = () => setDirection(prev => (prev === 'ascending' ? 'descending' : 'ascending'));

return (
<Box padding="l">
<SpaceBetween size="l">
<Box variant="h1">Table atomics — selectable + sortable (grid layout)</Box>

<SpaceBetween size="s">
<Header counter={`(${selectedIds.size}/${items.length})`}>Resources</Header>
<TableRoot columnLayout={{ type: 'grid', columns: COLUMNS }} ariaLabel="Resources">
<TableHead>
<TableHeaderRow>
<TableHeaderCell disablePaddings={true}>
<div className={styles['selection-cell']}>
<Checkbox
checked={allSelected}
indeterminate={someSelected && !allSelected}
onChange={toggleAll}
ariaLabel="Select all resources"
/>
</div>
</TableHeaderCell>
<TableHeaderCell ariaSort={direction}>
<button type="button" className={styles['sort-button']} onClick={toggleSort}>
Name
<Icon name={direction === 'ascending' ? 'caret-up-filled' : 'caret-down-filled'} />
</button>
</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
</TableHeaderRow>
</TableHead>
<TableBody>
{items.map((item: Item) => (
<TableRow
key={item.id}
variant={selectedIds.has(item.id) ? 'selected' : 'default'}
ariaSelected={selectedIds.has(item.id)}
>
<TableCell disablePaddings={true}>
<div className={styles['selection-cell']}>
<Checkbox
checked={selectedIds.has(item.id)}
onChange={() => toggleRow(item.id)}
ariaLabel={`Select ${item.name}`}
/>
</div>
</TableCell>
<TableCell>{item.name}</TableCell>
<TableCell>{item.status}</TableCell>
</TableRow>
))}
</TableBody>
</TableRoot>
</SpaceBetween>
</SpaceBetween>
</Box>
);
}
31 changes: 31 additions & 0 deletions pages/table-root/simple.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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 Header from '~components/header';
import SpaceBetween from '~components/space-between';
import TableRoot from '~components/table-root';

import { DataBody, DataHeader, makeItems } from './common';

// A minimal read-only table in auto layout. `columnLayout` is omitted, so it
// defaults to `{ type: 'auto' }` — columns size to their content and the count comes from the cells.
export default function TableSimplePage() {
const items = makeItems(8);
return (
<Box padding="l">
<SpaceBetween size="l">
<Box variant="h1">Table atomics — simple (auto layout)</Box>

<SpaceBetween size="s">
<Header counter={`(${items.length})`}>Resources</Header>
<TableRoot ariaLabel="Resources">
<DataHeader />
<DataBody items={items} />
</TableRoot>
</SpaceBetween>
</SpaceBetween>
</Box>
);
}
Loading
Loading