Skip to content

Commit 7956a62

Browse files
feat(manager-react-components): add tanstack virtualization component
ref: #MANAGER-19373 Signed-off-by: Alex Boungnaseng <[email protected]>
1 parent 13b83ee commit 7956a62

File tree

6 files changed

+163
-32
lines changed

6 files changed

+163
-32
lines changed

packages/manager-react-components/src/components/datagrid/Datagrid.component.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Table } from '@ovhcloud/ods-react';
33
import { TableBody } from './table/table-body/TableBody.component';
44
import { TableHeaderContent } from './table/table-head/table-header-content/TableHeaderContent.component';
55
import { FooterActions } from './table/table-footer/footer-actions/FooterActions.component';
6+
import { useContainerHeight } from './useContainerHeight';
67
import { useDatagrid } from './useDatagrid';
78
import { DatagridProps } from './Datagrid.props';
89
import './translations';
@@ -29,20 +30,36 @@ export const Datagrid = <T extends Record<string, unknown>>({
2930
});
3031

3132
const rowModel = getRowModel();
33+
const { rows } = rowModel;
3234
const headerGroups = getHeaderGroups();
35+
const tableContainerRef = useRef<HTMLDivElement>(null);
36+
const containerHeight = useContainerHeight({ tableContainerRef });
3337

3438
return (
3539
<>
36-
<Table>
37-
<TableHeaderContent<T>
38-
headerGroups={headerGroups}
39-
onSortChange={onSortChange}
40-
sorting={sorting}
41-
headerRefs={headerRefs.current}
42-
contentAlignLeft={contentAlignLeft}
43-
/>
44-
<TableBody rowModel={rowModel} />
45-
</Table>
40+
<div
41+
ref={tableContainerRef}
42+
style={{
43+
overflow: 'auto', //our scrollable table container
44+
position: 'relative', //needed for sticky header
45+
height: rows?.length > 6 ? containerHeight : '100%',
46+
}}
47+
>
48+
{/* Even though we're still using semantic table tags, we must use CSS grid and flexbox for dynamic row heights */}
49+
<Table className="w-full" style={{ display: 'grid' }}>
50+
<TableHeaderContent<T>
51+
headerGroups={headerGroups}
52+
onSortChange={onSortChange}
53+
sorting={sorting}
54+
headerRefs={headerRefs.current}
55+
contentAlignLeft={contentAlignLeft}
56+
/>
57+
<TableBody
58+
rowModel={rowModel}
59+
tableContainerRef={tableContainerRef}
60+
/>
61+
</Table>
62+
</div>
4663
<FooterActions
4764
hasNextPage={hasNextPage}
4865
onFetchAllPages={onFetchAllPages}
Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,70 @@
1-
import { flexRender } from '@tanstack/react-table';
1+
import { flexRender, Row } from '@tanstack/react-table';
22
import { TableBodyProps } from './TableBody.props';
3+
import { useVirtualizer } from '@tanstack/react-virtual';
4+
5+
export const TableBody = <T,>({
6+
rowModel,
7+
tableContainerRef,
8+
}: TableBodyProps<T>) => {
9+
const { rows } = rowModel;
10+
11+
const rowVirtualizer = useVirtualizer({
12+
count: rows.length,
13+
estimateSize: () => 33, //estimate row height for accurate scrollbar dragging
14+
getScrollElement: () => tableContainerRef.current,
15+
//measure dynamic row height, except in firefox because it measures table border height incorrectly
16+
measureElement:
17+
typeof window !== 'undefined' &&
18+
navigator.userAgent.indexOf('Firefox') === -1
19+
? (element) => element?.getBoundingClientRect().height
20+
: undefined,
21+
overscan: 30,
22+
});
323

4-
export const TableBody = <T,>({ rowModel }: TableBodyProps<T>) => {
524
return (
6-
<tbody>
7-
{rowModel?.rows.map((row) => (
8-
<tr key={row.id}>
9-
{row.getVisibleCells().map((cell) => (
10-
<td key={cell.id}>
11-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
12-
</td>
13-
))}
14-
</tr>
15-
))}
25+
<tbody
26+
style={{
27+
display: 'grid',
28+
height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is
29+
position: 'relative', //needed for absolute positioning of rows
30+
}}
31+
>
32+
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
33+
const row = rows[virtualRow.index] as Row<T>;
34+
return (
35+
<tr
36+
data-index={virtualRow.index} //needed for dynamic row height measurement
37+
ref={(node) => rowVirtualizer.measureElement(node)} //measure dynamic row height
38+
key={row.id}
39+
style={{
40+
display: 'flex',
41+
position: 'absolute',
42+
// transform: `translateY(${virtualRow.start}px)`,
43+
width: '100%',
44+
}}
45+
>
46+
{row.getVisibleCells().map((cell) => (
47+
<td
48+
key={cell.id}
49+
className="overflow-hidden"
50+
style={{
51+
display: 'flex',
52+
flex: 1,
53+
minWidth: 0,
54+
...(cell?.column?.columnDef?.maxSize && {
55+
maxWidth: cell?.column?.columnDef?.maxSize,
56+
}),
57+
...(cell?.column?.columnDef?.minSize && {
58+
minWidth: cell?.column?.columnDef?.minSize,
59+
}),
60+
}}
61+
>
62+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
63+
</td>
64+
))}
65+
</tr>
66+
);
67+
})}
1668
</tbody>
1769
);
1870
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { RefObject } from 'react';
12
import { RowModel } from '@tanstack/react-table';
23

34
export type TableBodyProps<T> = {
45
rowModel: RowModel<T>;
6+
tableContainerRef: RefObject<HTMLDivElement>;
57
};

packages/manager-react-components/src/components/datagrid/table/table-head/table-header-content/TableHeaderContent.component.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,33 @@ const TableHeaderContentComponent = <T,>({
1010
headerRefs,
1111
}: TableHeaderContentProps<T>) => {
1212
return (
13-
<thead>
13+
<thead
14+
style={{
15+
display: 'grid',
16+
position: 'sticky',
17+
top: 0,
18+
zIndex: 1,
19+
}}
20+
>
1421
{headerGroups?.map((headerGroup) => (
15-
<tr key={headerGroup.id}>
22+
<tr key={headerGroup.id} style={{ display: 'flex', width: '100%' }}>
1623
{headerGroup.headers.map((header) => (
1724
<th
1825
key={header.id}
19-
ref={(el) => {
20-
if (headerRefs && el) {
21-
// eslint-disable-next-line no-param-reassign
22-
headerRefs[header?.id ?? ''] = el;
23-
}
24-
}}
2526
className={`${
2627
contentAlignLeft ? 'text-left pl-4' : 'text-center'
27-
} h-11 whitespace-nowrap `}
28+
} h-11 whitespace-nowrap overflow-hidden`}
29+
style={{
30+
display: 'flex',
31+
flex: 1,
32+
minWidth: 0,
33+
...(header?.column?.columnDef?.maxSize && {
34+
maxWidth: header?.column?.columnDef?.maxSize,
35+
}),
36+
...(header?.column?.columnDef?.minSize && {
37+
minWidth: header?.column?.columnDef?.minSize,
38+
}),
39+
}}
2840
>
2941
{!header.isPlaceholder &&
3042
(onSortChange ? (

packages/manager-react-components/src/components/datagrid/table/table-head/table-header-sorting/TableHeaderSorting.component.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export const TableHeaderSorting = <T,>({
1414
return (
1515
<div {...attrs} data-testid={`header-${header.id}`}>
1616
<span>
17-
{flexRender(header.column.columnDef.header, header.getContext())}
17+
{flexRender(header.column.columnDef.header, header.getContext())} width
18+
: {header.getSize()}
1819
</span>
1920
<span className="align-middle inline-block pl-1 -mt-1">
2021
<Icon
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useState, useEffect, RefObject } from 'react';
2+
3+
export const useContainerHeight = ({
4+
tableContainerRef,
5+
}: {
6+
tableContainerRef: RefObject<HTMLDivElement>;
7+
}) => {
8+
const [containerHeight, setContainerHeight] = useState('100%');
9+
10+
useEffect(() => {
11+
const calculateHeight = () => {
12+
if (!tableContainerRef.current) return;
13+
14+
const { current } = tableContainerRef;
15+
const container = current;
16+
const { parentElement } = container;
17+
18+
if (!parentElement) return;
19+
20+
// Get the parent element's position relative to viewport
21+
const parentRect = parentElement.getBoundingClientRect();
22+
const viewportHeight = window.innerHeight;
23+
24+
// Calculate available height (viewport height minus parent's top position)
25+
const availableHeight = viewportHeight - parentRect.top;
26+
27+
// Set a minimum height of 200px and maximum of 80vh
28+
const calculatedHeight = Math.max(
29+
200,
30+
Math.min(availableHeight, viewportHeight * 0.8),
31+
);
32+
33+
setContainerHeight(`${calculatedHeight - 120}px`);
34+
};
35+
36+
// Calculate initial height
37+
calculateHeight();
38+
39+
// Add resize listener
40+
window.addEventListener('resize', calculateHeight);
41+
42+
// Cleanup
43+
return () => window.removeEventListener('resize', calculateHeight);
44+
}, []);
45+
46+
return containerHeight;
47+
};

0 commit comments

Comments
 (0)