Skip to content

feat (DataTable): add anchored header for groups#686

Open
paanSinghCoder wants to merge 2 commits intomainfrom
feat/anchored-group-header
Open

feat (DataTable): add anchored header for groups#686
paanSinghCoder wants to merge 2 commits intomainfrom
feat/anchored-group-header

Conversation

@paanSinghCoder
Copy link
Contributor

@paanSinghCoder paanSinghCoder commented Mar 10, 2026

Description

feat (data-table): add anchored group header. Toggled with stickyGroupHeader. Default false

Test it here: https://apsara-git-feat-anchored-group-header-raystack.vercel.app/examples/datatable

Screen.Recording.2026-03-10.at.4.36.37.PM.mov

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactor (no functional changes, no bug fixes just code improvements)
  • Chore (changes to the build process or auxiliary tools and libraries such as documentation generation)
  • Style (changes that do not affect the meaning of the code (white-space, formatting, etc))
  • Test (adding missing tests or correcting existing tests)
  • Improvement (Improvements to existing code)
  • Other (please specify)

How Has This Been Tested?

[Describe the tests that you ran to verify your changes]

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (.mdx files)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works

Screenshots (if appropriate):

[Add screenshots here]

Related Issues

[Link any related issues here using #issue-number]

Summary by CodeRabbit

  • New Features

    • Sticky group header for DataTable (anchor group labels under the table header when grouped).
    • New example page demonstrating an infinite-scroll DataTable with grouping.
  • Documentation

    • Added docs and usage examples for the sticky group header feature.
  • Tests

    • Added tests validating sticky group header behavior.
  • Bug Fixes

    • Fixed ARIA attribute on sidebar item icon for correct DOM accessibility.

@vercel
Copy link

vercel bot commented Mar 10, 2026

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

Project Deployment Actions Updated (UTC)
apsara Ready Ready Preview, Comment Mar 10, 2026 2:36pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4ba0eab0-6c56-4c42-9e71-255bc8f591e0

📥 Commits

Reviewing files that changed from the base of the PR and between c931f21 and a513165.

📒 Files selected for processing (1)
  • packages/raystack/components/sidebar/sidebar-item.tsx

📝 Walkthrough

Walkthrough

Adds a new stickyGroupHeader DataTable prop and implements sticky group-label behavior across non-virtualized and virtualized renderers, updates CSS, docs, tests, and an example infinite-scroll page; also includes a minor ARIA attribute fix in the sidebar item.

Changes

Cohort / File(s) Summary
Example Page
apps/www/src/app/examples/datatable/page.tsx
New client-side page demonstrating an infinite-scroll (server mode) DataTable with sample data and a loadMore simulation (500ms delay, PAGE_SIZE/MAX_ROWS controls).
Documentation
apps/www/src/content/docs/components/datatable/index.mdx, apps/www/src/content/docs/components/datatable/props.ts
Added docs and prop entry for stickyGroupHeader with usage examples showing how to anchor the current group label under the header during scroll.
Type Definitions
packages/raystack/components/data-table/data-table.types.tsx
Added stickyGroupHeader?: boolean to DataTableProps and TableContextType.
Core Component
packages/raystack/components/data-table/data-table.tsx
Exposed stickyGroupHeader on DataTableRoot props and threaded it into the table context value.
Content Renderer
packages/raystack/components/data-table/components/content.tsx
Added stickyGroupHeader prop to Rows and GroupHeader, applied stickySectionHeader class to non-virtual group headers when enabled.
Virtualized Renderer
packages/raystack/components/data-table/components/virtualized-content.tsx
Implemented sticky-group tracking and rendering: header measurement, scroll handling (updateStickyGroup), a fixed-position anchor row for the active group, and related state.
Styling
packages/raystack/components/data-table/data-table.module.css
Added .stickyGroupAnchor and .stickySectionHeader styles and small formatting tweaks.
Tests
packages/raystack/components/data-table/__tests__/data-table.test.tsx
Added tests verifying stickySectionHeader class presence when stickyGroupHeader is enabled and absence by default.
Misc
packages/raystack/components/sidebar/sidebar-item.tsx
Changed icon prop ARIA attribute from ariaHidden to aria-hidden (attribute naming fix).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant DataTable as DataTableRoot
participant Virtualized as VirtualizedContent
participant Header as TableHeader
participant Scroll as ScrollContainer

User->>Scroll: scroll event
Scroll->>Virtualized: handleVirtualScroll()
Virtualized->>Virtualized: compute visible rows, find current group
Virtualized->>Header: measure headerHeight (useLayoutEffect)
Virtualized->>DataTable: update stickyGroup state
DataTable->>Virtualized: render sticky anchor (fixed position) with group label
Virtualized->>User: anchored group header remains visible

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

Do not merge

Suggested reviewers

  • rsbh
  • rohilsurana

Poem

🐰 I hopped through rows and grouped the throng,

Anchored titles hum a sticky song,
Virtual winds may scoot and swirl,
But group labels cling — behold the pearl! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main feature: adding an anchored (sticky) group header to the DataTable component, which aligns with the primary changes across all modified files.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/anchored-group-header

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.

@paanSinghCoder paanSinghCoder changed the title feat: add anchored header for groups feat (DataTable): add anchored header for groups Mar 10, 2026
Copy link
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 (4)
packages/raystack/components/data-table/data-table.module.css (1)

172-179: Fixed header height assumption may be fragile.

The top: var(--rs-space-10) assumes a fixed table header height. If the header height varies (e.g., multi-line headers, different font sizes), the sticky section header may not align correctly under it. Consider whether this could be made more dynamic or documented as a constraint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/data-table/data-table.module.css` around lines
172 - 179, .stickySectionHeader currently uses a fixed top: var(--rs-space-10)
which breaks if the table header height changes; update the approach so the
sticky section uses a dynamic header height: either introduce and use a
dedicated CSS custom property (e.g. top: var(--table-header-height,
var(--rs-space-10))) and ensure the table header sets --table-header-height to
its computed height, or add a small script that measures the actual header
element height (querySelector for the table header element) and sets the
.stickySectionHeader style.top (or document.documentElement
style.setProperty('--table-header-height', `${height}px`)) on mount and window
resize; reference .stickySectionHeader and the new --table-header-height
variable (or the header measurement logic) when applying the change.
packages/raystack/components/data-table/components/virtualized-content.tsx (3)

341-354: Consider extracting duplicated group label rendering.

The sticky anchor content (lines 347-352) duplicates the rendering logic from VirtualGroupHeader (lines 74-78). Consider extracting the shared label+badge rendering into a reusable component:

♻️ Suggested extraction
// Extract shared rendering
function GroupLabelContent({ data }: { data: GroupedData<unknown> }) {
  return (
    <Flex gap={3} align='center'>
      {data.label}
      {data.showGroupCount ? (
        <Badge variant='neutral'>{data.count}</Badge>
      ) : null}
    </Flex>
  );
}

// Then use in both places:
// In VirtualGroupHeader:
<div role='row' className={styles.virtualSectionHeader} style={style}>
  <GroupLabelContent data={data} />
</div>

// In sticky anchor:
{stickyGroupHeader && isGrouped && stickyGroup && (
  <div role='row' className={styles.stickyGroupAnchor} style={{ top: headerHeight }}>
    <GroupLabelContent data={stickyGroup} />
  </div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/data-table/components/virtualized-content.tsx`
around lines 341 - 354, The sticky group anchor duplicates the label+badge
render logic found in VirtualGroupHeader; extract that shared markup into a
small presentational component (e.g., GroupLabelContent) that accepts the
grouped data shape and renders the label and optional Badge, then replace the
inline JSX in both VirtualGroupHeader (use GroupLabelContent with the existing
data prop) and the sticky anchor block (use GroupLabelContent with stickyGroup)
so both places reference the same component and remove the duplicated code.

267-278: Performance: virtualizer in dependency array causes callback recreation on every render.

useVirtualizer returns a new object reference each render, so including virtualizer in the dependency array means updateStickyGroup is recreated every render. This cascades to handleVirtualScroll and the useLayoutEffect at line 305-307, causing unnecessary work.

Consider reading the virtualizer via a ref or accessing getVirtualItems() directly within the scroll handler without memoizing this function separately:

♻️ Suggested refactor using ref pattern
+  const virtualizerRef = useRef(virtualizer);
+  virtualizerRef.current = virtualizer;
+
   const updateStickyGroup = useCallback(() => {
     if (!stickyGroupHeader || !isGrouped || groupHeaderList.length === 0) {
       setStickyGroup(null);
       return;
     }
-    const items = virtualizer.getVirtualItems();
+    const items = virtualizerRef.current.getVirtualItems();
     const firstIndex = items[0]?.index ?? 0;
     const current = groupHeaderList
       .filter(g => g.index <= firstIndex)
       .pop()?.data;
     setStickyGroup(current ?? null);
-  }, [stickyGroupHeader, isGrouped, groupHeaderList, virtualizer]);
+  }, [stickyGroupHeader, isGrouped, groupHeaderList]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/data-table/components/virtualized-content.tsx`
around lines 267 - 278, The callback updateStickyGroup is being recreated every
render because virtualizer (from useVirtualizer) is a new object reference each
time; remove virtualizer from the dependency array and instead read
virtualizer.getVirtualItems() via a stable ref or directly inside the scroll
handler so updateStickyGroup only depends on stable values (stickyGroupHeader,
isGrouped, groupHeaderList). Concretely: create a ref (e.g., virtualizerRef) and
assign the virtualizer to it after initialization, update updateStickyGroup to
call virtualizerRef.current?.getVirtualItems() rather than referencing
virtualizer in the closure, and update any consumers (handleVirtualScroll and
the useLayoutEffect) to use the ref so the callbacks are no longer recreated
each render.

305-307: Minor: Redundant dependencies in effect.

groupHeaderList and isGrouped are already captured in updateStickyGroup's dependency array, making them redundant here. If the callback recreation issue from the previous comment is addressed, you can simplify:

✨ Suggested simplification
   useLayoutEffect(() => {
     if (stickyGroupHeader) updateStickyGroup();
-  }, [stickyGroupHeader, updateStickyGroup, groupHeaderList, isGrouped]);
+  }, [stickyGroupHeader, updateStickyGroup]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/data-table/components/virtualized-content.tsx`
around lines 305 - 307, The useLayoutEffect dependency array includes redundant
entries: remove groupHeaderList and isGrouped from the dependencies since they
are already captured by the updateStickyGroup callback; keep stickyGroupHeader
and updateStickyGroup in the useLayoutEffect dependency list (and ensure
updateStickyGroup's own dependencies correctly include groupHeaderList and
isGrouped) so the effect only runs when stickyGroupHeader or the callback
changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/www/src/content/docs/components/datatable/props.ts`:
- Around line 39-43: Update the JSDoc for the grouping sticky-label prop in
apps/www/src/content/docs/components/datatable/props.ts so the human-readable
sentence matches the actual default: remove or correct the "(default)" token in
the phrase "When true (default)" and instead state the behavior with the
true/false semantics and keep the `@defaultValue` false unchanged; ensure the
description reads something like "When true, the current group label sticks
under the table header while scrolling (anchor group title). `@defaultValue`
false" so docs and implementation align.

---

Nitpick comments:
In `@packages/raystack/components/data-table/components/virtualized-content.tsx`:
- Around line 341-354: The sticky group anchor duplicates the label+badge render
logic found in VirtualGroupHeader; extract that shared markup into a small
presentational component (e.g., GroupLabelContent) that accepts the grouped data
shape and renders the label and optional Badge, then replace the inline JSX in
both VirtualGroupHeader (use GroupLabelContent with the existing data prop) and
the sticky anchor block (use GroupLabelContent with stickyGroup) so both places
reference the same component and remove the duplicated code.
- Around line 267-278: The callback updateStickyGroup is being recreated every
render because virtualizer (from useVirtualizer) is a new object reference each
time; remove virtualizer from the dependency array and instead read
virtualizer.getVirtualItems() via a stable ref or directly inside the scroll
handler so updateStickyGroup only depends on stable values (stickyGroupHeader,
isGrouped, groupHeaderList). Concretely: create a ref (e.g., virtualizerRef) and
assign the virtualizer to it after initialization, update updateStickyGroup to
call virtualizerRef.current?.getVirtualItems() rather than referencing
virtualizer in the closure, and update any consumers (handleVirtualScroll and
the useLayoutEffect) to use the ref so the callbacks are no longer recreated
each render.
- Around line 305-307: The useLayoutEffect dependency array includes redundant
entries: remove groupHeaderList and isGrouped from the dependencies since they
are already captured by the updateStickyGroup callback; keep stickyGroupHeader
and updateStickyGroup in the useLayoutEffect dependency list (and ensure
updateStickyGroup's own dependencies correctly include groupHeaderList and
isGrouped) so the effect only runs when stickyGroupHeader or the callback
changes.

In `@packages/raystack/components/data-table/data-table.module.css`:
- Around line 172-179: .stickySectionHeader currently uses a fixed top:
var(--rs-space-10) which breaks if the table header height changes; update the
approach so the sticky section uses a dynamic header height: either introduce
and use a dedicated CSS custom property (e.g. top: var(--table-header-height,
var(--rs-space-10))) and ensure the table header sets --table-header-height to
its computed height, or add a small script that measures the actual header
element height (querySelector for the table header element) and sets the
.stickySectionHeader style.top (or document.documentElement
style.setProperty('--table-header-height', `${height}px`)) on mount and window
resize; reference .stickySectionHeader and the new --table-header-height
variable (or the header measurement logic) when applying the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb45d7be-42cc-4ee9-9ce5-155e6f27122c

📥 Commits

Reviewing files that changed from the base of the PR and between 14de0ba and c931f21.

📒 Files selected for processing (9)
  • apps/www/src/app/examples/datatable/page.tsx
  • apps/www/src/content/docs/components/datatable/index.mdx
  • apps/www/src/content/docs/components/datatable/props.ts
  • packages/raystack/components/data-table/__tests__/data-table.test.tsx
  • packages/raystack/components/data-table/components/content.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.tsx
  • packages/raystack/components/data-table/data-table.types.tsx

Comment on lines +39 to +43
/**
* When true (default), the current group label sticks under the table header while scrolling (anchor group title).
* Applies to both Content and VirtualizedContent when grouping is enabled.
* @defaultValue false
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Documentation inconsistency: description contradicts default value.

Line 40 says "When true (default)" but the @defaultValue on line 42 says false. The implementation in data-table.tsx confirms the default is false, so the description should be corrected.

📝 Proposed fix
   /**
-   * When true (default), the current group label sticks under the table header while scrolling (anchor group title).
+   * When true, the current group label sticks under the table header while scrolling (anchor group title).
    * Applies to both Content and VirtualizedContent when grouping is enabled.
    * `@defaultValue` false
    */
   stickyGroupHeader?: boolean;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* When true (default), the current group label sticks under the table header while scrolling (anchor group title).
* Applies to both Content and VirtualizedContent when grouping is enabled.
* @defaultValue false
*/
/**
* When true, the current group label sticks under the table header while scrolling (anchor group title).
* Applies to both Content and VirtualizedContent when grouping is enabled.
* `@defaultValue` false
*/
stickyGroupHeader?: boolean;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/datatable/props.ts` around lines 39 -
43, Update the JSDoc for the grouping sticky-label prop in
apps/www/src/content/docs/components/datatable/props.ts so the human-readable
sentence matches the actual default: remove or correct the "(default)" token in
the phrase "When true (default)" and instead state the behavior with the
true/false semantics and keep the `@defaultValue` false unchanged; ensure the
description reads something like "When true, the current group label sticks
under the table header while scrolling (anchor group title). `@defaultValue`
false" so docs and implementation align.

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