Skip to content

Commit

Permalink
add option to virtualize Table rows
Browse files Browse the repository at this point in the history
  • Loading branch information
giogonzo committed Sep 21, 2023
1 parent 205860e commit 826e7fa
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 47 deletions.
4 changes: 3 additions & 1 deletion packages/bento-design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@
"@react-types/overlays": "3.8.1",
"@react-types/radio": "3.5.0",
"@react-types/shared": "3.19.0",
"@tanstack/react-virtual": "3.0.0-beta.60",
"@tanstack/virtual-core": "3.0.0-beta.60",
"@vanilla-extract/css": "1.13.0",
"@vanilla-extract/dynamic": "2.0.3",
"@vanilla-extract/recipes": "0.5.0",
Expand Down Expand Up @@ -142,8 +144,8 @@
"@storybook/addon-essentials": "7.4.0",
"@storybook/addon-links": "7.4.0",
"@storybook/addons": "7.4.0",
"@storybook/react": "7.4.0",
"@storybook/builder-vite": "7.4.0",
"@storybook/react": "7.4.0",
"@storybook/react-vite": "7.4.0",
"@storybook/testing-library": "0.2.1",
"@storybook/types": "7.4.0",
Expand Down
130 changes: 84 additions & 46 deletions packages/bento-design-system/src/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ import {
table,
} from "./Table.css";
import { Column as ColumnType, GridWidth, Row as RowType } from "./types";
import { useLayoutEffect, useMemo, useState, CSSProperties, useEffect } from "react";
import { useLayoutEffect, useMemo, useState, CSSProperties, useEffect, useRef } from "react";
import { IconQuestionSolid, IconInfo } from "../Icons";
import { match, __ } from "ts-pattern";
import { useBentoConfig } from "../BentoConfigContext";
import { assignInlineVars } from "@vanilla-extract/dynamic";
import { useVirtualizer } from "@tanstack/react-virtual";

type SortFn<C extends ReadonlyArray<ColumnType<string, {}, any>>> = (
a: Row<RowType<C>>,
Expand Down Expand Up @@ -77,15 +78,18 @@ type SortingProps<C extends ReadonlyArray<ColumnType<string, {}, any>>> =
type Props<C extends ReadonlyArray<ColumnType<string, {}, any>>> = {
columns: C;
data: ReadonlyArray<RowType<C>>;
groupBy?: C[number]["accessor"];
noResultsTitle?: LocalizedString;
noResultsDescription?: LocalizedString;
noResultsFeedbackSize?: FeedbackProps["size"];
initialSorting?: Array<SortingRule<C>>;
stickyHeaders?: boolean;
height?: { custom: string | number };
onRowPress?: (row: Row<RowType<C>>) => void;
} & SortingProps<C>;
} & (
| { groupBy?: C[number]["accessor"]; virtualizeRows?: never }
| { groupBy?: never; virtualizeRows?: { estimateRowHeight: (index: number) => number } }
) &
SortingProps<C>;

/**
* A component that renders a Table, with sorting capabilities
Expand Down Expand Up @@ -119,6 +123,7 @@ export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
stickyHeaders,
height,
onRowPress,
virtualizeRows,
}: Props<C>) {
const config = useBentoConfig().table;
const customOrderByFn = useMemo(
Expand Down Expand Up @@ -236,6 +241,22 @@ export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
}
}, [data.length, stickyLeftColumnsIds]);

const tableContainerRef = useRef<HTMLDivElement>(null);

const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => tableContainerRef.current,
estimateSize: virtualizeRows?.estimateRowHeight ?? (() => 0),
});
const virtualRows = rowVirtualizer.getVirtualItems();

const virtualPaddingTop =
virtualizeRows && virtualRows.length > 0 ? virtualRows[0]?.start ?? 0 : 0;
const virtualPaddingBottom =
virtualizeRows && virtualRows.length > 0
? rowVirtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0)
: 0;

if (data.length === 0) {
return (
<Box
Expand Down Expand Up @@ -299,50 +320,67 @@ export function Table<C extends ReadonlyArray<ColumnType<string, {}, any>>>({
}

return (
<Box
{...getTableProps()}
alignItems="stretch"
overflow="auto"
className={table}
style={{ ...getTableProps().style, gridTemplateColumns, height: tableHeight(height) }}
>
{headerGroups.map((headerGroup) =>
headerGroup.headers.map((column, index) => (
<ColumnHeader
column={column}
key={column.id}
style={stickyLeftColumnStyle[column.id]}
lastLeftSticky={index === lastStickyColumnIndex}
stickyHeaders={stickyHeaders}
sticky={stickyLeftColumnsIds.includes(column.id)}
first={index === 0}
last={index + 1 === columns.length}
/>
))
)}

{rows.flatMap((row, index) => {
if (row.isGrouped) {
return [
<SectionHeader
key={row.groupByVal}
label={row.groupByVal}
numberOfStickyColumns={stickyLeftColumnsIds.length}
/>,
...row.leafRows.map((row, index) => {
<Box style={{ height: tableHeight(height) }} ref={tableContainerRef} overflow="auto">
<Box
{...getTableProps()}
alignItems="stretch"
className={table}
style={{
...getTableProps().style,
gridTemplateColumns,
height: virtualizeRows ? `${rowVirtualizer.getTotalSize()}px` : tableHeight(height),
paddingTop: virtualPaddingTop,
paddingBottom: virtualPaddingBottom,
}}
>
{headerGroups.map((headerGroup) =>
headerGroup.headers.map((column, index) => (
<ColumnHeader
column={column}
key={column.id}
style={stickyLeftColumnStyle[column.id]}
lastLeftSticky={index === lastStickyColumnIndex}
stickyHeaders={stickyHeaders}
sticky={stickyLeftColumnsIds.includes(column.id)}
first={index === 0}
last={index + 1 === columns.length}
/>
))
)}
{virtualizeRows
? virtualRows.map((virtualRow) => {
const index = virtualRow.index;
const row = rows[index];
prepareRow(row);
return renderCells(row.cells, index, false);
}),
];
} else {
prepareRow(row);
return (
<RowContainer key={index} row={row} onPress={onRowPress}>
{renderCells(row.cells, index, onRowPress !== undefined)}
</RowContainer>
);
}
})}
return (
<RowContainer key={index} row={row} onPress={onRowPress}>
{renderCells(row.cells, index, onRowPress !== undefined)}
</RowContainer>
);
})
: rows.flatMap((row, index) => {
if (row.isGrouped) {
return [
<SectionHeader
key={row.groupByVal}
label={row.groupByVal}
numberOfStickyColumns={stickyLeftColumnsIds.length}
/>,
...row.leafRows.map((row, index) => {
prepareRow(row);
return renderCells(row.cells, index, false);
}),
];
} else {
prepareRow(row);
return (
<RowContainer key={index} row={row} onPress={onRowPress}>
{renderCells(row.cells, index, onRowPress !== undefined)}
</RowContainer>
);
}
})}
</Box>
</Box>
);
}
Expand Down
18 changes: 18 additions & 0 deletions packages/bento-design-system/stories/Components/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -473,3 +473,21 @@ export const InteractiveRow = {
onRowPress: action("onRowPress"),
},
} satisfies Story;

function repeatToLength<T>(arr: T[], n: number): T[] {
if (arr.length <= 0) return [];
let result: T[] = [];
while (result.length < n) {
result = result.concat(arr);
}
return result.slice(0, n);
}

export const VirtualizedRows = {
args: {
stickyHeaders: true,
height: { custom: 340 },
virtualizeRows: { estimateRowHeight: () => 92 },
data: repeatToLength(exampleData, 10_000),
},
} satisfies Story;
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 826e7fa

Please sign in to comment.