Repository navigation
feat: add sticky header to the atomic table components - #5055
gethinwebster wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
eb4ba10 to
cf583e5
Compare
cf583e5 to
d418faf
Compare
b3b0279 to
6c09281
Compare
There was a problem hiding this comment.
🟡 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
Observe descendant changes when suppressing copied header tab stops · New Skip header measurements when no sticky copy exists · New Initialize the sticky header's scroll position on mount · New Mirror focus to the visible outline of the copied select-all checkbox · New Skip row scrolling for mouse-originated focus · New
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
stickyHeaderandstickyHeaderVerticalOffset, 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
6c09281 to
ed05370
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved sticky-header defects can desynchronize interactive controls, misalign columns, and impair keyboard accessibility through missing focus indicators, duplicate tab stops, and unexpected scrolling.
5 open issues
Detect hidden focusable candidates in the sticky copy · New Sticky header copies can diverge in interactive state · New Refresh resize subscriptions when header cells change · New Mirror focus for text inputs in the sticky header · New Avoid scrolling the page when the scroll region receives focus · New
5 resolved since last review
Review effort: Balanced
ed05370 to
ef66438
Compare
607d7ce to
9af97bd
Compare
There was a problem hiding this comment.
🟡 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
a451ed2 to
715e7f2
Compare
There was a problem hiding this comment.
🟡 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
715e7f2 to
398279e
Compare
There was a problem hiding this comment.
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
| useLayoutEffect(() => { | ||
| if (containerRef.current) { | ||
| for (const element of getAllFocusableCandidates(containerRef.current)) { | ||
| element.tabIndex = -1; | ||
| } | ||
| } | ||
| }); |
| <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}> |
| 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(); | ||
| }); |
There was a problem hiding this comment.
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
8 open issues
This effect runs after every render and performs a fullquerySelectorAllover the container each… · New The new API contract requires that whenstickyHeaderis provided,childrenmust contain only… · NewsyncColumnWidths()runs on every render due to the unscopeduseLayoutEffect, and it reads… · NewonScrollerBlurclearsfocusedHeaderPathon every bubbling blur event from the scroller subtree… · NewsyncColumnWidths()runs after every render due to the missing dependency array on the… The sticky header copy isaria-hiddenbut still contains interactive descendants that can receive… This hook mutatestabIndexon all focusable candidates on every render and never restores the…z-index: 800is a hard-coded layering value that may be difficult to reason about or keep… · New
Review effort: Lite


Description
Add a sticky header to
TableRoot, so the column headers stay visible while the table scrolls, in both grid and auto layout.TableRootprops:stickyHeader, which takes theTableHeadto pin (childrenthen holds only theTableBody), andstickyHeaderVerticalOffset, for pages with other fixed content at the top.<table>now setsrole="table"in auto layout as well as grid layout, so browsers don't guess whether it's a layout table.table-root/sticky-header.Related links, issue #, if available: n/a
How has this been tested?
role="table".Review checklist
The following items are to be evaluated by the author(s) and the reviewer(s).
Correctness
CONTRIBUTING.md.CONTRIBUTING.md.Security
checkSafeUrlfunction.Testing
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.