+
{data?.label}
{data.showGroupCount ? (
@@ -85,16 +85,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 +111,17 @@ function VirtualRows({
};
if (isGroupHeader) {
+ const isHidden = item.index === hiddenGroupIndex;
return (
}
- style={positionStyle}
+ aria-hidden={isHidden ? 'true' : undefined}
+ style={
+ isHidden
+ ? { ...positionStyle, visibility: 'hidden' }
+ : positionStyle
+ }
/>
);
}
@@ -209,7 +217,7 @@ const DefaultEmptyComponent = () => (
export function VirtualizedContent({
rowHeight = 40,
- groupHeaderHeight,
+ groupHeaderHeight = 40,
overscan = 5,
loadMoreOffset = 100,
emptyState,
@@ -233,23 +241,31 @@ 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];
const isGrouped = Boolean(groupBy) && groupBy !== defaultGroupOption.id;
const groupHeaderList = useMemo(() => {
- const list: { index: number; data: GroupedData }[] = [];
+ const list: {
+ index: number;
+ start: number;
+ data: GroupedData;
+ }[] = [];
+ let offset = 0;
rows.forEach((row, i) => {
- if (row.subRows && row.subRows.length > 0) {
- list.push({ index: i, data: row.original as GroupedData });
+ const isGroupHeader = row.subRows && row.subRows.length > 0;
+ if (isGroupHeader) {
+ list.push({
+ index: i,
+ start: offset,
+ data: row.original as GroupedData
+ });
}
+ offset += isGroupHeader ? groupHeaderHeight : rowHeight;
});
return list;
- }, [rows]);
+ }, [rows, groupHeaderHeight, rowHeight]);
const showLoaderRows = isLoading && rows.length > 0;
@@ -259,28 +275,27 @@ 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,
+ 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 +306,7 @@ export function VirtualizedContent({
isLoading,
loadMoreData,
loadMoreOffset,
- updateStickyGroup
+ recomputeStickyGroup
]);
const totalHeight = virtualizer.getTotalSize();
@@ -302,10 +317,6 @@ export function VirtualizedContent({
}
}, [headerGroups]);
- useLayoutEffect(() => {
- if (stickyGroupHeader) updateStickyGroup();
- }, [stickyGroupHeader, updateStickyGroup, groupHeaderList, isGrouped]);
-
const hasData = rows?.length > 0 || isLoading;
const hasChanges = hasActiveQuery(tableQuery || {}, defaultSort);
@@ -340,9 +351,13 @@ export function VirtualizedContent({
{stickyGroupHeader && isGrouped && stickyGroup && (
{stickyGroup.label}
@@ -364,6 +379,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..45017da4e 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);
@@ -93,6 +92,10 @@
border-spacing: 0;
}
+.contentRoot thead {
+ z-index: 2;
+}
+
.row {
background: var(--rs-color-background-base-primary);
}
@@ -131,7 +134,7 @@
.stickyHeader {
position: sticky;
top: 0;
- z-index: 1;
+ z-index: 2;
background: var(--rs-color-background-base-primary);
}
@@ -181,13 +184,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 +205,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 +224,13 @@
}
/* Non-virtualized: sticky section header under table header */
-.stickySectionHeader {
+.contentRoot .stickySectionHeader {
position: sticky;
- top: var(--rs-space-8);
+ top: var(--rs-space-9);
z-index: 1;
+ height: var(--rs-space-10);
+ 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/data-table.types.tsx b/packages/raystack/components/data-table/data-table.types.tsx
index 156708841..1af28b3c3 100644
--- a/packages/raystack/components/data-table/data-table.types.tsx
+++ b/packages/raystack/components/data-table/data-table.types.tsx
@@ -139,7 +139,7 @@ export type DataTableContentProps = DataTableContentBaseProps;
export type VirtualizedContentProps = DataTableContentBaseProps & {
/** Height of each row in pixels. */
rowHeight?: number;
- /** Height of group header rows in pixels. Falls back to rowHeight if not set. */
+ /** Height of group header rows in pixels. Defaults to 40 (matches the non-virtualized section header height). */
groupHeaderHeight?: number;
/** Number of rows to render outside visible area. */
overscan?: number;
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..7c3f56d15
--- /dev/null
+++ b/packages/raystack/components/data-table/hooks/useStickyGroupAnchor.tsx
@@ -0,0 +1,79 @@
+import { useCallback, useLayoutEffect, useState } from 'react';
+import { GroupedData } from '../data-table.types';
+
+interface UseStickyGroupAnchorParams