Skip to content

Commit 1aad5f1

Browse files
authored
Fix Virtualization height (neolution-ch#84)
1 parent 7395986 commit 1aad5f1

4 files changed

Lines changed: 135 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Virtualization height computation. This is a well know issue on Tanstack virtualizer package.
13+
When using virtualization, assign to your table the following css
14+
15+
````css
16+
:root {
17+
--your-pseudo-height--variable: 0px;
18+
}
19+
20+
table::after {
21+
content: "";
22+
display: block;
23+
height: var(--your-pseudo-height--variable);
24+
}
25+
```css
26+
27+
and use the `onPseudoHeightChange` to set it on your side
28+
29+
```tsx
30+
<ReactDataTable<T>
31+
...
32+
onPseudoHeightChange={(height) => document.documentElement.style.setProperty("--your-pseudo-height--variable", `${height}px`)}
33+
/>
34+
````
35+
1036
## [5.13.0] - 2025-10-08
1137

1238
### Changed

src/lib/ReactDataTable/ReactDataTable.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ import { getFilterValue, setFilterValue } from "../utils/customFilterMethods";
1616
import { useVirtualizer, Virtualizer } from "@tanstack/react-virtual";
1717
import { useRef } from "react";
1818
import { TableBody } from "./TableBody";
19+
import { useVirtualizationTableHeight } from "../hooks/useVirtualizationTableHeight";
1920

2021
interface TableInternalProps<TData, TFilter extends FilterModel = Record<string, never>> extends ReactDataTableProps<TData, TFilter> {
2122
virtualizer?: Virtualizer<HTMLDivElement, Element>;
23+
tableRef?: React.RefObject<HTMLTableElement>;
2224
}
2325

2426
const TableInternal = <TData, TFilter extends FilterModel = Record<string, never>>(props: TableInternalProps<TData, TFilter>) => {
@@ -37,6 +39,8 @@ const TableInternal = <TData, TFilter extends FilterModel = Record<string, never
3739
noEntriesMessage,
3840
isStriped = true,
3941
showClearSearchButton = true,
42+
tableRef,
43+
tableHeaderStyle,
4044
} = props;
4145

4246
const {
@@ -60,9 +64,10 @@ const TableInternal = <TData, TFilter extends FilterModel = Record<string, never
6064
}
6165
: tableStyle
6266
}
67+
innerRef={tableRef}
6368
>
6469
{!withoutHeaders && (
65-
<thead>
70+
<thead style={tableHeaderStyle}>
6671
{table.getHeaderGroups().map((headerGroup) => (
6772
<Fragment key={headerGroup.id}>
6873
<tr key={`${headerGroup.id}-col-header`}>
@@ -253,7 +258,7 @@ const TableInternal = <TData, TFilter extends FilterModel = Record<string, never
253258
);
254259
};
255260

256-
/**b
261+
/**
257262
* The table renderer for the react data table
258263
* @param props according to {@link ReactDataTableProps}
259264
*/
@@ -267,6 +272,7 @@ const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, neve
267272
totalRecords = table.getCoreRowModel().rows.length,
268273
dragAndDropOptions,
269274
pagingNavigationComponents,
275+
onPseudoHeightChange,
270276
} = props;
271277

272278
const { pagination } = table.getState();
@@ -291,6 +297,13 @@ const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, neve
291297

292298
const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {}));
293299

300+
const { scrollableRef, tableRef } = useVirtualizationTableHeight({
301+
parentRef,
302+
virtualizer,
303+
enabled: virtualizerOptions.enabled ?? false,
304+
onPseudoHeightChange,
305+
});
306+
294307
return (
295308
<>
296309
<DndContext
@@ -303,8 +316,8 @@ const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, neve
303316

304317
{virtualizerOptions.enabled ? (
305318
<div ref={parentRef} style={{ height: virtualizerOptions.height ?? 600, overflow: "auto" }}>
306-
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
307-
<TableInternal<TData, TFilter> {...props} virtualizer={virtualizer} />
319+
<div ref={scrollableRef} style={{ height: `${virtualizer.getTotalSize()}px` }}>
320+
<TableInternal<TData, TFilter> {...props} virtualizer={virtualizer} tableRef={tableRef} />
308321
</div>
309322
</div>
310323
) : (

src/lib/ReactDataTable/ReactDataTableProps.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { FilterModel } from "../types/TableState";
44
import { DragAndDropOptions } from "./DragAndDropOptions";
55
import { VirtualizationOptions } from "./VirtualizationOptions";
66
import { PagingNavigationComponents } from "@neolution-ch/react-pattern-ui";
7+
import { useVirtualizationTableHeightProps } from "../hooks/useVirtualizationTableHeight";
78

89
/**
910
* The props for the ReactDataTable component
1011
*/
11-
export interface ReactDataTableProps<TData, TFilter extends FilterModel> {
12+
export interface ReactDataTableProps<TData, TFilter extends FilterModel>
13+
extends Pick<useVirtualizationTableHeightProps, "onPseudoHeightChange"> {
1214
/**
1315
* the table instance returned from useReactDataTable or useReactTable
1416
*/
@@ -30,6 +32,11 @@ export interface ReactDataTableProps<TData, TFilter extends FilterModel> {
3032
*/
3133
tableStyle?: CSSProperties;
3234

35+
/**
36+
* custom header table row style
37+
*/
38+
tableHeaderStyle?: CSSProperties;
39+
3340
/**
3441
* total number of records in the table, if not supplied,
3542
* the table will assume that all the data is loaded and set it to the length of the data array
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Virtualizer } from "@tanstack/react-virtual";
2+
import { useCallback, useEffect, useRef, useState } from "react";
3+
4+
const adjustTableHeight = (
5+
tableRef: React.RefObject<HTMLTableElement>,
6+
virtualHeight: number,
7+
onPseudoHeightChange: useVirtualizationTableHeightProps["onPseudoHeightChange"],
8+
) => {
9+
if (!tableRef.current) return;
10+
11+
// calculate the height for the pseudo element after the table
12+
const existingPseudoElement = window.getComputedStyle(tableRef.current, "::after");
13+
const existingPseudoHeight = parseFloat(existingPseudoElement.height) || 0;
14+
const tableHeight = tableRef.current.clientHeight - existingPseudoHeight;
15+
const pseudoHeight = Math.max(virtualHeight - tableHeight, 0);
16+
onPseudoHeightChange?.(pseudoHeight);
17+
18+
return pseudoHeight;
19+
};
20+
21+
export interface useVirtualizationTableHeightProps {
22+
parentRef: React.RefObject<HTMLDivElement>;
23+
virtualizer: Virtualizer<HTMLDivElement, Element>;
24+
enabled: boolean;
25+
onPseudoHeightChange?: (height: number) => void;
26+
}
27+
28+
// https://github.com/TanStack/virtual/issues/640
29+
const useVirtualizationTableHeight = (props: useVirtualizationTableHeightProps) => {
30+
const { parentRef, virtualizer, enabled, onPseudoHeightChange } = props;
31+
const scrollableRef = useRef<HTMLDivElement>(null);
32+
const tableRef = useRef<HTMLTableElement>(null);
33+
const [isScrollNearBottom, setIsScrollNearBottom] = useState(false);
34+
35+
// avoid calling virtualizer methods when virtualization is disabled
36+
const virtualItems = enabled ? virtualizer.getVirtualItems() : [];
37+
const virtualSize = enabled ? virtualizer.getTotalSize() : 0;
38+
39+
// callback to adjust the height of the pseudo element
40+
const handlePseudoResize = useCallback(
41+
() => adjustTableHeight(tableRef, virtualSize, onPseudoHeightChange),
42+
[tableRef, virtualSize, onPseudoHeightChange],
43+
);
44+
45+
// callback to handle scrolling, checking if we are near the bottom
46+
const handleScroll = useCallback(() => {
47+
if (parentRef.current) {
48+
const scrollPosition = parentRef.current?.scrollTop;
49+
const visibleHeight = parentRef.current?.clientHeight;
50+
setIsScrollNearBottom(scrollPosition > virtualSize * 0.95 - visibleHeight);
51+
}
52+
}, [parentRef, virtualSize]);
53+
54+
// add an event listener on the scrollable parent container and resize the
55+
// pseudo element whenever the table renders with new data
56+
useEffect(() => {
57+
if (!enabled) {
58+
return;
59+
}
60+
61+
const scrollable = parentRef.current;
62+
if (scrollable) scrollable.addEventListener("scroll", handleScroll);
63+
handlePseudoResize();
64+
65+
return () => {
66+
if (scrollable) scrollable.removeEventListener("scroll", handleScroll);
67+
};
68+
}, [handleScroll, handlePseudoResize, parentRef, enabled]);
69+
70+
// if we are near the bottom of the table, resize the pseudo element each time
71+
// the length of virtual items changes (which is effectively the number of table
72+
// rows rendered to the DOM). This ensures we don't scroll too far or too short.
73+
useEffect(() => {
74+
if (!enabled) {
75+
return;
76+
}
77+
78+
if (isScrollNearBottom) handlePseudoResize();
79+
}, [isScrollNearBottom, virtualItems.length, handlePseudoResize, enabled]);
80+
81+
return { scrollableRef, tableRef };
82+
};
83+
84+
export { useVirtualizationTableHeight };

0 commit comments

Comments
 (0)