Skip to content

Commit

Permalink
Merge pull request #774 from buildo/virtualized-table-rows
Browse files Browse the repository at this point in the history
add option to virtualize Table rows
  • Loading branch information
giogonzo authored Sep 21, 2023
2 parents 7922fe3 + 3dfa14f commit 636e908
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 32 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
121 changes: 90 additions & 31 deletions packages/bento-design-system/src/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ import {
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
Expand Down Expand Up @@ -93,19 +94,25 @@ type Props<
> = {
columns: C;
data: ReadonlyArray<RowType<C>>;
groupBy?: C extends ReadonlyArray<SimpleColumnType<string, any, any>>
? C[number]["accessor"]
: C extends ReadonlyArray<GroupedColumnType<string, any, any>>
? C[number]["columns"][number]["accessor"]
: never;
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 extends ReadonlyArray<SimpleColumnType<string, any, any>>
? C[number]["accessor"]
: C extends ReadonlyArray<GroupedColumnType<string, any, any>>
? C[number]["columns"][number]["accessor"]
: never;
virtualizeRows?: never;
}
| { groupBy?: never; virtualizeRows?: boolean | { estimateRowHeight: (index: number) => number } }
) &
SortingProps<C>;

/**
* A component that renders a Table, with sorting capabilities
Expand Down Expand Up @@ -143,6 +150,7 @@ export function Table<
stickyHeaders,
height,
onRowPress,
virtualizeRows: virtualizeRowsConfig,
}: Props<C>) {
const config = useBentoConfig().table;
const customOrderByFn = useMemo(
Expand Down Expand Up @@ -292,6 +300,29 @@ export function Table<
}
}, [data.length, headerGroups, stickyLeftColumnsIds, stickyLeftColumnGroupsIds]);

const tableContainerRef = useRef<HTMLDivElement>(null);

const virtualizeRows =
typeof virtualizeRowsConfig === "boolean" ? virtualizeRowsConfig : virtualizeRowsConfig != null;
const estimateSize =
typeof virtualizeRowsConfig === "boolean"
? () => 52 // Default height of a medium-sized text cell
: virtualizeRowsConfig?.estimateRowHeight ?? (() => 0);

const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => tableContainerRef.current,
estimateSize,
});
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 @@ -354,13 +385,63 @@ export function Table<
));
}

const renderedRows = virtualizeRows
? columns
.map((_, index) => (
<div key={`paddingTop${index}`} style={{ marginTop: virtualPaddingTop }} />
))
.concat(
virtualRows.map((virtualRow) => {
const index = virtualRow.index;
const row = rows[index];
prepareRow(row);
return (
<RowContainer key={index} row={row} onPress={onRowPress}>
{renderCells(row.cells, index, onRowPress !== undefined)}
</RowContainer>
);
})
)
.concat(
columns.map((_, index) => (
<div key={`paddingBottom${index}`} style={{ marginBottom: virtualPaddingBottom }} />
))
)
: 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>
);
}
});

return (
<Box
{...getTableProps()}
alignItems="stretch"
overflow="auto"
className={table}
style={{ ...getTableProps().style, gridTemplateColumns, height: tableHeight(height) }}
style={{
...getTableProps().style,
gridTemplateColumns,
height: tableHeight(height),
}}
ref={tableContainerRef}
>
{headerGroups.map((headerGroup) =>
headerGroup.headers.map((header, index) => (
Expand Down Expand Up @@ -388,29 +469,7 @@ export function Table<
/>
))
)}

{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>
);
}
})}
{renderedRows}
</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 @@ -559,3 +559,21 @@ export const GroupedHeaders = {
height: { custom: 320 },
},
} 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: true,
data: repeatToLength(exampleData, 1_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 636e908

Please sign in to comment.