From d22695871d573f8646608b01c895f1f00ff74434 Mon Sep 17 00:00:00 2001 From: Giovanni Gonzaga Date: Thu, 21 Sep 2023 13:42:07 +0200 Subject: [PATCH 1/3] add option to virtualize Table rows --- packages/bento-design-system/package.json | 4 +- .../bento-design-system/src/Table/Table.tsx | 138 ++++++++++++------ .../stories/Components/Table.stories.tsx | 18 +++ pnpm-lock.yaml | 17 +++ 4 files changed, 133 insertions(+), 44 deletions(-) diff --git a/packages/bento-design-system/package.json b/packages/bento-design-system/package.json index b3a548ea4..8a316df17 100644 --- a/packages/bento-design-system/package.json +++ b/packages/bento-design-system/package.json @@ -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", @@ -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", diff --git a/packages/bento-design-system/src/Table/Table.tsx b/packages/bento-design-system/src/Table/Table.tsx index bf0e45453..6b428b02a 100644 --- a/packages/bento-design-system/src/Table/Table.tsx +++ b/packages/bento-design-system/src/Table/Table.tsx @@ -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 @@ -93,11 +94,6 @@ type Props< > = { columns: C; data: ReadonlyArray>; - groupBy?: C extends ReadonlyArray> - ? C[number]["accessor"] - : C extends ReadonlyArray> - ? C[number]["columns"][number]["accessor"] - : never; noResultsTitle?: LocalizedString; noResultsDescription?: LocalizedString; noResultsFeedbackSize?: FeedbackProps["size"]; @@ -105,7 +101,18 @@ type Props< stickyHeaders?: boolean; height?: { custom: string | number }; onRowPress?: (row: Row>) => void; -} & SortingProps; +} & ( + | { + groupBy?: C extends ReadonlyArray> + ? C[number]["accessor"] + : C extends ReadonlyArray> + ? C[number]["columns"][number]["accessor"] + : never; + virtualizeRows?: never; + } + | { groupBy?: never; virtualizeRows?: { estimateRowHeight: (index: number) => number } } +) & + SortingProps; /** * A component that renders a Table, with sorting capabilities @@ -143,6 +150,7 @@ export function Table< stickyHeaders, height, onRowPress, + virtualizeRows, }: Props) { const config = useBentoConfig().table; const customOrderByFn = useMemo( @@ -292,6 +300,22 @@ export function Table< } }, [data.length, headerGroups, stickyLeftColumnsIds, stickyLeftColumnGroupsIds]); + const tableContainerRef = useRef(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 ( - {headerGroups.map((headerGroup) => - headerGroup.headers.map((header, index) => ( - + const renderedRows = virtualizeRows + ? columns + .map((_, index) => ( +
)) - )} - - {rows.flatMap((row, index) => { + .concat( + virtualRows.map((virtualRow) => { + const index = virtualRow.index; + const row = rows[index]; + prepareRow(row); + return ( + + {renderCells(row.cells, index, onRowPress !== undefined)} + + ); + }) + ) + .concat( + columns.map((_, index) => ( +
+ )) + ) + : rows.flatMap((row, index) => { if (row.isGrouped) { return [ ); } - })} + }); + + return ( + + + {headerGroups.map((headerGroup) => + headerGroup.headers.map((header, index) => ( + + )) + )} + {renderedRows} + ); } diff --git a/packages/bento-design-system/stories/Components/Table.stories.tsx b/packages/bento-design-system/stories/Components/Table.stories.tsx index 5eafc02ec..bea9f91fe 100644 --- a/packages/bento-design-system/stories/Components/Table.stories.tsx +++ b/packages/bento-design-system/stories/Components/Table.stories.tsx @@ -559,3 +559,21 @@ export const GroupedHeaders = { height: { custom: 320 }, }, } satisfies Story; + +function repeatToLength(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; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2449bb29d..ea266ee75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,8 @@ importers: '@storybook/react-vite': 7.4.0 '@storybook/testing-library': 0.2.1 '@storybook/types': 7.4.0 + '@tanstack/react-virtual': 3.0.0-beta.60 + '@tanstack/virtual-core': 3.0.0-beta.60 '@testing-library/dom': 9.3.1 '@testing-library/jest-dom': 6.1.2 '@testing-library/react': 14.0.0 @@ -188,6 +190,8 @@ importers: '@react-types/overlays': 3.8.1_react@18.2.0 '@react-types/radio': 3.5.0_react@18.2.0 '@react-types/shared': 3.19.0_react@18.2.0 + '@tanstack/react-virtual': 3.0.0-beta.60_react@18.2.0 + '@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_3qusp464cmatvw2qbjjzblbe54 @@ -7288,6 +7292,19 @@ packages: dependencies: defer-to-connect: 1.1.3 + /@tanstack/react-virtual/3.0.0-beta.60_react@18.2.0: + resolution: {integrity: sha512-F0wL9+byp7lf/tH6U5LW0ZjBqs+hrMXJrj5xcIGcklI0pggvjzMNW9DdIBcyltPNr6hmHQ0wt8FDGe1n1ZAThA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || 18 + dependencies: + '@tanstack/virtual-core': 3.0.0-beta.60 + react: 18.2.0 + dev: false + + /@tanstack/virtual-core/3.0.0-beta.60: + resolution: {integrity: sha512-QlCdhsV1+JIf0c0U6ge6SQmpwsyAT0oQaOSZk50AtEeAyQl9tQrd6qCHAslxQpgphrfe945abvKG8uYvw3hIGA==} + dev: false + /@testing-library/dom/9.3.1: resolution: {integrity: sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==} engines: {node: '>=14'} From c5854beb9dd2ef3311f3134696578b7cfe25e0c0 Mon Sep 17 00:00:00 2001 From: Giovanni Gonzaga Date: Thu, 21 Sep 2023 16:52:20 +0200 Subject: [PATCH 2/3] simplify layout --- .../bento-design-system/src/Table/Table.tsx | 78 +++++++++---------- .../stories/Components/Table.stories.tsx | 2 +- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/bento-design-system/src/Table/Table.tsx b/packages/bento-design-system/src/Table/Table.tsx index 6b428b02a..d1d5f9104 100644 --- a/packages/bento-design-system/src/Table/Table.tsx +++ b/packages/bento-design-system/src/Table/Table.tsx @@ -424,45 +424,45 @@ export function Table< }); return ( - - - {headerGroups.map((headerGroup) => - headerGroup.headers.map((header, index) => ( - - )) - )} - {renderedRows} - + + {headerGroups.map((headerGroup) => + headerGroup.headers.map((header, index) => ( + + )) + )} + {renderedRows} ); } diff --git a/packages/bento-design-system/stories/Components/Table.stories.tsx b/packages/bento-design-system/stories/Components/Table.stories.tsx index bea9f91fe..90f4d11df 100644 --- a/packages/bento-design-system/stories/Components/Table.stories.tsx +++ b/packages/bento-design-system/stories/Components/Table.stories.tsx @@ -574,6 +574,6 @@ export const VirtualizedRows = { stickyHeaders: true, height: { custom: 340 }, virtualizeRows: { estimateRowHeight: () => 92 }, - data: repeatToLength(exampleData, 10_000), + data: repeatToLength(exampleData, 1_000), }, } satisfies Story; From 3dfa14fd0035674ededf3a27d7c0368b92760954 Mon Sep 17 00:00:00 2001 From: Giovanni Gonzaga Date: Thu, 21 Sep 2023 16:53:01 +0200 Subject: [PATCH 3/3] add default row height an d simplify config --- packages/bento-design-system/src/Table/Table.tsx | 13 ++++++++++--- .../stories/Components/Table.stories.tsx | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/bento-design-system/src/Table/Table.tsx b/packages/bento-design-system/src/Table/Table.tsx index d1d5f9104..dfa8c38e7 100644 --- a/packages/bento-design-system/src/Table/Table.tsx +++ b/packages/bento-design-system/src/Table/Table.tsx @@ -110,7 +110,7 @@ type Props< : never; virtualizeRows?: never; } - | { groupBy?: never; virtualizeRows?: { estimateRowHeight: (index: number) => number } } + | { groupBy?: never; virtualizeRows?: boolean | { estimateRowHeight: (index: number) => number } } ) & SortingProps; @@ -150,7 +150,7 @@ export function Table< stickyHeaders, height, onRowPress, - virtualizeRows, + virtualizeRows: virtualizeRowsConfig, }: Props) { const config = useBentoConfig().table; const customOrderByFn = useMemo( @@ -302,10 +302,17 @@ export function Table< const tableContainerRef = useRef(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: virtualizeRows?.estimateRowHeight ?? (() => 0), + estimateSize, }); const virtualRows = rowVirtualizer.getVirtualItems(); diff --git a/packages/bento-design-system/stories/Components/Table.stories.tsx b/packages/bento-design-system/stories/Components/Table.stories.tsx index 90f4d11df..7ac70ebda 100644 --- a/packages/bento-design-system/stories/Components/Table.stories.tsx +++ b/packages/bento-design-system/stories/Components/Table.stories.tsx @@ -573,7 +573,7 @@ export const VirtualizedRows = { args: { stickyHeaders: true, height: { custom: 340 }, - virtualizeRows: { estimateRowHeight: () => 92 }, + virtualizeRows: true, data: repeatToLength(exampleData, 1_000), }, } satisfies Story;