Skip to content

Table: six row and header fixes — in-cell clicks, column alignment, cut-off labels - #1192

Open
JeanMarcMilletScality wants to merge 16 commits into
development/1.0from
bugfix/CUI-table-row-clicks-and-headers
Open

Table: six row and header fixes — in-cell clicks, column alignment, cut-off labels#1192
JeanMarcMilletScality wants to merge 16 commits into
development/1.0from
bugfix/CUI-table-row-clicks-and-headers

Conversation

@JeanMarcMilletScality

@JeanMarcMilletScality JeanMarcMilletScality commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

TL;DR — Six defects in Table that only show up once a table is used in earnest: clicking a button inside a row also selected the row (and the re-render closed whatever the button had just opened), header labels sat over the wrong columns, a value ignored its column's alignment, a cut-off header label had no way to show its full text, and a selectable table looked inert until the first click. All six are fixed.

Context

All six surfaced while putting a table to real use, and they overlap so heavily in the same two files that as separate PRs they would conflict with each other rather than review independently.

Approach

Most of them come back to one mechanism — how a header cell and its body cells agree on a column's width — which is what makes them one PR rather than six.

Defect Cause → fix
A click on an in-cell control also selected the row, and the selection re-render closed whatever that control had just opened row onClick/onKeyDown fired for any target inside the row → one shouldIgnoreRowEvent guard, bounded to the row at both ends
Header tracks drifted from body tracks, and drifted again once one column ran short TableRowMultiSelectable never declared the gap HeadRow has, and body cells lacked the header's min-width: 0 reset → declare both
A plain string value ignored its column's alignment — 94px off centre on a centred column ConstrainedTextContainer hard-coded text-align: left, plus a one-sided Box mr={4} → inherit the alignment and drop the margin; the row's own gap already separates columns
An ellipsized header label had nothing to recover it body cells recover via ConstrainedText, headers had no equivalent → TruncatableHeaderLabel, offering a Tooltip only once the label is genuinely cut off
A selectable table looked inert until its first click the hover affordance was gated on $selectedId — "something is selected" — not on whether the table can be selected → gate it on $selectable
The header reserved the caret's room on its behalf, across three constants that had to agree by hand the caret was a zero-width proxy with the room in HeaderContent's padding → the caret carries its own width, and one constant describes it

The in-flow caret is only safe because of the min-width: 0 fix: real caret width lifts a sortable header's min-content above its body cell's, and that only stops mattering once min-width: auto is out of play — the two changes are coupled. Header-minus-body left and width are exactly 0 on every column across ten widths from 1400px down to 360px, single- and multi-selectable.

Screenshots

Table → Simple Content Table at full width — Age and its values share one right edge, Health and its caret are centred over the statuses, and the trailing blank header carries one right-aligned button per row:

cui1192-table-alignment-wide

The same table at 620px — labels ellipsize, the four tracks still agree between header and body, and the action column keeps its button intact rather than shrinking it:

cui1192-table-alignment-620px

Review focus

  • 🔴 SingleSelectableContent.tsx / MultiSelectableContent.tsx › row handlers — a consumer relying on "a click anywhere in the row selects it" will notice, and one carrying its own stopPropagation on an interactive cell can now drop it; TableCommon.tsx › INTERACTIVE_SELECTOR is the whole guard, so check nothing a cell renders is missing from it.
  • 🟡 Tablestyle.tsx › SortCaretWrapper + HeaderContent — changes header geometry for every sortable table, and is safe only because the min-width: 0 fix landed in the same PR, so review the two together.
  • 🟡 constrainedtext/Constrainedtext.component.tsx — the only change outside tablev2, on a publicly exported component: text-align goes from a hard-coded left to inherit, so an external consumer relying on it staying left-aligned under a centring ancestor will see it centre.

How to test

  1. npm run storybook, open Table → Simple Content Table. Each header label sits over its own column and stays there as you narrow the browser: Age flush-right over the numbers, Health centred over the statuses, the name columns flush-left.
  2. Hover a sortable header — the sort incentive appears and nothing moves. Hover the blank action header: no caret, and not a tab stop.
  3. Responsive Column Drop With Reveal — narrow until the +N trigger appears, then click it on an unselected row. The panel opens and stays open; it used to select the row, re-render and close.
  4. Table With View Action — click the button (the action fires, the row does not select), click the row's plain text (it selects), Tab to the button and press Enter (the button activates only). The button now sits inside its own column instead of overhanging by 7px.
  5. Narrow until a header label ellipsizes and hover it — the full label appears; widen until it fits and hover again — no tooltip. Then hover a row in any single-selectable story before clicking anything: the affordance and pointer cursor are already there.

Follow-up

  • TooltipContainer is an inline-block with no min-width: 0, which is why ConstrainedText and now HeaderLabelFrame both carry the same unwrapping workaround. Fixing it at source deletes both, but it touches every Tooltip in the library.
  • organisms/attachments/AttachmentTable.tsx carries a marginLeft: 'auto' that is inert at a grow-sum above 1 but becomes live below it, and a <Box flex={0.5} /> Header that is a no-op. Both were cleared as causes of the misalignment fixed here, so they are left alone rather than widening this PR.
  • react-table v7 spreads key inside getRowProps()/getCellProps(), which React reports as an error on every row and header. Pre-existing and library-wide, but it is console noise for anyone testing the above.
What changed

The alignment demonstration moved onto the shared default columns in stories/tablev2.stories.tsx rather than a separate story, so every story built on them changes appearance and gains a button per row. That action column's 8.5rem is measured against the button's current label rather than derived, so it needs revisiting if the label changes.

Three defects in the same component, all measured rather than reasoned about.

**A row's handler swallowed clicks meant for a control inside a cell.** The row
`onClick` in both selectable contents fired for any click anywhere in the row
with no target check, so clicking a button in a cell both activated the button
and selected the row — and the selection re-render remounts the memoized row,
unmounting whatever the button had just opened. The `+N` dropped-columns trigger
is core-ui hitting itself with this. `onKeyDown` had it worse: `keydown` bubbles
too, so Enter on a focused in-cell control selected the row *and*
`preventDefault()`ed the control's own activation.

Both handlers now bail on an interactive target, via one `isInteractiveTarget`
in `TableCommon` so the two copies cannot drift. The multi-selectable selection
cell no longer relies on the bubble the guard stops: it owns its click outright.
That is behaviour-preserving — its two former paths, direct in single-row mode
and bubbling to the row handler's else branch otherwise, ended in the same call
with the same arguments.

**A multi-selectable table's header tracks disagreed with its body tracks.**
`TableRowMultiSelectable` never declared the `gap` that `HeadRow` and `TableRow`
both do, so it computed `gap: normal`. Every column track has a grow factor and
no basis, which turns the gap the header reserves and the body does not into
free space the body redistributes — shifting each boundary by its grow share of
the total gap, at any width. Measured on a three-column table with grow factors
summing to 2.5: the header's three 14px gaps moved the columns by 25.20 / 8.41 /
8.40px, exactly 0.6 / 0.2 / 0.2 of 42px. One declaration; all deltas now zero.

This also clears the two suspects that were on the list. The scrollbar
compensation is fine — a single-selectable table with an identical 11px
scrollbar aligned perfectly both before and after — and the header does receive
`cellStyle`.

**An ellipsized header gave no way to read the full label.** Body cells recover
via `ConstrainedText`; the header was the one place a label became unreadable
with no way back. `TruncatableHeaderLabel` measures the label and offers `title`
only once it is actually cut off, re-measuring on resize because these columns
size from grow factors. Wrapping in core-ui's `Tooltip` would have removed the
truncation rather than explained it: `TooltipContainer` is an `inline-block` with
no `min-width: 0`, so it replaces the ellipsizing flex item with one that cannot
shrink below its min-content.

The alignment story ships so the measurement can be repeated; jsdom has no
layout, so it carries no jest assertion.
@bert-e

bert-e commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Hello jeanmarcmilletscality,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval
/bypass_build_status Bypass the build and test status
/bypass_commit_size Bypass the check on the size of the changeset TBA
/bypass_incompatible_branch Bypass the check on the source branch prefix
/bypass_jira_check Bypass the Jira issue check
/bypass_peer_approval Bypass the pull request peers' approval
/bypass_leader_approval Bypass the pull request leaders' approval
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request.
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

@bert-e

bert-e commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

The key names a private tracker project from a public repo. The test's own name
already says what it covers.
The `gap` fix closed the header/body disagreement at wide widths only. Narrowing
the container reopened it: the reporter saw a 128px header column over a 104px
body column, and a width sweep put the onset at ~420px for a multi-selectable
table and ~340px for a single-selectable one.

`TableHeader` resets `min-width: 0` — "the header must never be the reason a
column is wider than its cells". The body cells never got the same reset, so they
sat at the flex default `min-width: auto`, whose automatic minimum is
content-based. Two symptoms follow from that one asymmetry:

- short body cells ("Attached", an action button) freeze at their content floor
  while their headers keep shrinking, so those columns read wider in the body;
- flexbox then redistributes the frozen items' share of the negative free space
  onto the row's only still-shrinkable cell, which over-shrinks *below* its own
  header. That is the 128-vs-104 the reporter measured.

Both rows now shrink by the same rules. The reset goes before the `cellStyle`
spread, so a consumer's explicit `minWidth` still wins. Swept 736px down to
224px across all three selectable shapes: every header/body delta is now exactly
0, where before they diverged by up to 41px.

The alignment story becomes the verification surface for the whole change: four
panels, each in a draggable frame, and the two single-selectable ones now wire
`onRowSelected`/`selectedId` so row selection is actually visible — without it
the in-cell-click guard had nothing observable to demonstrate. Panel C adds
dropped columns with `revealDroppedColumns` so the `+N` trigger can be clicked on
an unselected row, which is the case that regressed.

Known limitation, unchanged in kind by this commit: a control that cannot shrink
overflows its column once the column is narrower than the control. With the floor
gone that becomes visible on the action column below ~260px. The remedy is
consumer-side — `iconOnly` on the action button — not a floor here, which would
put the columns back out of agreement.
…ected

`TableRow` decided whether to paint the hover highlight and the pointer cursor
from `$selectedId` — whether *something is currently selected* — which starts
undefined. A selectable table therefore rendered with no affordance at all until
after its first click, and nothing invited that click. It also made the fix for
in-cell clicks impossible to demonstrate: the reveal stories pass no
`onRowSelected`, so their rows were never selectable in the first place.

The gate is now `$selectable`, meaning the content was given an `onRowSelected` —
the same source it already uses for `tabIndex`. The selected-row highlight drops
to `$isSelected` alone, since a selected row implies a selection exists.
`Tablestyle` is not re-exported from `index.ts` or `next.ts`, so widening
`TableRowType` is internal.

Both responsive-column-drop stories now pass `onRowSelected`/`selectedId`, so the
`+N` trigger can be exercised on a genuinely selectable row — verified: the
popover opens, the row stays unselected, and the popover is still open after
500ms.

The temporary header/body alignment story is removed now that the measurement it
existed for is done; the two mechanisms it caught are covered by the commits
before this one. Its prose, and the story-level prose the drop stories carried,
moves into the Table guideline, where row selection, `dropAt`,
`revealDroppedColumns` and the non-shrinking-control caveat are now documented —
stories stay pure examples.
target: EventTarget | null;
}): boolean =>
event.target instanceof Element &&
!!event.target.closest(INTERACTIVE_SELECTOR);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

.closest() walks the entire ancestor chain, not just within the row. If the table sits inside an element matching INTERACTIVE_SELECTOR (e.g. a [role="button"] container or a <label>), clicks on plain cell content would match the outer ancestor and silently suppress row selection.

Scoping to event.currentTarget avoids this:

Suggested change
!!event.target.closest(INTERACTIVE_SELECTOR);
!!event.target.closest(INTERACTIVE_SELECTOR)?.closest('[class*="tr"]') === null
? false
: true;

Actually, a cleaner fix — check the matched element is inside the handler's own row:

export const isInteractiveTarget = (event: {
  target: EventTarget | null;
  currentTarget: EventTarget | null;
}): boolean => {
  if (!(event.target instanceof Element)) return false;
  const hit = event.target.closest(INTERACTIVE_SELECTOR);
  return !!hit && (event.currentTarget instanceof Element
    ? event.currentTarget.contains(hit)
    : true);
};

This way an ancestor <a> or [role="button"] wrapping the table won't disable row selection.

JeanMarcMilletScality and others added 2 commits August 28, 2026 13:38
…dable

Three things a review of the previous commits turned up, all verified in a
browser rather than only in jsdom.

**The row-click guard was unbounded in both directions.** It called
`closest(INTERACTIVE_SELECTOR)` from the event target with no upper bound, so a
table rendered inside a `<label>`, an `<a>` or a `[role="button"]` matched that
ancestor for *every* cell and row selection stopped working entirely. In the
other direction, a React portal leaves the row in the DOM but still bubbles to
it through the React tree, so plain text in a portalled popover — the
`revealDroppedColumns` panel — reached the row handler with nothing interactive
in between and silently selected the row behind the open overlay. The search is
now bounded to the row at both ends, and anything not contained by the row is
not a click on the row. Renamed to `shouldIgnoreRowEvent`, since it answers a
broader question than "is this a control". Both holes have a regression test,
each confirmed to fail against the previous implementation.

**An ellipsized header offered no way to read the full label.** Body cells
recover through `ConstrainedText`; headers had nothing. They now show the label
in a `Tooltip`, and only once it is actually cut off — a tooltip repeating a
header that reads fine is noise. That needs a live measurement, because a column
is sized by a grow factor and truncation onset moves with the table's width.
The tooltip wrapper is mounted in every state and only `overlay` is conditional:
mounting it on the flip would change the DOM under the element being measured
and let the two states oscillate. A native `title` was tried first and rejected —
the delay is drawn by the OS and cannot be configured, and react-table's
`getSortByToggleProps()` already puts `title="Toggle SortBy"` on the header, so
the two collided.

**`HeaderLabel` relied on its parent for the ellipsis.** `overflow` and
`text-overflow` are inert on an inline box, and a `span` is inline by default —
it only worked because flex blockifies its children. Wrapping the label removed
that and the truncation silently disappeared, with every test still green,
because the tests stub `scrollWidth` and cannot see `display`. `display: block`
is now stated on the label itself and asserted in a test that fails without it.

Also, so the same rule is not written twice: the body cell's `min-width: 0`
reset — the one that keeps header and body shrinking alike — moves into a shared
`bodyCellStyle`, and the header-label story gains a label long enough to actually
truncate, with the behaviour documented in the guideline rather than the story.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guideline still said a truncated header carries its text as a `title`. That
was true of an earlier draft; the label now recovers through a `Tooltip`, because
`title` is drawn by the OS after a delay that cannot be configured and collides
with the `title` react-table already puts on a sortable header. It also said
every string header carries it, where it appears only while the label is cut off.
@bert-e

bert-e commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

The caret was a zero-width flex item holding an absolutely-positioned glyph,
with HeaderContent reserving the room for it in padding. Three constants had to
agree by hand -- caretGlyphSize plus caretGutter against caretSpace -- with
nothing enforcing it, and the glyph's position was expressed twice, once per
alignment branch.

The premise for keeping the caret out of the flow was that a caret with real
width lifts a sortable header's min-content above its body cell's, so the two
rows stop agreeing on column widths. That only binds while min-width: auto is in
play. The header chain and the body cells now both carry min-width: 0, so the
floor sits at 0 and an in-flow caret costs nothing.

SortCaretWrapper is therefore a plain flex item with a real width, and that
width is the whole reserve: one value instead of three, and a sortable header's
intrinsic width finally includes its own caret. The width stays unconditional
because the glyph does not -- SortIncentive appears only on hover, and a reserve
that came and went with it would shift the header out from under the pointer.

Centred sortable headers keep a counterweight, because flex centres
label-plus-caret and leaves the label half a caret off centre. One caret width of
leading padding corrects that exactly, capped at 15% of the header so a column
too narrow to afford the correction spends its width on the label instead: it
drifts off centre rather than truncating sooner. The cap starts binding below a
117px header and bounds the drift at 8.75px.

Story case D pins two centred sortable columns narrow enough for the cap to
bind, so the trade-off is visible rather than asserted.
tabIndex and the hover affordance are rendered output, so they were being read
out of a ref during render. That reads fresh today only because react-table
hands react-window a new rows array every render, which stops the memoized row
from ever bailing out -- a coincidence of someone else's memoization rather than
anything this component guarantees. A plain derived boolean plus a memo
dependency says the same thing without depending on it; the ref stays for the
event handlers, which is what it is for.

The two new tests cover an affordance that had none: every existing selectability
test passes onRowSelected, so "no tabIndex when rows are not selectable" was
never asserted. They pass against the ref read as well, and are coverage rather
than a regression guard.
Comment thread stories/tablev2.stories.tsx Outdated
},
];

export const SortCaretHeaderAlignment = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The PR description and test plan reference a HeaderBodyColumnAlignment story ("Components/Data Display/Table → Header Body Column Alignment") that should be added in this file, but it isn't present in the diff or anywhere in the codebase.

The description says it "ships so the measurement can be repeated" for the multi-selectable gap fix — a three-column shape with and without a scrollbar, in both selectable modes. Without it, the test plan's step 1 can't be followed and the fix can't be visually verified in Storybook.

A column declares its alignment once, in cellStyle. The header honoured it;
a plain string value never did. The value inherited the alignment correctly all
the way down the wrapper chain and was then overridden at the last element:
ConstrainedTextContainer hard-codes text-align: left, and a rule on the element
beats anything inherited. Measured on a 1200px viewport, a centred column's value
sat 94.33px left of its own header label and an end-aligned column's 143.2px
short of it.

The scope is what let it survive: DefaultRenderer routes only string values
through ConstrainedText, so a column with a custom Cell renderer aligned
correctly and a plain string column did not -- the two disagreed with each other
as much as with the header.

ConstrainedText now inherits instead, with the centered prop kept as an explicit
override for a container that sets no alignment of its own. Of its four
in-library call sites, the chart labels pass centered explicitly and are
unaffected; the rest sit under no centring ancestor, so inherit resolves to left
exactly as before.

Behind that sat a second, independent offset: DefaultRenderer wrapped the value
in a Box mr={4}, 8px on one side only, which left a centred value 4px from its
label and an end-aligned one 8px short even once the alignment was right. A
body-only margin cannot keep header and body in agreement. The row's own gap
already separates the columns, and both rows share it.

Header-minus-value is now 0 on every column and every alignment, and story panel
D pairs each narrow centred sortable column with an identical column that is not
sortable, so the caret's counterweight can be read off directly instead of
inferred.
… its label

A centred sortable header needs a counterweight, because flex centres
label-plus-caret and leaves the label half a caret off centre. That counterweight
was a fixed padding capped at 15% of the header, and a percentage cannot tell
"no room" from "some room": it drifted a 98px column that had ample space for the
full correction, while still not giving a 70px column enough.

It is now a pseudo-element flex item instead, with a shrink factor weighted far
above the label's. Negative free space goes to it first, so a centred header is
exactly centred whenever its label fits beside the reserve, and the reserve --
not the label -- is what gets spent when it does not.

Measured on the story's two centred sortable columns, label offset from its own
header box centre:

  98px column   capped padding -1.41   counterweight  0
  70px column   capped padding -3.50   counterweight -1.96

The 70px case cannot reach zero: a 6-character label plus two 17.5px reserves
needs 74px, so at 70px the column is over-constrained at rest and -1.96 is the
least drift available without losing a character. 74px measures exactly 0.

A pseudo-element rather than a real element so nothing is added to the DOM or the
accessibility tree, and so the counterweight cannot leak into an alignment that
has no asymmetry to correct -- it exists only inside the centred sortable branch.
The reserve stays unconditional with respect to the glyph, which appears only on
hover; hovering still moves nothing.
@JeanMarcMilletScality JeanMarcMilletScality changed the title bugfix(tablev2): keep row clicks, header tracks and header labels honest bugfix(tablev2): keep rows, header tracks and header labels honest Aug 31, 2026
@JeanMarcMilletScality
JeanMarcMilletScality marked this pull request as ready for review August 31, 2026 17:56
Two of them read as if a table nested inside a `<label>` were a supported
pattern. It is not, and it was never the point: each test stands for one bound
of the row-event guard, and the wrapper is only the cheapest way to put a
matching element where the bound has to stop.

- The upward-bound test now wraps the table in a clickable card, which is a
  shape a table really does turn up inside, and its name says what it asserts:
  an interactive ancestor above the table must not disable row selection.
- The portal test is named for the case it stands for -- an overlay opened out
  of a cell -- rather than for the mechanism used to build one.
- The selector's `label` entry is documented as an in-cell label wrapping that
  cell's own control, which is the case that justifies it.

Both bounds are mutation-checked: removing either one fails exactly the test
that covers it, and nothing else.

Drops the test that read `overflow`, `display` and `tagName` back out of the
header label. Tests here assert user-facing behaviour, not CSS, and the three
tests above it already cover what a user can tell apart -- the tooltip appears
when the label is cut off and stays away when it is not.
…ry's modal

`isOpen`, `close` and `title` were each passed twice, verbatim, to the same
`Modal`. React takes the last of a repeated attribute, so the behaviour was
never wrong -- but it is a real `TS17001` in a file this branch is already
editing, and the second copy is the one a reader has to reason about before
concluding it changes nothing.

Nothing else in the story moves; only the three repeated lines go.
…oduction story

`SortCaretHeaderAlignment` existed to prove the header work, not to document the
component. Four panels of paired sortable/non-sortable columns is a measuring rig:
it reads as a page about carets rather than a page about tables, and a reader who
wants to know how a column declares its alignment has to infer it from a
reproduction. It goes, along with its three fixtures.

The behaviour it demonstrated moves into the default columns, where a reader meets
it first. `Age` becomes right-aligned and `Health` centred, which puts all three
alignments on one table and makes the two non-left-aligned headers the two that
carry a sort caret -- the end-aligned label keeping the trailing edge and the
centred label balanced against the caret are exactly what panels B and D were for.
Measured on the default story at a 1400px viewport, the value-minus-label offset on
each column's own edge is 0.00px left, 0.00px right and 0.00px centre, and no column
disagrees with its header on width.

The columns also gain a trailing action column: blank header, `disableSortBy`, one
`View details` button per row. It is not decoration. The guideline claims a control
in a cell keeps its own click and embeds this story as the evidence, and until now
no cell in it held a control -- there was nothing for a row-wide handler to steal.
Clicking the button leaves `aria-selected` at `false`; clicking a plain cell in the
same row turns it `true`.

The column is a fixed 8.5rem track rather than a grow factor. The button is
`white-space: nowrap` and will not shrink below its label, so on a grow factor it
overflows into its neighbour as soon as its share falls under its own width -- which
is the failure the guideline's last section warns about, and it would have shown up
in the 500px sync-button story. Measured at a 14px root the button is 113.84px
(1px border + 14px padding + 24.5px icon slot + 74.34px label); 8.5rem is 119px, so
it holds with 5.16px of slack, and below 8.13rem it bleeds. Horizontal placement is
`alignItems`, not `justifyContent`, because a body cell's own vertical centring is
applied after the column's `cellStyle` and takes the main axis.

`tablev2.guideline.mdx` gains a `Column alignment` section carrying what the panels
showed in prose: alignment is declared once and reaches header and values alike, a
caret reserves its own width instead of taking it from the label, and on a column
too narrow to hold both the centred label drifts off centre rather than truncating
earlier than it would without a caret.

An earlier commit on this branch closes with "story panel D pairs each narrow centred
sortable column with an identical column that is not sortable" -- panel D is what is
being removed here, and the reasoning it recorded now lives in the guideline section.
@JeanMarcMilletScality

Copy link
Copy Markdown
Contributor Author

@claude The Missing SortCaretHeaderAlignment story finding is stale — it was read against the previous PR description. That story was deliberately removed at the reviewer's request; the alignment behaviour now lives on the default columns fixture in SimpleContentTable (Age right-aligned, Health centred, plus a header-less action column), and tablev2.guideline.mdx gained a Column alignment section. The description and test plan were rewritten to match at 08:12 UTC, after this check had already started at 08:10 — the live body no longer references any panel A–D. Please re-review against the current description.

…n column

`TableWithViewAction` is the story the guideline points at to show how an action
column is built, and it was breaking the rule the guideline's own last section
states. On `flex: 1` its action column measured 106.66px in this 700px table
against a 113.84px button, so the button hung 7.18px past its column -- the
button is `white-space: nowrap` and will not shrink below its label, so a grow
factor cannot hold it.

Two of the three declarations on that column were also dead. `display: flex` and
`justifyContent: flex-end` are both applied by the body cell's own flex centring
*after* the column's `cellStyle` is spread, so the button was never right-aligned
either -- it sat flush left and poked out on the right. `alignItems` is on the
cross axis and survives, which is where a control's horizontal placement has to
be stated.

The column now matches the default columns' action column exactly: a fixed 8.5rem
track, `alignItems: flex-end`. Measured after: the column is 119px, the button's
right edge is flush with it, 5.16px of slack on the left, header and body agree on
every column, and no row overhangs the body.
Same pass across every comment this branch adds: keep the reason a declaration
is load-bearing or a default is wrong, drop the walk-through that produced it.

The bounded-search note loses its two-bullet anatomy but keeps both holes it
closes. The header tooltip loses the accessibility-tree reassurance and the
`title` collision keeps its one line. The centred-caret counterweight keeps why
a mirrored pseudo-element beats a fixed reserve, without re-deriving flexbox's
resolution rules. The action column keeps its measured button width, since the
`8.5rem` track is meaningless without it, and loses the box decomposition.

191 comment lines down to 141, no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second pass, tighter calibration: nine blocks lose the narrative retelling and
keep the fact. Notably `TruncatableHeaderLabel` 10 -> 8, `shouldIgnoreRowEvent`
6 -> 4, the caret reserve 6 -> 5 and the centred-caret counterweight 6 -> 4.
Every measured number a maintainer would need is still there -- 113.84px against
the 8.5rem track, the 4px/8px header offset -- without the walk-through.
@JeanMarcMilletScality JeanMarcMilletScality changed the title bugfix(tablev2): keep rows, header tracks and header labels honest Table: six row and header fixes — in-cell clicks, column alignment, cut-off labels Sep 3, 2026
@bert-e

bert-e commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Conflict

There is a conflict between your branch bugfix/CUI-table-row-clicks-and-headers and the
destination branch development/1.0.

Please resolve the conflict on the feature branch (bugfix/CUI-table-row-clicks-and-headers).

git fetch && \
git checkout origin/bugfix/CUI-table-row-clicks-and-headers && \
git merge origin/development/1.0

Resolve merge conflicts and commit

git push origin HEAD:bugfix/CUI-table-row-clicks-and-headers

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.

2 participants