Skip to content

feat: add sticky header to the atomic table components - #5055

Open
gethinwebster wants to merge 1 commit into
mainfrom
dev-v3-gethinw-table-sticky-header
Open

gethinwebster wants to merge 1 commit into
mainfrom
dev-v3-gethinw-table-sticky-header

Conversation

@gethinwebster

@gethinwebster gethinwebster commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Description

Add a sticky header to TableRoot, so the column headers stay visible while the table scrolls, in both grid and auto layout.

  • Two new TableRoot props: stickyHeader, which takes the TableHead to pin (children then holds only the TableBody), and stickyHeaderVerticalOffset, for pages with other fixed content at the top.
  • The pinned header is a copy of the real one, using the existing Table's sticky header internals from refactor: share sticky header internals between Table and Container #5212. It's hidden from screen readers and left out of the tab order, but stays clickable; focus that lands in it, from a click, moves to the matching control in the real header. When a control in the real header has keyboard focus, the copy shows the focus ring.
  • Grid layout lines the copy up through the shared column template. Auto layout copies the real header cells' measured widths and updates them when the cells resize.
  • The <table> now sets role="table" in auto layout as well as grid layout, so browsers don't guess whether it's a layout table.
  • New dev page: table-root/sticky-header.

Related links, issue #, if available: n/a

How has this been tested?

  • New unit tests: the copy is hidden and out of the tab order but clickable, including hidden controls, and focus that lands in it moves to the real header; it shows the focus ring of a focused real header control, including a checkbox for keyboard focus only; it copies cell widths in auto layout (also after a resize with no re-render, and after switching from grid layout) and not in grid layout, and tables without a sticky header don't measure their cells; it pins at the given offset; it follows the body's horizontal scroll, including when it mounts after the body has scrolled; and a focused body row scrolls out from under the copy for keyboard focus but not for clicks or for the scroll region itself.
  • New integration tests on the dev page, in grid and auto layout: the header pins to the top as the page scrolls; its cells line up with the columns on load, after a viewport resize, and after scrolling the table horizontally or with the sticky scrollbar; and clicking a pinned sort button sorts and moves focus to the real header without scrolling the page.
  • The auto layout role test is updated for role="table".
  • Existing table, table-root and container unit tests pass. The documenter snapshot only adds the two new props.
Review checklist

The following items are to be evaluated by the author(s) and the reviewer(s).

Correctness

  • Changes include appropriate documentation updates.
  • Changes are backward-compatible if not indicated, see CONTRIBUTING.md.
  • Changes do not include unsupported browser features, see CONTRIBUTING.md.
  • Changes were manually tested for accessibility, see accessibility guidelines.

Security

Testing

  • Changes are covered with new/existing unit tests?
  • Changes are covered with new/existing integration tests?

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@codecov

codecov Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.92473% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 97.82%. Comparing base (d2e84d8) to head (398279e).

Files with missing lines Patch % Lines
src/table-root/use-sticky-header.ts 98.66% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5055   +/-   ##
=======================================
  Coverage   97.82%   97.82%           
=======================================
  Files        1006     1009    +3     
  Lines       32574    32663   +89     
  Branches    12100    12129   +29     
=======================================
+ Hits        31865    31954   +89     
  Misses        667      667           
  Partials       42       42           

☔ 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.

@gethinwebster
gethinwebster force-pushed the dev-v3-gethinw-table-sticky-header branch from eb4ba10 to cf583e5 Compare November 3, 2026 08:29
@gethinwebster
gethinwebster force-pushed the dev-v3-gethinw-table-sticky-header branch from cf583e5 to d418faf Compare November 5, 2026 09:01
@gethinwebster
gethinwebster force-pushed the dev-v3-gethinw-table-sticky-header branch 2 times, most recently from b3b0279 to 6c09281 Compare November 5, 2026 09:25
@gethinwebster
gethinwebster requested a balanced review from Copilot November 5, 2026 09:29
@gethinwebster gethinwebster changed the title feat: add sticky header to the low-level table components feat: add sticky header to the atomic table components Nov 5, 2026

Copilot AI left a comment

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.

🟡 Changes recommended

Unresolved sticky-header defects leave hidden duplicate controls tabbable, obscure checkbox focus, misalign columns after activation, disrupt pointer clicks, and add unnecessary layout reads to non-sticky tables.

5 open issues
What changed in this PR

Adds sticky headers to the atomic table components, reusing the shared sticky-header internals introduced in #5212 for both grid and auto layouts.

Changes:

  • Adds stickyHeader and stickyHeaderVerticalOffset, with header-copy sizing, scroll synchronization, and focus mirroring.
  • Updates resize-observer callback timing and explicitly sets table semantics in auto layout.
  • Adds unit coverage, API documentation snapshots, and a sticky-header demo page.
File Description
src/​table-root/​use-sticky-header.ts Coordinates sticky positioning, column widths, and focus mirroring.
src/​table-root/​styles.scss Styles the pinned header, fixed-layout copy, and focus indicator.
src/​table-root/​sticky-header-slot.tsx Renders the header copy hidden from assistive technology.
src/​table-root/​internal.tsx Integrates the sticky copy and synchronized scrolling.
src/​table-root/​interfaces.ts Documents the new sticky-header props.
src/​table-root/​index.tsx Records sticky-header usage in telemetry.
src/​table-root/​__tests__/​sticky-header.test.tsx Tests header copies, sizing, focus, offsets, and scrolling.
src/​table-root/​__tests__/​basic-table-roles.test.tsx Updates expectations for explicit auto-layout table semantics.
src/​internal/​components/​sticky-header/​use-untabbable-descendants.ts Removes copied controls from the tab order.
src/​internal/​components/​sticky-header/​use-cells-resize-observer.ts Updates the resize callback during layout effects.
src/​internal/​components/​sticky-header/​__tests__/​use-cells-resize-observer.test.tsx Covers callback freshness when observing new cells.
src/​__tests__/​snapshot-tests/​__snapshots__/​documenter.test.ts.snap Records the new public props and content guidance.
pages/​table-root/​sticky-header.page.tsx Demonstrates sticky headers in grid and auto layouts.

Review effort: Balanced

Comment thread src/table-root/use-sticky-header.ts Resolved
Comment thread src/table-root/use-sticky-header.ts Resolved
Comment thread src/table-root/use-sticky-header.ts Resolved
Comment thread src/table-root/use-sticky-header.ts Resolved

Copilot AI left a comment

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.

Comment thread src/table-root/internal.tsx Resolved
Comment thread src/table-root/use-sticky-header.ts Resolved
Comment thread src/table-root/use-sticky-header.ts Resolved
Comment thread src/table-root/use-sticky-header.ts Resolved

Copilot AI left a comment

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.

🟡 Changes recommended

Browser coverage is missing for sticky-header pinning and rendered column alignment during scrolling and resizing in both grid and auto layouts.

1 open issue
4 resolved since last review

Review effort: Balanced

Comment thread src/table-root/styles.scss Resolved

Copilot AI left a comment

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.

🟡 Changes recommended

The stickyHeader slot-presence flag is recorded under props instead of metadata.hasStickyHeader, contrary to the component-structure convention separating configuration values from derived facts.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread src/table-root/index.tsx Resolved

Copilot AI left a comment

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.

🟡 Changes recommended

Copied header controls can still receive pointer focus inside an aria-hidden subtree; focus handling must be corrected and covered by a browser interaction test.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread src/table-root/sticky-header-slot.tsx Resolved

Copilot AI left a comment

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.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

3 open issues
1 resolved since last review

Review effort: Lite

Comment on lines +10 to +16
useLayoutEffect(() => {
if (containerRef.current) {
for (const element of getAllFocusableCandidates(containerRef.current)) {
element.tabIndex = -1;
}
}
});
Comment on lines +34 to +41
<div
ref={slotRef}
aria-hidden={true}
className={clsx(styles['sticky-slot'], isStuck && styles['sticky-slot-stuck'])}
style={style}
onFocus={onFocus}
>
<div className={styles['sticky-copy-scroller']} ref={copyScrollerRef} onScroll={onScroll} tabIndex={-1}>
Comment on lines +84 to +96
const syncColumnWidths = useCallback(() => {
if (!copyHeaderRef.current) {
return;
}
const copyCells = getHeaderCells(copyHeaderRef.current);
const widths = isGridRef.current
? []
: getHeaderCells(realHeaderRef.current).map(cell => cell.getBoundingClientRect().width);
copyCells.forEach((cell, index) => setElementWidths(cell, { width: widths[index] }));
}, []);
useLayoutEffect(() => {
syncColumnWidths();
});

Copilot AI left a comment

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.

Comment thread src/table-root/internal.tsx Resolved
Comment thread src/table-root/use-sticky-header.ts Resolved
Comment thread src/table-root/use-sticky-header.ts Resolved
Comment thread src/table-root/styles.scss Resolved
@gethinwebster
gethinwebster marked this pull request as ready for review November 5, 2026 17:21
@gethinwebster
gethinwebster requested a review from a team as a code owner November 5, 2026 17:21
@gethinwebster
gethinwebster requested review from NathanZlion and removed request for a team November 5, 2026 17:21

This branch was successfully deployed

3 active deployments
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