Skip to content

fix(data-table): Group fixes for virtualized and non virtualized table#791

Open
paanSinghCoder wants to merge 18 commits intomainfrom
fix/datatable-group-anchor-push-up
Open

fix(data-table): Group fixes for virtualized and non virtualized table#791
paanSinghCoder wants to merge 18 commits intomainfrom
fix/datatable-group-anchor-push-up

Conversation

@paanSinghCoder
Copy link
Copy Markdown
Contributor

@paanSinghCoder paanSinghCoder commented May 7, 2026

Aligns virtualized group anchor scroll behavior + styling with the non-virtualized table.

  • Hide natural section header for the current sticky group so it can't slide past the anchor.
  • New useStickyGroupAnchor hook (scrollTop-based current-group tracking via public virtualizer.getOffsetForIndex).
  • Styling parity across both variants: color, letter-spacing, padding, 0.5px box-shadow.
  • Lock non-virtualized .stickySectionHeader height to 32px to match virtualized.
  • Stacking fix so column header's bottom border isn't covered by section header in grouped mode.

Routes

  • /examples/datatable — non-virtualized DataTable.
  • /examples/datatable-virtual — virtualized DataTable with grouping and infinite scroll.

Test plan

  • Virtualized + grouping: single label at top, natural section overlaps anchor on scroll (no slide-past).
  • Non-virtualized + grouping: section header height matches virtualized.
  • Both variants: column header bottom border visible at every scroll position.

`position: sticky` is bounded by its containing block. The sticky class was on the inner `VirtualHeaders` rowgroup, but the wrapping `<div ref={headerRef}>` (used for measurement) became the containing block and was only one header-row tall — so the header detached after ~one row of scroll.

Move `stickyHeader` onto the wrapper itself so its containing block becomes `.virtualTable` (full content height), restoring sticky behavior for the entire scroll range.
`.virtualSectionHeader` and `.stickyGroupAnchor` had no font-size rule
and inherited the document default (~16px), while the non-virtualized
variant renders group headers at small (~12px) via the `<table>` font
cascade. Added font-size and line-height tokens to both so they match.
- Hide natural section header for the current sticky group so it can't slide past the anchor.
- New `useStickyGroupAnchor` hook (scrollTop-based current-group tracking, replaces overscan-affected index logic).
- `groupHeaderHeight` default 32 — matches non-virtualized section header height.
- Styling parity across both variants: color, letter-spacing, padding, 0.5px box-shadow.
- Lock non-virtualized `.stickySectionHeader` height to 32px.
- z-index/stacking fixes so column header's bottom border isn't covered in grouped mode.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
apsara Ready Ready Preview, Comment May 7, 2026 0:16am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Warning

Rate limit exceeded

@paanSinghCoder has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 43 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 003ddd4a-832c-4ad0-8cd5-1fc0b39a6df3

📥 Commits

Reviewing files that changed from the base of the PR and between 1cdb835 and 49c67ec.

📒 Files selected for processing (6)
  • apps/www/src/app/examples/datatable-virtual/page.tsx
  • apps/www/src/app/examples/datatable/page.tsx
  • packages/raystack/components/data-table/components/virtualized-content.tsx
  • packages/raystack/components/data-table/data-table.module.css
  • packages/raystack/components/data-table/data-table.types.tsx
  • packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx
📝 Walkthrough

Walkthrough

This pull request adds a sticky group anchor feature to the virtualized data-table component. A new useStickyGroupAnchor hook detects scroll position and identifies which group header should remain anchored at the top. The VirtualizedContent component integrates this hook, introduces a groupHeaderHeight prop, and extends the VirtualRows API with a hiddenGroupIndex parameter to visually hide the anchored group header within the scrolling virtual list while preserving its layout. CSS updates establish proper z-index layering and adjust positioning for table headers, sticky elements, and the anchor element itself.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant VirtualizedContent
  participant useStickyGroupAnchor
  participant VirtualRows
  participant ScrollContainer

  Client->>VirtualizedContent: render table with groupHeaderList & virtualizer
  VirtualizedContent->>useStickyGroupAnchor: initialize hook(enabled, groupHeaderList, virtualizer, ref)
  ScrollContainer->>VirtualizedContent: scroll event
  VirtualizedContent->>useStickyGroupAnchor: recompute()
  useStickyGroupAnchor->>VirtualizedContent: stickyGroupIndex
  VirtualizedContent->>VirtualRows: render(hiddenGroupIndex=stickyGroupIndex)
  VirtualRows->>VirtualRows: hide matching virtual row (visibility:hidden, aria-hidden)
  VirtualizedContent->>Client: display anchored group via stickyGroupAnchor element
Loading

Suggested reviewers

  • rsbh
  • rohanchkrabrty
  • rohilsurana
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title mentions 'Group fixes for virtualized and non virtualized table', which accurately reflects the main objective of aligning group-anchor behavior and styling across both table variants while adding the useStickyGroupAnchor hook.
Description check ✅ Passed The PR description clearly describes the changeset's purpose and aligns with the actual code changes across all three modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx (1)

50-58: ⚡ Quick win

measurementsCache is an internal class field, not a documented public API

virtualizer.measurementsCache is initialized in the Virtualizer constructor as this.measurementsCache = [] but is not listed in the official TanStack Virtual v3 Virtualizer API surface. Reading it directly couples this hook to an implementation detail that could be renamed or made non-enumerable in a future minor release.

The public getVirtualItems() only returns the currently rendered window, which is insufficient here (group headers above the viewport won't appear). A safer alternative is to derive positions from the known estimateSize since group header heights are uniform and fixed (groupHeaderHeight):

// Instead of virtualizer.measurementsCache[groupHeaderList[i].index]
// derive the start offset from the virtualizer's public offset API:
const offset = virtualizer.getOffsetForIndex(groupHeaderList[i].index)?.[0];

Or accumulate positions from estimateSize at construction time (since group headers have a fixed height). Using measurementsCache works today but carries a breakage risk across @tanstack/react-virtual patch/minor bumps.

Does `@tanstack/react-virtual` v3 Virtualizer expose a public API to get the pixel offset/start position of a specific item index, such as getOffsetForIndex or similar?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx`
around lines 50 - 58, The loop in useStickyGroupAnchor.tsx reads the internal
virtualizer.measurementsCache (coupling to an internal API); replace that by
deriving item start offsets via the virtualizer public API or by computing
offsets from the known uniform groupHeaderHeight: inside the loop that iterates
groupHeaderList use virtualizer.getOffsetForIndex(groupHeaderList[i].index) to
obtain the start position (or compute start = index * groupHeaderHeight if you
track/derive sizes) instead of accessing virtualizer.measurementsCache, and keep
the same logic that compares start to scrollTop to set currentIdx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/raystack/components/data-table/data-table.module.css`:
- Around line 96-98: The z-index on .contentRoot thead is ineffective because
thead is not positioned; remove that rule and instead increase the z-index on
the positioned header cells so column headers stack above section headers—update
the .head rule (which already uses position: sticky) to a higher z-index (e.g.,
2) and ensure .stickySectionHeader keeps a lower z-index (e.g., 1) so header
cells render on top.

---

Nitpick comments:
In `@packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx`:
- Around line 50-58: The loop in useStickyGroupAnchor.tsx reads the internal
virtualizer.measurementsCache (coupling to an internal API); replace that by
deriving item start offsets via the virtualizer public API or by computing
offsets from the known uniform groupHeaderHeight: inside the loop that iterates
groupHeaderList use virtualizer.getOffsetForIndex(groupHeaderList[i].index) to
obtain the start position (or compute start = index * groupHeaderHeight if you
track/derive sizes) instead of accessing virtualizer.measurementsCache, and keep
the same logic that compares start to scrollTop to set currentIdx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1fe7b1e0-c8db-45a8-8dbc-58229eb88035

📥 Commits

Reviewing files that changed from the base of the PR and between 0ae3f82 and 25e8fb0.

📒 Files selected for processing (3)
  • packages/raystack/components/data-table/components/virtualized-content.tsx
  • packages/raystack/components/data-table/data-table.module.css
  • packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx

Comment on lines +96 to +98
.contentRoot thead {
z-index: 2;
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

z-index on a non-positioned thead is a no-op — the stacking fix won't take effect

z-index only works on positioned elements (position: absolute, relative, fixed, or sticky) and flex items. The thead element has no position property set in this stylesheet (nor anywhere that would apply to it), so the z-index of a non-positioned element has no effect.

The intended stacking fix must be applied to the actual positioned elements. The individual column-header cells via .head carry position: sticky; z-index: 1, and .stickySectionHeader also uses z-index: 1 — same stacking level. Without z-index, elements stack in the order they appear in the DOM (the lowest one down at the same hierarchy level appears on top), so section-header cells (later in DOM) would paint on top of column-header cells when they share z-index: 1.

The correct fix is to raise the column header cells' own z-index, not the container:

🛡️ Proposed fix
-.contentRoot thead {
-  z-index: 2;
-}
-

 .head {
   position: sticky;
   top: 0;
-  z-index: 1;
+  z-index: 2;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/data-table/data-table.module.css` around lines
96 - 98, The z-index on .contentRoot thead is ineffective because thead is not
positioned; remove that rule and instead increase the z-index on the positioned
header cells so column headers stack above section headers—update the .head rule
(which already uses position: sticky) to a higher z-index (e.g., 2) and ensure
.stickySectionHeader keeps a lower z-index (e.g., 1) so header cells render on
top.

…x not 28px)

Previously used `--rs-space-8` (28px) thinking it was 32px. Actual 32px token is `--rs-space-9`. Now the non-virtualized section header height matches the virtualized `groupHeaderHeight = 32` default, and the sticky offset (32.5px) lands flush at the column header's bottom edge.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/raystack/components/data-table/components/virtualized-content.tsx (2)

342-359: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

A11y: the currently anchored group is never announced.

The sticky anchor is aria-hidden='true' (correct, since it's a visual duplicate), and the natural in-flow group header at the same index is also rendered with aria-hidden='true' + visibility: 'hidden' (lines 119–124). For users opening the table, the very first group becomes the sticky one immediately, so its label/count is never reachable by assistive tech — only subsequent groups (which were briefly visible before being absorbed) have been announced.

Consider keeping the natural row exposed to AT (drop aria-hidden on the hidden in-flow row, or expose the sticky anchor instead) so the current group context is always announced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/data-table/components/virtualized-content.tsx`
around lines 342 - 359, The anchored group header is visually duplicated and
currently both the sticky anchor (in VirtualizedContent render:
stickyGroupHeader && isGrouped && stickyGroup) and its original in-flow group
header are hidden from assistive tech, so the first group is never announced;
fix by keeping the visual sticky anchor aria-hidden='true' but ensure the
in-flow group header (the original group row rendered elsewhere with
visibility:'hidden') is not aria-hidden — remove or toggle aria-hidden on that
in-flow group header so screen readers can announce the current group
label/count (or alternatively mark the sticky anchor as exposed and remove
aria-hidden there), updating the code paths that render the in-flow group header
and the sticky anchor (stickyGroup, stickyGroup.showGroupCount,
styles.stickyGroupAnchor) accordingly.

285-300: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

recomputeStickyGroup calls setState on every scroll event regardless of whether the active group changed.

handleVirtualScroll fires on every scroll tick and unconditionally calls recomputeStickyGroup() when stickyGroupHeader is enabled. The hook's recompute function performs a linear scan through groupHeaderList and always updates setStickyGroup and setStickyGroupIndex, even when the computed group hasn't actually changed. This causes unnecessary re-renders. Add an early return or compare against the previous group state before updating. Additionally, recompute's dependencies (groupHeaderList, virtualizer) may change frequently, causing the callback identity to shift and handleVirtualScroll to be recreated as a result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/data-table/components/virtualized-content.tsx`
around lines 285 - 300, handleVirtualScroll currently calls recomputeStickyGroup
on every scroll tick which triggers setStickyGroup/setStickyGroupIndex
unconditionally; modify recomputeStickyGroup (the function referenced in this
file) to compute the new group and compare it with the previous group/index
(store previous in a ref) and return early without calling setState if nothing
changed, and then memoize recomputeStickyGroup with useCallback (or keep a
stable ref wrapper) so its identity doesn't change when
groupHeaderList/virtualizer frequently update; this prevents unnecessary
re-renders and avoids recreating handleVirtualScroll on each dependency change.
🧹 Nitpick comments (2)
packages/raystack/components/data-table/components/virtualized-content.tsx (2)

66-82: 💤 Low value

Confirm spread props can't override critical attributes.

Spreading ...rest after role='row' and className is fine for the current call site, which passes only aria-hidden and style. Just be aware that any future caller passing role, className, or onClick will silently override these. If you want to harden this, place spread before the fixed attributes (or whitelist accepted props).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/data-table/components/virtualized-content.tsx`
around lines 66 - 82, Spread props are currently applied after fixed attributes
in VirtualGroupHeader, allowing callers to override role/className/onClick;
change this by either spreading {...rest} before the fixed attributes or by
explicitly extracting and sanitizing props (e.g., const { role, className,
onClick, ...safeRest } = rest) then using safeRest while merging className with
styles.virtualSectionHeader and hardcoding role='row' to prevent overrides;
update VirtualGroupHeader to enforce role and merge className so callers can
pass aria-hidden/style but cannot replace role/className/onClick.

272-283: 💤 Low value

anchorPixelHeight and virtualizer estimateSize duplicate the same fallback expression.

Both this line and estimateSize (line 267) compute groupHeaderHeight ?? rowHeight for group rows. Extracting a single groupHeaderPixelHeight const and reusing it in both places (and passing it to the hook if it needs it) keeps them from drifting if the fallback rule ever changes.

♻️ Suggested extraction
+  const groupHeaderPixelHeight = groupHeaderHeight ?? rowHeight;
+
   const virtualizer = useVirtualizer({
     count: rows.length,
     getScrollElement: () => scrollContainerRef.current,
     estimateSize: index => {
       const row = rows[index];
       const isGroupHeader = row?.subRows && row.subRows.length > 0;
-      return isGroupHeader ? (groupHeaderHeight ?? rowHeight) : rowHeight;
+      return isGroupHeader ? groupHeaderPixelHeight : rowHeight;
     },
     overscan
   });
-
-  const anchorPixelHeight = groupHeaderHeight ?? rowHeight;
+  const anchorPixelHeight = groupHeaderPixelHeight;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/data-table/components/virtualized-content.tsx`
around lines 272 - 283, anchorPixelHeight and the virtualizer's estimateSize
both compute the same fallback (groupHeaderHeight ?? rowHeight); extract a
single constant (e.g., groupHeaderPixelHeight) assigned to groupHeaderHeight ??
rowHeight and use that constant everywhere instead of repeating the expression —
update anchorPixelHeight to reference groupHeaderPixelHeight, update the
estimateSize logic to reference groupHeaderPixelHeight, and if
useStickyGroupAnchor requires the value, pass groupHeaderPixelHeight into its
props; ensure you replace all occurrences of the duplicated expression in
virtualized-content.tsx (including references around anchorPixelHeight,
estimateSize, and the useStickyGroupAnchor call).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/raystack/components/data-table/components/virtualized-content.tsx`:
- Around line 342-359: The anchored group header is visually duplicated and
currently both the sticky anchor (in VirtualizedContent render:
stickyGroupHeader && isGrouped && stickyGroup) and its original in-flow group
header are hidden from assistive tech, so the first group is never announced;
fix by keeping the visual sticky anchor aria-hidden='true' but ensure the
in-flow group header (the original group row rendered elsewhere with
visibility:'hidden') is not aria-hidden — remove or toggle aria-hidden on that
in-flow group header so screen readers can announce the current group
label/count (or alternatively mark the sticky anchor as exposed and remove
aria-hidden there), updating the code paths that render the in-flow group header
and the sticky anchor (stickyGroup, stickyGroup.showGroupCount,
styles.stickyGroupAnchor) accordingly.
- Around line 285-300: handleVirtualScroll currently calls recomputeStickyGroup
on every scroll tick which triggers setStickyGroup/setStickyGroupIndex
unconditionally; modify recomputeStickyGroup (the function referenced in this
file) to compute the new group and compare it with the previous group/index
(store previous in a ref) and return early without calling setState if nothing
changed, and then memoize recomputeStickyGroup with useCallback (or keep a
stable ref wrapper) so its identity doesn't change when
groupHeaderList/virtualizer frequently update; this prevents unnecessary
re-renders and avoids recreating handleVirtualScroll on each dependency change.

---

Nitpick comments:
In `@packages/raystack/components/data-table/components/virtualized-content.tsx`:
- Around line 66-82: Spread props are currently applied after fixed attributes
in VirtualGroupHeader, allowing callers to override role/className/onClick;
change this by either spreading {...rest} before the fixed attributes or by
explicitly extracting and sanitizing props (e.g., const { role, className,
onClick, ...safeRest } = rest) then using safeRest while merging className with
styles.virtualSectionHeader and hardcoding role='row' to prevent overrides;
update VirtualGroupHeader to enforce role and merge className so callers can
pass aria-hidden/style but cannot replace role/className/onClick.
- Around line 272-283: anchorPixelHeight and the virtualizer's estimateSize both
compute the same fallback (groupHeaderHeight ?? rowHeight); extract a single
constant (e.g., groupHeaderPixelHeight) assigned to groupHeaderHeight ??
rowHeight and use that constant everywhere instead of repeating the expression —
update anchorPixelHeight to reference groupHeaderPixelHeight, update the
estimateSize logic to reference groupHeaderPixelHeight, and if
useStickyGroupAnchor requires the value, pass groupHeaderPixelHeight into its
props; ensure you replace all occurrences of the duplicated expression in
virtualized-content.tsx (including references around anchorPixelHeight,
estimateSize, and the useStickyGroupAnchor call).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e63d8c00-a190-4a92-b8d7-9a6bd1136be9

📥 Commits

Reviewing files that changed from the base of the PR and between 25e8fb0 and 1cdb835.

📒 Files selected for processing (3)
  • packages/raystack/components/data-table/components/virtualized-content.tsx
  • packages/raystack/components/data-table/data-table.module.css
  • packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx
  • packages/raystack/components/data-table/data-table.module.css

@paanSinghCoder paanSinghCoder changed the title fix(data-table): align virtualized group anchor with non-virtualized fix(data-table): Group fixes for virtualized and non virtualized table May 7, 2026
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.

1 participant