From 9477979d5399c865a022666e4249b9c8dea7bb1e Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Wed, 6 May 2026 08:25:09 +0530 Subject: [PATCH 01/16] fix(data-table): keep virtualized header sticky across full scroll range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `position: sticky` is bounded by its containing block. The sticky class was on the inner `VirtualHeaders` rowgroup, but the wrapping `
` (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. --- .../data-table/components/virtualized-content.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/raystack/components/data-table/components/virtualized-content.tsx b/packages/raystack/components/data-table/components/virtualized-content.tsx index d6f3b5a9b..a4fdd9abb 100644 --- a/packages/raystack/components/data-table/components/virtualized-content.tsx +++ b/packages/raystack/components/data-table/components/virtualized-content.tsx @@ -332,11 +332,11 @@ export function VirtualizedContent({ onScroll={handleVirtualScroll} >
-
- +
+
{stickyGroupHeader && isGrouped && stickyGroup && (
Date: Wed, 6 May 2026 14:26:36 +0530 Subject: [PATCH 02/16] fix(data-table): adjust padding, height, and margin for improved header layout --- .../raystack/components/data-table/data-table.module.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/raystack/components/data-table/data-table.module.css b/packages/raystack/components/data-table/data-table.module.css index 187b2ca48..810729899 100644 --- a/packages/raystack/components/data-table/data-table.module.css +++ b/packages/raystack/components/data-table/data-table.module.css @@ -186,7 +186,10 @@ align-items: center; background: var(--rs-color-background-base-secondary); font-weight: var(--rs-font-weight-medium); - padding: var(--rs-space-3); + padding: 0 var(--rs-space-3); + height: var(--rs-space-10); + margin-bottom: calc(-1 * var(--rs-space-10)); + box-sizing: border-box; border-bottom: 0.5px solid var(--rs-color-border-base-primary); box-shadow: 0 1px 0 0 var(--rs-color-border-base-primary); } @@ -202,7 +205,7 @@ /* Non-virtualized: sticky section header under table header */ .stickySectionHeader { position: sticky; - top: var(--rs-space-10); + top: var(--rs-space-8); z-index: 1; background: var(--rs-color-background-base-secondary); box-shadow: 0 1px 0 0 var(--rs-color-border-base-primary); From 3f40e2674f316103af6c3604d253a7d50a577683 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 7 May 2026 08:44:54 +0530 Subject: [PATCH 03/16] fix(data-table): add styles to ensure proper table layout with separate border-collapse and bottom border --- .../raystack/components/data-table/data-table.module.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/raystack/components/data-table/data-table.module.css b/packages/raystack/components/data-table/data-table.module.css index 810729899..f82aa12bd 100644 --- a/packages/raystack/components/data-table/data-table.module.css +++ b/packages/raystack/components/data-table/data-table.module.css @@ -33,7 +33,7 @@ min-width: 0; } -.display-popover-properties-select > span { +.display-popover-properties-select>span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -83,6 +83,11 @@ overflow: auto; } +.contentRoot table { + border-collapse: separate; + border-spacing: 0; +} + .row { background: var(--rs-color-background-base-primary); } @@ -209,4 +214,4 @@ z-index: 1; background: var(--rs-color-background-base-secondary); box-shadow: 0 1px 0 0 var(--rs-color-border-base-primary); -} +} \ No newline at end of file From de56f78d44b34f9dd3a333389c5f6ffb18313c86 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 7 May 2026 11:03:40 +0530 Subject: [PATCH 04/16] fix(data-table): use small font size for virtualized group headers `.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 `` font cascade. Added font-size and line-height tokens to both so they match. --- packages/raystack/components/data-table/data-table.module.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/raystack/components/data-table/data-table.module.css b/packages/raystack/components/data-table/data-table.module.css index f82aa12bd..a9ca29273 100644 --- a/packages/raystack/components/data-table/data-table.module.css +++ b/packages/raystack/components/data-table/data-table.module.css @@ -179,7 +179,9 @@ display: flex; align-items: center; background: var(--rs-color-background-base-secondary); + font-size: var(--rs-font-size-small); font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-small); padding: var(--rs-space-3); } @@ -190,7 +192,9 @@ display: flex; align-items: center; background: var(--rs-color-background-base-secondary); + font-size: var(--rs-font-size-small); font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-small); padding: 0 var(--rs-space-3); height: var(--rs-space-10); margin-bottom: calc(-1 * var(--rs-space-10)); From 25e8fb0e673a4cde669cf1e12e5308559d06e1ca Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 7 May 2026 14:28:07 +0530 Subject: [PATCH 05/16] fix(data-table): align virtualized group anchor with non-virtualized MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .../components/virtualized-content.tsx | 60 ++++++++------- .../data-table/data-table.module.css | 29 ++++--- .../data-table/hooks/useStickyGroupAnchor.tsx | 76 +++++++++++++++++++ 3 files changed, 129 insertions(+), 36 deletions(-) create mode 100644 packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx diff --git a/packages/raystack/components/data-table/components/virtualized-content.tsx b/packages/raystack/components/data-table/components/virtualized-content.tsx index 3dd4ff55f..902525249 100644 --- a/packages/raystack/components/data-table/components/virtualized-content.tsx +++ b/packages/raystack/components/data-table/components/virtualized-content.tsx @@ -19,6 +19,7 @@ import { VirtualizedContentProps } from '../data-table.types'; import { useDataTable } from '../hooks/useDataTable'; +import { useStickyGroupAnchor } from '../hooks/useStickyGroupAnchor'; import { hasActiveQuery } from '../utils'; function VirtualHeaders({ @@ -85,16 +86,18 @@ function VirtualRows({ rows, virtualizer, onRowClick, - classNames + classNames, + hiddenGroupIndex }: { rows: Row[]; virtualizer: ReturnType; onRowClick?: (row: TData) => void; classNames?: { row?: string }; + hiddenGroupIndex?: number | null; }) { const items = virtualizer.getVirtualItems(); - return items.map((item, idx) => { + return items.map(item => { const row = rows[item.index]; if (!row) return null; @@ -109,11 +112,16 @@ function VirtualRows({ }; if (isGroupHeader) { + const isHidden = item.index === hiddenGroupIndex; return ( } - style={positionStyle} + style={ + isHidden + ? { ...positionStyle, visibility: 'hidden' } + : positionStyle + } /> ); } @@ -209,7 +217,7 @@ const DefaultEmptyComponent = () => ( export function VirtualizedContent({ rowHeight = 40, - groupHeaderHeight, + groupHeaderHeight = 32, overscan = 5, loadMoreOffset = 100, emptyState, @@ -233,9 +241,6 @@ export function VirtualizedContent({ const scrollContainerRef = useRef(null); const headerRef = useRef(null); - const [stickyGroup, setStickyGroup] = useState | null>( - null - ); const [headerHeight, setHeaderHeight] = useState(40); const groupBy = tableQuery?.group_by?.[0]; @@ -259,28 +264,28 @@ export function VirtualizedContent({ estimateSize: index => { const row = rows[index]; const isGroupHeader = row?.subRows && row.subRows.length > 0; - return isGroupHeader ? (groupHeaderHeight ?? rowHeight) : rowHeight; + return isGroupHeader ? groupHeaderHeight : rowHeight; }, overscan }); - const updateStickyGroup = useCallback(() => { - if (!stickyGroupHeader || !isGrouped || groupHeaderList.length === 0) { - setStickyGroup(null); - return; - } - const items = virtualizer.getVirtualItems(); - const firstIndex = items[0]?.index ?? 0; - const current = groupHeaderList - .filter(g => g.index <= firstIndex) - .pop()?.data; - setStickyGroup(current ?? null); - }, [stickyGroupHeader, isGrouped, groupHeaderList, virtualizer]); + const anchorPixelHeight = groupHeaderHeight; + + const { + stickyGroup, + stickyGroupIndex, + recompute: recomputeStickyGroup + } = useStickyGroupAnchor({ + enabled: stickyGroupHeader && isGrouped, + groupHeaderList, + virtualizer, + scrollContainerRef + }); const handleVirtualScroll = useCallback(() => { const el = scrollContainerRef.current; if (!el) return; - if (stickyGroupHeader) updateStickyGroup(); + if (stickyGroupHeader) recomputeStickyGroup(); if (isLoading) return; const { scrollTop, scrollHeight, clientHeight } = el; if (scrollHeight - scrollTop - clientHeight < loadMoreOffset) { @@ -291,7 +296,7 @@ export function VirtualizedContent({ isLoading, loadMoreData, loadMoreOffset, - updateStickyGroup + recomputeStickyGroup ]); const totalHeight = virtualizer.getTotalSize(); @@ -302,10 +307,6 @@ export function VirtualizedContent({ } }, [headerGroups]); - useLayoutEffect(() => { - if (stickyGroupHeader) updateStickyGroup(); - }, [stickyGroupHeader, updateStickyGroup, groupHeaderList, isGrouped]); - const hasData = rows?.length > 0 || isLoading; const hasChanges = hasActiveQuery(tableQuery || {}, defaultSort); @@ -342,7 +343,11 @@ export function VirtualizedContent({
{stickyGroup.label} @@ -364,6 +369,7 @@ export function VirtualizedContent({ classNames={{ row: classNames.row }} + hiddenGroupIndex={stickyGroupIndex} />
diff --git a/packages/raystack/components/data-table/data-table.module.css b/packages/raystack/components/data-table/data-table.module.css index 46a7a123e..85c4d0d7b 100644 --- a/packages/raystack/components/data-table/data-table.module.css +++ b/packages/raystack/components/data-table/data-table.module.css @@ -93,6 +93,10 @@ border-spacing: 0; } +.contentRoot thead { + z-index: 2; +} + .row { background: var(--rs-color-background-base-primary); } @@ -131,7 +135,7 @@ .stickyHeader { position: sticky; top: 0; - z-index: 1; + z-index: 2; background: var(--rs-color-background-base-primary); } @@ -181,13 +185,18 @@ position: absolute; width: 100%; left: 0; + z-index: 1; display: flex; align-items: center; background: var(--rs-color-background-base-secondary); + color: var(--rs-color-foreground-base-primary); font-size: var(--rs-font-size-small); font-weight: var(--rs-font-weight-medium); line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); padding: var(--rs-space-3); + box-sizing: border-box; + box-shadow: 0 0.5px 0 0 var(--rs-color-border-base-primary); } /* Sticky group anchor: shows current group label while scrolling (virtualized) */ @@ -197,15 +206,14 @@ display: flex; align-items: center; background: var(--rs-color-background-base-secondary); + color: var(--rs-color-foreground-base-primary); font-size: var(--rs-font-size-small); font-weight: var(--rs-font-weight-medium); line-height: var(--rs-line-height-small); - padding: 0 var(--rs-space-3); - height: var(--rs-space-10); - margin-bottom: calc(-1 * var(--rs-space-10)); + letter-spacing: var(--rs-letter-spacing-small); + padding: var(--rs-space-3); box-sizing: border-box; - border-bottom: 0.5px solid var(--rs-color-border-base-primary); - box-shadow: 0 1px 0 0 var(--rs-color-border-base-primary); + box-shadow: 0 0.5px 0 0 var(--rs-color-border-base-primary); } .loaderContainer { @@ -217,10 +225,13 @@ } /* Non-virtualized: sticky section header under table header */ -.stickySectionHeader { +.contentRoot .stickySectionHeader { position: sticky; - top: var(--rs-space-8); + top: calc(var(--rs-space-8) + 0.5px); z-index: 1; + height: var(--rs-space-8); + padding: var(--rs-space-3); + box-sizing: border-box; background: var(--rs-color-background-base-secondary); - box-shadow: 0 1px 0 0 var(--rs-color-border-base-primary); + box-shadow: 0 0.5px 0 0 var(--rs-color-border-base-primary); } \ No newline at end of file diff --git a/packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx b/packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx new file mode 100644 index 000000000..2ab618a55 --- /dev/null +++ b/packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx @@ -0,0 +1,76 @@ +import type { Virtualizer } from '@tanstack/react-virtual'; +import { useCallback, useLayoutEffect, useState } from 'react'; +import { GroupedData } from '../data-table.types'; + +interface UseStickyGroupAnchorParams { + enabled: boolean; + groupHeaderList: { index: number; data: GroupedData }[]; + virtualizer: Virtualizer; + scrollContainerRef: React.RefObject; +} + +interface UseStickyGroupAnchorResult { + stickyGroup: GroupedData | null; + stickyGroupIndex: number | null; + recompute: () => void; +} + +/** + * Tracks the active group for the virtualized sticky group anchor. + * + * Picks the latest group whose measured offset has been scrolled past, so the + * anchor's content stays in sync with the natural section header underneath. + * + * Returns the current group's data plus the row index of its natural section + * header — the consumer hides that row in the virtualized body so the natural + * header doesn't visually slide past the anchor (matching the non-virtualized + * table where CSS sticky pins each section header at the offset). + */ +export function useStickyGroupAnchor({ + enabled, + groupHeaderList, + virtualizer, + scrollContainerRef +}: UseStickyGroupAnchorParams): UseStickyGroupAnchorResult { + const [stickyGroup, setStickyGroup] = useState | null>( + null + ); + const [stickyGroupIndex, setStickyGroupIndex] = useState(null); + + const recompute = useCallback(() => { + if (!enabled || groupHeaderList.length === 0) { + setStickyGroup(null); + setStickyGroupIndex(null); + return; + } + const el = scrollContainerRef.current; + if (!el) return; + const scrollTop = el.scrollTop; + + let currentIdx = -1; + for (let i = 0; i < groupHeaderList.length; i++) { + const m = virtualizer.measurementsCache[groupHeaderList[i].index]; + if (!m) continue; + if (m.start <= scrollTop) { + currentIdx = i; + } else { + break; + } + } + + if (currentIdx < 0) { + setStickyGroup(null); + setStickyGroupIndex(null); + return; + } + + setStickyGroup(groupHeaderList[currentIdx].data); + setStickyGroupIndex(groupHeaderList[currentIdx].index); + }, [enabled, groupHeaderList, virtualizer, scrollContainerRef]); + + useLayoutEffect(() => { + recompute(); + }, [recompute]); + + return { stickyGroup, stickyGroupIndex, recompute }; +} From 97ab38df89190118247ace9b351e7745fdf6d315 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 7 May 2026 15:13:21 +0530 Subject: [PATCH 06/16] fix(data-table): use correct space token for stickySectionHeader (32px 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. --- packages/raystack/components/data-table/data-table.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/raystack/components/data-table/data-table.module.css b/packages/raystack/components/data-table/data-table.module.css index 85c4d0d7b..a47da2b11 100644 --- a/packages/raystack/components/data-table/data-table.module.css +++ b/packages/raystack/components/data-table/data-table.module.css @@ -227,9 +227,9 @@ /* Non-virtualized: sticky section header under table header */ .contentRoot .stickySectionHeader { position: sticky; - top: calc(var(--rs-space-8) + 0.5px); + top: calc(var(--rs-space-9) + 0.5px); z-index: 1; - height: var(--rs-space-8); + height: var(--rs-space-9); padding: var(--rs-space-3); box-sizing: border-box; background: var(--rs-color-background-base-secondary); From 8ae0b688028150fbf5ee2f20319554b3e96a8a94 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 7 May 2026 15:18:48 +0530 Subject: [PATCH 07/16] fix(data-table): correct padding --- .../raystack/components/data-table/data-table.module.css | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/raystack/components/data-table/data-table.module.css b/packages/raystack/components/data-table/data-table.module.css index a47da2b11..67df89a2a 100644 --- a/packages/raystack/components/data-table/data-table.module.css +++ b/packages/raystack/components/data-table/data-table.module.css @@ -1,6 +1,5 @@ .toolbar { - padding: var(--rs-space-3) var(--rs-space-7) var(--rs-space-3) - var(--rs-space-5); + padding: var(--rs-space-3) var(--rs-space-7) var(--rs-space-3) var(--rs-space-5); align-self: stretch; border-bottom: 0.5px solid var(--rs-color-border-base-primary); @@ -227,7 +226,7 @@ /* Non-virtualized: sticky section header under table header */ .contentRoot .stickySectionHeader { position: sticky; - top: calc(var(--rs-space-9) + 0.5px); + top: var(--rs-space-9); z-index: 1; height: var(--rs-space-9); padding: var(--rs-space-3); From b943773444aefa21c9652a029688c4a512b621ee Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 7 May 2026 15:20:44 +0530 Subject: [PATCH 08/16] fix(data-table): make groupHeaderHeight optional with fallback to rowHeight --- .../data-table/components/virtualized-content.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/raystack/components/data-table/components/virtualized-content.tsx b/packages/raystack/components/data-table/components/virtualized-content.tsx index 902525249..f6219a398 100644 --- a/packages/raystack/components/data-table/components/virtualized-content.tsx +++ b/packages/raystack/components/data-table/components/virtualized-content.tsx @@ -217,7 +217,7 @@ const DefaultEmptyComponent = () => ( export function VirtualizedContent({ rowHeight = 40, - groupHeaderHeight = 32, + groupHeaderHeight, overscan = 5, loadMoreOffset = 100, emptyState, @@ -264,12 +264,12 @@ export function VirtualizedContent({ estimateSize: index => { const row = rows[index]; const isGroupHeader = row?.subRows && row.subRows.length > 0; - return isGroupHeader ? groupHeaderHeight : rowHeight; + return isGroupHeader ? (groupHeaderHeight ?? rowHeight) : rowHeight; }, overscan }); - const anchorPixelHeight = groupHeaderHeight; + const anchorPixelHeight = groupHeaderHeight ?? rowHeight; const { stickyGroup, From 22377b101fd2c2b46139dcd0bc6eae3ca3a31de2 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 7 May 2026 15:24:10 +0530 Subject: [PATCH 09/16] fix(data-table): improve group anchor calculation for sticky headers --- .../components/data-table/hooks/useStickyGroupAnchor.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx b/packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx index 2ab618a55..6fb169d92 100644 --- a/packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx +++ b/packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx @@ -49,9 +49,12 @@ export function useStickyGroupAnchor({ let currentIdx = -1; for (let i = 0; i < groupHeaderList.length; i++) { - const m = virtualizer.measurementsCache[groupHeaderList[i].index]; - if (!m) continue; - if (m.start <= scrollTop) { + const start = virtualizer.getOffsetForIndex( + groupHeaderList[i].index, + 'start' + )?.[0]; + if (start === undefined) continue; + if (start <= scrollTop) { currentIdx = i; } else { break; From 1cdb835218a20ff1d3056fb4d2cf9214dc1e6049 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 7 May 2026 15:50:55 +0530 Subject: [PATCH 10/16] fix(data-table): enhance accessibility and styling for virtual group headers --- .../data-table/components/virtualized-content.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/raystack/components/data-table/components/virtualized-content.tsx b/packages/raystack/components/data-table/components/virtualized-content.tsx index f6219a398..5ba8bd013 100644 --- a/packages/raystack/components/data-table/components/virtualized-content.tsx +++ b/packages/raystack/components/data-table/components/virtualized-content.tsx @@ -65,13 +65,12 @@ function VirtualHeaders({ function VirtualGroupHeader({ data, - style + ...rest }: { data: GroupedData; - style?: React.CSSProperties; -}) { +} & React.HTMLAttributes) { return ( -
+
{data?.label} {data.showGroupCount ? ( @@ -117,6 +116,7 @@ function VirtualRows({ } + aria-hidden={isHidden ? 'true' : undefined} style={ isHidden ? { ...positionStyle, visibility: 'hidden' } @@ -341,7 +341,7 @@ export function VirtualizedContent({
{stickyGroupHeader && isGrouped && stickyGroup && (