Skip to content

feat: add composable low-level table components (TableRoot, TableHead, TableRow, TableHeaderCell, TableBodyCell) - #4977

Open
gethinwebster wants to merge 20 commits into
mainfrom
dev-v3-gethinw-table-cell-extract
Open

gethinwebster wants to merge 20 commits into
mainfrom
dev-v3-gethinw-table-cell-extract

Conversation

@gethinwebster

@gethinwebster gethinwebster commented Sep 7, 2026

Copy link
Copy Markdown
Member

What this adds

A set of low-level, composable table components — TableRoot, TableHead, TableBody, TableRow, TableHeaderCell, and TableBodyCell. They let a consumer assemble a table from primitives, as a lower-level alternative to the existing high-level Table component.

Composition model

  • Rows are a single TableRow component. The column-header row is a TableRow variant="header" placed inside TableHead; data rows go in TableBody. variant (default | selected | shaded | header) is the only row discriminant, so there is no separate header-row component.
  • Cells mirror the two HTML cell elements. TableHeaderCell is the column header (<th scope="col">); TableBodyCell is the data cell (<td>). A row header is a <th scope="row"> that visually matches a data cell, so it is a TableBodyCell isRowHeader rather than a header cell.
  • Grid vs. auto layout, the shared column template, sorting, and selection are composed by the consumer; TableRoot renders 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 existing Table already uses for its own cells and rows. The existing Table is 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 a data-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 Table renders 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 existing Table never sets, so the shared styles are reused (not duplicated) and stay inert for the existing Table.

Verification

  • The existing Table is confirmed visually unchanged across selection, striping, sticky columns, inline editing, loading/empty, footer, and grouped-header states.
  • The new components are checked for visual and behavioural parity with the equivalent existing-Table output — including selection-control alignment and the absence of any content shift when a row's selection is toggled.
  • Full build, linting, unit tests, and the generated API / test-utils / documentation artifacts all pass.

Notes for reviewers

  • This builds on the recently merged change that deduplicated table cell spacing and moved first/last-row edge borders to CSS, so shared spacing lives in one place.

@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.43182% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 97.69%. Comparing base (81007d5) to head (b66e2b8).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/table-root/internal.tsx 95.23% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread src/table/body-cell/styles.scss Outdated
Comment thread src/table/body-cell/td-element.tsx Outdated
Comment thread src/table/internal.tsx Outdated
);

return (
<TableContextProvider value={defaultTableContext}>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need these providers? The context reads should work just fine w/o these.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above, to reset in case of nesting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pages/table-root/selection.page.tsx
Comment thread pages/table-root/selection-edge-cases.page.tsx Outdated
Comment thread pages/table-root/selection.page.tsx
Comment thread pages/table-root/sorting.page.tsx
Comment thread pages/table-root/striped-rows.page.tsx Outdated
…cell-extract

# Conflicts:
#	src/table/body-cell/td-element.tsx
…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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about making it more explicit such as:

tag: "td" | "th"; // td by default

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be any special treatment for one-theme as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the useVisualRefresh hook returns true for onetheme too

Comment thread src/table-body/internal.tsx Outdated

import styles from './styles.css.js';

export interface InternalTableBodyProps extends TableBodyProps, InternalBaseComponentProps {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to export this interface?

ref
) => {
const { columnLayout } = useTableContext();
const isVisualRefresh = useVisualRefresh();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same question about one-theme here

Comment thread src/table-root/use-table-root.ts Outdated
return `${size}px`;
}
const min = `${clamp(column.minWidth) ?? 0}px`;
const flex = typeof column.size === 'object' ? clamp(column.size.flex) : undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit]:

Suggested change
const flex = typeof column.size === 'object' ? clamp(column.size.flex) : undefined;
const flex = !!column.size?.flex ? clamp(column.size.flex) : undefined;

Comment thread src/table-root/interfaces.ts Outdated
* * `{ 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provide one columns entry per column, in display order
Can we reorder columns in the future and how would this affect the reordering?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can we make it clear that size, minWidth, and maxWidth are per column before listing them?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, have updated

Comment thread src/table-root/interfaces.ts Outdated
* * `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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't set a min width, the content might get cut off or overlap is that expected? should we call this out?

Image

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, we should document this, updating

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants