feat: Table atomic components (extracted from classic Table) - #4977
Draft
gethinwebster wants to merge 29 commits into
Draft
feat: Table atomic components (extracted from classic Table)#4977gethinwebster wants to merge 29 commits into
gethinwebster wants to merge 29 commits into
Conversation
…ment Introduce src/table/table-cell (internal, not yet exported) holding the pure body-cell substrate: the td/th element, base .body-cell padding, the .body-cell-content wrapper, disablePaddings support, and raw-node ref forwarding. TableTdElement now composes InternalTableCell, keeping all feature layering (sticky/resize/editable/selection/expandable + compound .body-cell.* CSS) on its own wrapper. DOM/CSS-identical by construction: same classes, same child order, feature SCSS unchanged; only an inert .body-cell-content.disable-paddings rule is added (never applied by classic).
Carve the <tbody> and <thead> element substrates out of classic's render sites into thin internal forwardRef components (src/table/table-body, src/table/table-head), mirroring the Inc1/Inc2/Inc3a pattern. Render-neutral: each substrate renders only the bare element and forwards ref/className/nativeAttributes/children; all feature wiring stays on the composing sites (internal.tsx, thead.tsx). TableRoot's <table> is deferred (performanceMarkAttributes data-* threading is incompatible with the data-*-free substrate contract) -- see evidence FINDINGS/DECISIONS.
…ubstrates (Inc4a)
…internal-only) + review fixes (Inc7b)
…adding + header dividers (v1/v3)
…root-classic) Recreate each pages/table-root atomic demo with the shipped classic Table as a per-scenario VR oracle. Same data (makeItems/makeLine), columns, counts and initial state so the atomic-vs-classic diff is content-identical. Auto-registered at table-root-classic/<name>. Multi-column sort and row virtualization have no classic equivalent (see FINDINGS). Pages only; src untouched.
Set variant="borderless" on the classic Table in every table-root-classic demo so the classic twin is chrome-free (no outer Container border/radius/ shadow), matching the atomic TableRoot. This is the agreed VR oracle for the atomic-vs-classic parity work.
Two dominant atomic-vs-classic (borderless) gaps from the VR punch-list: a1 header row height (29px -> 37px): the public TableHeaderCell rendered its children with no content box, so the header row was 8px short. Wrap the children in a .header-cell-content box carrying padding-block:$space-scaled-xxs (+ line-height:$line-height-body-m), reproducing classic's header-cell-content vertical sizing. No inline padding, so the v1 is-visual-refresh first-column inset and the v3 ::after divider are untouched. a2 edge-row height (first/last body rows 1px taller): TableBody now publishes each row's first/last position via a RowPositionContext (a no-DOM provider), and TableCell applies classic's own body-cell-first-row / body-cell-last-row edge classes. Reusing classic's classes means the 2px transparent placeholder border and the body-cell-last-row:not(.body-cell-selected) selection guard come for free -- no reimplementation, no data-* hook, no element/universal selector. Auto layout verified pixel-exact vs the borderless classic twin (simple page: header 37, rows 40/39/.../40). Atomic layer only; classic composition (the src/table substrates) is untouched.
…tes (drop redundant typed props)
… inset, grid row height (a3/a4/a5)
The .body-cell-content truncation clip (overflow:hidden) also applied to disablePaddings cells, whose content box collapses to the control height. That cropped the focus ring of a checkbox/radio in a selection-control column. Opt disable-paddings cells back out of the clip (overflow:visible), mirroring the existing body-cell-edit-active idiom. Classic Table is unaffected (disable-paddings is atomic-only). VR unchanged, jest 673/673.
…th body (== classic)
… via data-selected adjacency TableRow auto-emits data-selected from variant='selected'; the atomic cell module reads sibling adjacency to collapse the shared edge into a single 1px divider, square the inner corners, and (via has-selection + a full-height stretch) enclose the selection-control column in the box. Keyed on data-selected, a hook classic never emits, so classic VR stays 0.0000%.
The simple, striped-rows and single-selection atomic demos have no sort UI, but their classic twins reused sorting-capable column sets. Classic Table renders a sort caret for any column declaring sortingField/sortingComparator (independent of sortingColumn/onSortingChange), so those twins showed carets the atomic lacks. Add non-sorting column sets (dataColumnsPlain, nameStatusColumnsPlain) and point the no-sort twins at them; sorting twins keep the sortable sets.
…assic
The grid columns [{size:40},{minWidth:160},{size:140}] flexed Name to fill and pinned Status to a fixed 140px, shoving Status far right. Classic Table splits the non-control space ~53:47 (measured, proportional across viewports). Reproduce with proportional flex weights [{size:40},{size:{flex:53}},{size:{flex:47}}] so atomic per-column widths match classic to the pixel.
Point the classic loading-and-empty VR oracle at dataColumnsPlain (no sortingField/sortingComparator) so it renders no sort carets, matching the non-sortable atomic demo. Removes the stray down-carets on all 4 headers and converges column widths to the atomic (was Size -26.5px).
The header column-divider ::after painted a 1px divider (border-inline-start,
color-border-divider-default) on disablePaddings header cells (the selection
control column). Classic Table draws no divider there. Add
.header-cell.disable-paddings::after { display: none }. Classic never emits
disable-paddings, so this is inert on the classic path (selection-single VR
0.0000% vs oracle).
…ia data-shaded, align first-column header
…header divider, and equal-height cells
Standalone visual-parity fixes surfaced by eyeballing the demo pages
against borderless classic Table twins (percentages proved unreliable for
sparse table UI):
- Equal-height cells: move the stretch+center treatment onto the grid-mode
markers (.cell-grid / .header-cell-grid) instead of the unconditional
.cell, so auto-layout tables keep native display:table-cell (fixes the
collapsed simple/auto layout) while grid tables get colinear control/data
boxes and centered controls.
- Consecutive-selected seam: set the shared bottom edge to the 1px list
divider (mirroring classic body-cell-next-selected) so the two-row seam
paints 2px like classic, not 1px.
- Control-column header divider: remove the erroneous
.header-cell.disable-paddings::after { display:none } suppression -
mainline classic DOES draw this divider. The empty single-selection
control header no longer collapses (equal-height fix), so the restored
divider renders full-cell like classic with no stray streak.
All keyed on atomic-only selectors classic never emits; classic Table VR
byte-identical (0%), jest green.
An isolated selected row was +1px taller (40px) than unselected (39px):
the selection border grows +2px total while classic's body-cell-selected
reclaim returns only 1px. In grid mode each row is its own grid-auto-rows
track sized by cell border-box, so that +1px grew the track and cascaded
every row below by 1px on each select/deselect, resizing the table.
Consecutive-run rows already net 39px via the seam/placeholder-start rules,
so only the isolated selected state leaked. Absorb its surplus pixel into
the selected cell's own padding-block-end, grid-mode only:
[data-selected]:not([data-selected] + [data-selected]):not(:has(+ [data-selected])) > .cell-grid { padding-block-end: 0 }
Result: every selection state is 39px; select/deselect causes zero row-height
change, zero table resize, zero cascade (measured, both selection pages).
2px selection border, merged-pair outline, and control centering unchanged.
Grid-scoped, so auto-layout tables and classic (never emit .cell-grid) are
inert. Constant-height chosen over classic's neighbor-compensation (user
decision) since it eliminates all movement, including classic's own 1px nudge.
…zero content shift) Replace the real-border selection outline with an absolutely-positioned ::after ring on the selected row, and neutralize the reused body-cell selection border to a constant 1px so real border widths never change between selected/unselected. Grid content-centering is then border-insensitive: on select/deselect the cell content, control, row top, and table height are all invariant (measured dy=0, every row 39px), matching classic's zero content movement — which the real-border approach could not do under grid layout. - reset the selected cell's content-wrapper bottom padding to the unselected value (classic's reclaim compensated for a real border the atomic no longer has), removing the last 0.5px text nudge; - draw the consecutive-selected seam from the first-of-pair ring's own 2px bottom edge (matches the outer ring width) instead of a 1px tinted divider; - suppress (transparent, width unchanged) the unselected-above row's divider so no grey line abuts the ring top, matching classic. Retires the constant-height padding-block-end:0 hack and per-state seam border-width juggling. Atomic-only hooks (data-selected, .cell-grid); classic-inert.
…assic' wording
Behaviour-neutral cleanup of the 7 atomic components:
- Trim multi-paragraph comments to concise WHY/gotchas; remove comments that
restate the code or narrate edit history (row-px values, 'earlier fix', etc.).
- Remove a dead `styles['disable-paddings']` class from the body <td> (no such
rule exists in the table-cell module; disablePaddings is handled by the
substrate on the inner content wrapper) — a remnant of the removed per-state
control-cell padding compensation.
- Replace ambiguous 'classic'/'classic Table' wording with 'the existing Table'
('classic' conventionally denotes the classic theme). Reused hashed class
names, import paths, and the is-visual-refresh theme token are unchanged.
No functional or CSS-selector changes. jest 37/37, gulp quick-build, and
stylelint all green.
These borderless-Table reference pages were scaffolding for the visual-parity work (an apples-to-apples oracle for eyeball comparison), not part of the shipped component. Removing them now that parity is established. Pages are auto-discovered, and nothing outside the directory referenced it.
gethinwebster
had a problem deploying
to
allure-report
September 8, 2026 16:01 — with
GitHub Actions
Failure
…y->ies finder derivation Adding the 7 public atomic components regenerated three global snapshots (documenter, test-utils-wrappers, test-utils-selectors) — additive only (the new component definitions + their generated finders). Also fixes a latent bug in the shared functional-tests/test-utils.test.tsx helper: it derived the expected findAll finder name by regex-scanning the wrapper prototype for `findAll<Pascal>.*`, which fails when the plural is not singular+suffix. TableBody is the first component whose finder is pluralized y->ies (findAllTableBodies), so the naive match resolved to undefined and threw `wrapper[findAllName] is not a function`. Derive the name from the same build-tools/utils/pluralize helper the generator uses, so the test can never drift from the generated finder names; drop the now-unused DomElementWrapper import. Full snapshot + functional + table-root suites: 10 suites / 730 tests / 107 snapshots green.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4977 +/- ##
========================================
Coverage 97.66% 97.67%
========================================
Files 959 990 +31
Lines 31345 31556 +211
Branches 11579 11630 +51
========================================
+ Hits 30614 30823 +209
+ Misses 724 687 -37
- Partials 7 46 +39 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Introduces 7 public Table atomic components —
TableRoot,TableHead,TableHeaderRow,TableHeaderCell,TableBody,TableRow,TableCell— extracted from the existingTable's own cell/row/section elements so the existingTableand the atomics share one substrate.Approach (extract from the existing Table)
Instead of building atomic parts separately (which diverged from the existing box model — sticky columns and geometry broke), this works backwards: internal element substrates (
TableCell/TableHeaderCell/TableRow/TableBody/TableHead) are carved out of the existingTable'std-element/th-element/internal.tsx/thead.tsx, and the existingTablecomposes those same substrates. The public atomic components layer the R1 API + standalone styling on top.TableRoot's<table>carries too much wiring to carve render-neutrally, so it is public-only for now.Because the existing
Tableand the atomics share the substrate, the existingTableis unchanged by construction, and the atomics reuse its proven box model + feature CSS (selection, shading, dividers). Where the atomic public layer must re-emit a class the existingTablecomputes centrally, it does so via atomic-only hooks (.cell-grid/.header-cell-gridgrid markers,data-selected/data-shadedrow adjacency) that the existingTablenever emits — so the shared CSS is reused, not duplicated, and stays inert for the existingTable.Verification
Tableis provably unchanged: byte-identical vs a pre-change 16-state oracle (incl. held sticky horizontal scroll); the two residual non-zero states match the baseline to the pixel and predate this work.Tabletwin pages (an apples-to-apples, chrome-free oracle) built during development and since removed as scaffolding.jestgreen; the existingTableVR 0% reconfirmed after every change.Fixes landed from the eyeball audit
.header-cell-content's inlinecell-offset); aligned to body.data-selectedadjacency; the seam is 2px, matching the outer ring..cell-grid/.header-cell-grid), so auto-layout tables keep nativedisplay:table-cell(an earlier unconditional rule broke auto-layout column sizing) while grid tables get colinear control/data boxes and vertically-centered controls.$color-border-cell-shadedby reusing the existingTable's body-cell striped rule, triggered viadata-shadedrow adjacency (dedup intact — the rule exists once in the substrate; only the trigger class was missing).Tabledraws it); the empty single-selection control header no longer collapses (equal-height fix), so the divider renders cleanly with no stray streak.::afterring on the selected row, and the reused selection border is neutralized to a constant 1px, so real border widths never change between selected/unselected. Grid content-centering is therefore border-insensitive: on select/deselect the cell content, control, row top, and table height are all invariant (measured dy=0, every row 39px) — reproducing the existingTable's zero content movement, which a real-border approach cannot do under grid layout. This retired an earlier constant-height padding hack and the per-state seam border-width juggling.disablePaddingscontrol cells fixed (overflow:visible).Known minor deltas (documented, not regressions)
columnLayout='auto'is zero-config intrinsictable-layout:auto;gridmode is the opt-in for explicit widths). Accepted.<button>-based sort header vs the existing native control — accepted.Table's (39 vs 40) — the deliberate constant-height/overlay tradeoff for zero toggle-shift. Selection ring is unchanged (2px); imperceptible.Notes
Dev pages:
pages/table-root/(atomic demos). Supersedes the Path-C dedup approach ondev-v3-gethinw-basic-table-mlp.Draft — pending human bug-bash + publish.