feat: add composable low-level table components (TableRoot, TableHead, TableRow, TableHeaderCell, TableBodyCell) - #4977
gethinwebster wants to merge 20 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4977 +/- ##
========================================
Coverage 97.68% 97.69%
========================================
Files 965 986 +21
Lines 31478 31645 +167
Branches 11639 11684 +45
========================================
+ Hits 30748 30914 +166
- Misses 684 724 +40
+ Partials 46 7 -39 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| ); | ||
|
|
||
| return ( | ||
| <TableContextProvider value={defaultTableContext}> |
There was a problem hiding this comment.
Why do we need these providers? The context reads should work just fine w/o these.
There was a problem hiding this comment.
as above, to reset in case of nesting
There was a problem hiding this comment.
The Table component is guaranteed to render its context and rows - so even if nested inside another table or table row - these contexts resets should never be necessary.
…ntext-provider resets
…s with data-awsui-
…cell-extract # Conflicts: # src/table/body-cell/td-element.tsx
…ePage for selection-edge-cases
…ableBodyCell - Rename TableCell -> TableBodyCell; it now renders row headers too via isRowHeader (th scope="row"), so TableHeaderCell is column-headers only (drops its scope prop). - Remove TableHeaderRow; the header row is TableRow variant="header", rendered through one unified TableRow path that shares the base .row / .row-grid box and CSS (header adds only a background). - Drop the head/body section context: variant (default|selected|shaded| header) is the single row discriminant, stamped dynamically as a data-awsui-variant-* hook (default carries none; new variants need no change here). - ariaRowcount no longer travels through table context; header rows take an explicit ariaRowindex like body rows, and gain aria-*/positionStyle. - Remove the TableHeaderRow test-util wrapper (not a component). - Migrate dev pages and unit tests; regenerate documenter + test-utils snapshots.
Header rows no longer need a distinct row variant. A header row's visual identity (the shaded background) lives on TableHeaderCell, and its structural role is conveyed by placement inside TableHead, so TableRow variant narrows to default | selected | shaded. This lets two more things go: - The row-level header background only backed the grid underfill strip (header cells paint themselves); with it removed, an underfill header shows cells shaded to their tracks, consistent with body rows. - grid-auto-rows only imposed a minimum row height on all-short rows; dropping it makes grid rows content-sized, matching auto mode and the existing Table, neither of which has a row-height floor.
…n tests Replace the hand-rolled ResizeObserver in TableRoot with two useResizeObserver calls (the scroller and the table it wraps), matching how the rest of the codebase observes elements and getting a synchronous initial measure for free. The re-measure effect narrows to gridTemplateColumns — the one overflow transition an observer misses (grid tracks overflow without either observed box resizing); auto-layout content growth resizes the table box and is caught by the child observer. The unit overflow-region test is rewritten to drive the transition through a column-template change (no ResizeObserver mock, which the shared hook's entry conversion would otherwise require). A new integ suite covers the two paths jsdom can't: keyboard-scrollability of the focusable region, a viewport resize starting/stopping the overflow (scroller observer), and auto-layout content growth starting it (table-box observer).
… data-attr RowVariantContext's only runtime effect was the atomic body cell's shaded-background class. Selection already paints from the row's data-awsui-variant-selected attribute via a `> .cell` rule, and the shaded *border* already did too — only the shaded *background* went through the context. Move it to a matching `[data-awsui-variant-shaded] > .cell` rule and the context has no consumers, so delete it along with the per-row provider and both `value="default"` resets (TableRoot and the existing Table). The existing Table keeps its TableContextProvider reset (defaultTableContext): its cells are the shared substrate and read the ambient column layout, so without it a Table nested in a grid-layout atomic cell would render grid roles/classes. That reset is load-bearing; the RowVariant one was not.
The variant tests asserted that atomic cells do NOT carry the existing Table's per-cell selection/shading classes — testing the absence of a class-based mechanism the atomic never used now that selection and shading both paint from the row's data-awsui-variant-* attribute. Drop those per-cell class-absence loops and the unused helper; each variant test now asserts the real contract (the correct data-awsui-variant-* hook on the <tr>, mutual exclusivity, and no aria-selected). Also delete two tests that guarded removed/never-built behavior: the stray-cell test that existed only to check the deleted RowVariantContext default, and the grid roving-tabindex negative (a deferred feature the atomic never implemented).
| * Renders the cell as a row header (`<th scope="row">`) instead of a data cell (`<td>`). A row header | ||
| * keeps the data-cell styling; use it for the cell that names its row. Defaults to `false`. | ||
| */ | ||
| isRowHeader?: boolean; |
There was a problem hiding this comment.
What about making it more explicit such as:
tag: "td" | "th"; // td by default
There was a problem hiding this comment.
isRowHeader also sets the role/scope attributes, so I think it's better to have an abstracted name than more explicit
| ) => { | ||
| const { columnLayout } = useTableContext(); | ||
| const variant = useRowVariant(); | ||
| const isVisualRefresh = useVisualRefresh(); |
There was a problem hiding this comment.
Should this be any special treatment for one-theme as well?
There was a problem hiding this comment.
the useVisualRefresh hook returns true for onetheme too
|
|
||
| import styles from './styles.css.js'; | ||
|
|
||
| export interface InternalTableBodyProps extends TableBodyProps, InternalBaseComponentProps {} |
There was a problem hiding this comment.
Do you need to export this interface?
| ref | ||
| ) => { | ||
| const { columnLayout } = useTableContext(); | ||
| const isVisualRefresh = useVisualRefresh(); |
There was a problem hiding this comment.
The same question about one-theme here
| return `${size}px`; | ||
| } | ||
| const min = `${clamp(column.minWidth) ?? 0}px`; | ||
| const flex = typeof column.size === 'object' ? clamp(column.size.flex) : undefined; |
There was a problem hiding this comment.
[nit]:
| const flex = typeof column.size === 'object' ? clamp(column.size.flex) : undefined; | |
| const flex = !!column.size?.flex ? clamp(column.size.flex) : undefined; |
| * * `{ type: 'auto' }` - Renders a standard HTML table whose columns size to their content. No | ||
| * column configuration is required. | ||
| * * `{ type: 'grid'; columns }` - Renders a CSS grid and applies each column's `size`, `minWidth`, | ||
| * and `maxWidth`. Provide one `columns` entry per column, in display order; cells bind to columns |
There was a problem hiding this comment.
Provide one
columnsentry per column, in display order
Can we reorder columns in the future and how would this affect the reordering?
There was a problem hiding this comment.
yes, and in that case the array would need to be reordered as, but so would the rendering of s in the body, so I think this is not unexpected behavior
| * * `{ type: 'grid'; columns }` - Renders a CSS grid and applies each column's `size`, `minWidth`, | ||
| * and `maxWidth`. Provide one `columns` entry per column, in display order; cells bind to columns | ||
| * by position. Virtualization requires this layout. | ||
| * * `size` (number | { flex: number }) - A number sets a fixed pixel width; `{ flex }` gives the |
There was a problem hiding this comment.
Nit: can we make it clear that size, minWidth, and maxWidth are per column before listing them?
There was a problem hiding this comment.
do you have a wording suggestion? for me "applies each column's size, minWidth and maxWidth" is already relatively clear that they are per-column
There was a problem hiding this comment.
I think what was missing is that It was hard to know columns is an array. We can rephrase Provide one 'columns' entry per column into 'columns' is an array with one entry per column.
There was a problem hiding this comment.
makes sense, have updated
| * * `size` (number | { flex: number }) - A number sets a fixed pixel width; `{ flex }` gives the | ||
| * column a weight that shares the remaining space in proportion. Omit it for a flexible column | ||
| * with the default weight of 1. | ||
| * * `minWidth` (number) - The minimum width in pixels, for a flexible column. |
There was a problem hiding this comment.
agree, we should document this, updating

What this adds
A set of low-level, composable table components —
TableRoot,TableHead,TableBody,TableRow,TableHeaderCell, andTableBodyCell. They let a consumer assemble a table from primitives, as a lower-level alternative to the existing high-levelTablecomponent.Composition model
TableRowcomponent. The column-header row is aTableRow variant="header"placed insideTableHead; data rows go inTableBody.variant(default|selected|shaded|header) is the only row discriminant, so there is no separate header-row component.TableHeaderCellis the column header (<th scope="col">);TableBodyCellis the data cell (<td>). A row header is a<th scope="row">that visually matches a data cell, so it is aTableBodyCell isRowHeaderrather than a header cell.TableRootrenders no rows itself.How they are built
Rather than reimplementing table cell and row markup and styling from scratch — which would risk drifting visually from the existing
Table— these components are built on the same internal building blocks the existingTablealready uses for its own cells and rows. The existingTableis refactored to compose those same building blocks.The result is a single shared implementation of the cell/row box model and its visual states (selection, shading, dividers, spacing), instead of two implementations that could diverge over time. A row's visual state is carried on the
<tr>as adata-awsui-variant-*hook that the shared cell styles read; header rows reuse the same base row box and only add the header background.Effect on the existing Table
The existing
Tablerenders exactly as before. The refactor only changes which internal pieces it composes, not the markup or styles it produces. The new components' additional styling is keyed on markers the existingTablenever sets, so the shared styles are reused (not duplicated) and stay inert for the existingTable.Verification
Tableis confirmed visually unchanged across selection, striping, sticky columns, inline editing, loading/empty, footer, and grouped-header states.Tableoutput — including selection-control alignment and the absence of any content shift when a row's selection is toggled.Notes for reviewers