Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Virtualization height computation. This is a well know issue on Tanstack virtualizer package.
When using virtualization, assign to your table the following css

````css
:root {
--your-pseudo-height--variable: 0px;
}

table::after {
content: "";
display: block;
height: var(--your-pseudo-height--variable);
}
```css

and use the `onPseudoHeightChange` to set it on your side

```tsx
<ReactDataTable<T>
...
onPseudoHeightChange={(height) => document.documentElement.style.setProperty("--your-pseudo-height--variable", `${height}px`)}
/>
````

## [5.13.0] - 2025-10-08

### Changed
Expand Down
21 changes: 17 additions & 4 deletions src/lib/ReactDataTable/ReactDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import { getFilterValue, setFilterValue } from "../utils/customFilterMethods";
import { useVirtualizer, Virtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
import { TableBody } from "./TableBody";
import { useVirtualizationTableHeight } from "../hooks/useVirtualizationTableHeight";

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

const TableInternal = <TData, TFilter extends FilterModel = Record<string, never>>(props: TableInternalProps<TData, TFilter>) => {
Expand All @@ -37,6 +39,8 @@ const TableInternal = <TData, TFilter extends FilterModel = Record<string, never
noEntriesMessage,
isStriped = true,
showClearSearchButton = true,
tableRef,
tableHeaderStyle,
} = props;

const {
Expand All @@ -60,9 +64,10 @@ const TableInternal = <TData, TFilter extends FilterModel = Record<string, never
}
: tableStyle
}
innerRef={tableRef}
>
{!withoutHeaders && (
<thead>
<thead style={tableHeaderStyle}>
{table.getHeaderGroups().map((headerGroup) => (
<Fragment key={headerGroup.id}>
<tr key={`${headerGroup.id}-col-header`}>
Expand Down Expand Up @@ -253,7 +258,7 @@ const TableInternal = <TData, TFilter extends FilterModel = Record<string, never
);
};

/**b
/**
* The table renderer for the react data table
* @param props according to {@link ReactDataTableProps}
*/
Expand All @@ -267,6 +272,7 @@ const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, neve
totalRecords = table.getCoreRowModel().rows.length,
dragAndDropOptions,
pagingNavigationComponents,
onPseudoHeightChange,
} = props;

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

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

const { scrollableRef, tableRef } = useVirtualizationTableHeight({
parentRef,
virtualizer,
enabled: virtualizerOptions.enabled ?? false,
onPseudoHeightChange,
});

return (
<>
<DndContext
Expand All @@ -303,8 +316,8 @@ const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, neve

{virtualizerOptions.enabled ? (
<div ref={parentRef} style={{ height: virtualizerOptions.height ?? 600, overflow: "auto" }}>
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
<TableInternal<TData, TFilter> {...props} virtualizer={virtualizer} />
<div ref={scrollableRef} style={{ height: `${virtualizer.getTotalSize()}px` }}>
<TableInternal<TData, TFilter> {...props} virtualizer={virtualizer} tableRef={tableRef} />
</div>
</div>
) : (
Expand Down
9 changes: 8 additions & 1 deletion src/lib/ReactDataTable/ReactDataTableProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { FilterModel } from "../types/TableState";
import { DragAndDropOptions } from "./DragAndDropOptions";
import { VirtualizationOptions } from "./VirtualizationOptions";
import { PagingNavigationComponents } from "@neolution-ch/react-pattern-ui";
import { useVirtualizationTableHeightProps } from "../hooks/useVirtualizationTableHeight";

/**
* The props for the ReactDataTable component
*/
export interface ReactDataTableProps<TData, TFilter extends FilterModel> {
export interface ReactDataTableProps<TData, TFilter extends FilterModel>
extends Pick<useVirtualizationTableHeightProps, "onPseudoHeightChange"> {
/**
* the table instance returned from useReactDataTable or useReactTable
*/
Expand All @@ -30,6 +32,11 @@ export interface ReactDataTableProps<TData, TFilter extends FilterModel> {
*/
tableStyle?: CSSProperties;

/**
* custom header table row style
*/
tableHeaderStyle?: CSSProperties;

/**
* total number of records in the table, if not supplied,
* the table will assume that all the data is loaded and set it to the length of the data array
Expand Down
84 changes: 84 additions & 0 deletions src/lib/hooks/useVirtualizationTableHeight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Virtualizer } from "@tanstack/react-virtual";
import { useCallback, useEffect, useRef, useState } from "react";

const adjustTableHeight = (
tableRef: React.RefObject<HTMLTableElement>,
virtualHeight: number,
onPseudoHeightChange: useVirtualizationTableHeightProps["onPseudoHeightChange"],
) => {
if (!tableRef.current) return;

// calculate the height for the pseudo element after the table
const existingPseudoElement = window.getComputedStyle(tableRef.current, "::after");
const existingPseudoHeight = parseFloat(existingPseudoElement.height) || 0;
const tableHeight = tableRef.current.clientHeight - existingPseudoHeight;
const pseudoHeight = Math.max(virtualHeight - tableHeight, 0);
onPseudoHeightChange?.(pseudoHeight);

return pseudoHeight;
};

export interface useVirtualizationTableHeightProps {
parentRef: React.RefObject<HTMLDivElement>;
virtualizer: Virtualizer<HTMLDivElement, Element>;
enabled: boolean;
onPseudoHeightChange?: (height: number) => void;
}

// https://github.com/TanStack/virtual/issues/640
const useVirtualizationTableHeight = (props: useVirtualizationTableHeightProps) => {
const { parentRef, virtualizer, enabled, onPseudoHeightChange } = props;
const scrollableRef = useRef<HTMLDivElement>(null);
const tableRef = useRef<HTMLTableElement>(null);
const [isScrollNearBottom, setIsScrollNearBottom] = useState(false);

// avoid calling virtualizer methods when virtualization is disabled
const virtualItems = enabled ? virtualizer.getVirtualItems() : [];
const virtualSize = enabled ? virtualizer.getTotalSize() : 0;

// callback to adjust the height of the pseudo element
const handlePseudoResize = useCallback(
() => adjustTableHeight(tableRef, virtualSize, onPseudoHeightChange),
[tableRef, virtualSize, onPseudoHeightChange],
);

// callback to handle scrolling, checking if we are near the bottom
const handleScroll = useCallback(() => {
if (parentRef.current) {
const scrollPosition = parentRef.current?.scrollTop;
const visibleHeight = parentRef.current?.clientHeight;
setIsScrollNearBottom(scrollPosition > virtualSize * 0.95 - visibleHeight);
}
}, [parentRef, virtualSize]);

// add an event listener on the scrollable parent container and resize the
// pseudo element whenever the table renders with new data
useEffect(() => {
if (!enabled) {
return;
}

const scrollable = parentRef.current;
if (scrollable) scrollable.addEventListener("scroll", handleScroll);
handlePseudoResize();

return () => {
if (scrollable) scrollable.removeEventListener("scroll", handleScroll);
};
}, [handleScroll, handlePseudoResize, parentRef, enabled]);

// if we are near the bottom of the table, resize the pseudo element each time
// the length of virtual items changes (which is effectively the number of table
// rows rendered to the DOM). This ensures we don't scroll too far or too short.
useEffect(() => {
if (!enabled) {
return;
}

if (isScrollNearBottom) handlePseudoResize();
}, [isScrollNearBottom, virtualItems.length, handlePseudoResize, enabled]);

return { scrollableRef, tableRef };
};

export { useVirtualizationTableHeight };
Loading