Compare commits

...

1 Commits

Author SHA1 Message Date
Claire Chabas c873736da6 Add virtualizaion for tables 2026-01-21 14:15:33 +00:00
5 changed files with 144 additions and 22 deletions
+6 -2
View File
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "gitbook",
@@ -135,6 +134,7 @@
"@radix-ui/react-tooltip": "^1.1.8",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-virtual": "^3.13.12",
"@tusbar/cache-control": "^1.0.2",
"ai": "^4.2.2",
"assert-never": "catalog:",
@@ -1478,7 +1478,9 @@
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.11", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "postcss": "^8.4.41", "tailwindcss": "4.1.11" } }, "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.10.8", "", {}, "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA=="],
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.18", "", { "dependencies": { "@tanstack/virtual-core": "3.13.18" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.18", "", {}, "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg=="],
"@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.10.8", "", { "dependencies": { "@tanstack/virtual-core": "3.10.8" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-DB5QA8c/LfqOqIUCpSs3RdOTVroRRdqeHMqBkYrcashSZtOzIv8xbiqHgg7RYxDfkH5F3Y+e0MkuuyGNDVB0BQ=="],
@@ -4034,6 +4036,8 @@
"@tailwindcss/postcss/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
"@tanstack/vue-virtual/@tanstack/virtual-core": ["@tanstack/virtual-core@3.10.8", "", {}, "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA=="],
"@ts-morph/common/fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="],
"@ts-morph/common/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+1
View File
@@ -29,6 +29,7 @@
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tusbar/cache-control": "^1.0.2",
"@tanstack/react-virtual": "^3.13.12",
"ai": "^4.2.2",
"assert-never": "catalog:",
"bidc": "catalog:",
@@ -1,7 +1,5 @@
import { type ClassValue, tcls } from '@/lib/tailwind';
import type { DocumentTableViewGrid } from '@gitbook/api';
import { tcls } from '@/lib/tailwind';
import { RecordColumnValue } from './RecordColumnValue';
import type { TableRecordKV, TableViewProps } from './Table';
import { getColumnWidth } from './ViewGrid';
@@ -13,12 +11,13 @@ export function RecordRow(
record: TableRecordKV;
autoSizedColumns: string[];
fixedColumns: string[];
className?: ClassValue;
}
) {
const { view, autoSizedColumns, fixedColumns, block, context } = props;
const { view, autoSizedColumns, fixedColumns, block, context, className } = props;
return (
<div className={styles.row} role="row">
<div className={tcls(styles.row, className)} role="row">
{view.columns.map((column) => {
const columnWidth = getColumnWidth({
column,
@@ -0,0 +1,105 @@
import { tcls } from '@/lib/tailwind';
import type { DocumentTableViewGrid } from '@gitbook/api';
import { useWindowVirtualizer } from '@tanstack/react-virtual';
import React from 'react';
import { RecordRow } from './RecordRow';
import type { TableViewProps } from './Table';
/**
* Virtualized group of rows for tables with many records.
*/
export const VIRTUALIZATION_THRESHOLD = 200;
const ROW_ESTIMATE_PX = 40;
export function RowGroupVirtualized(
props: TableViewProps<DocumentTableViewGrid> & {
autoSizedColumns: string[];
fixedColumns: string[];
tableWidth: string;
}
) {
const { records, tableWidth, ...rest } = props;
const [isHydrated, setIsHydrated] = React.useState(false);
const [scrollMargin, setScrollMargin] = React.useState(0);
const parentRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
setIsHydrated(true);
}, []);
React.useEffect(() => {
if (!isHydrated) {
return;
}
const updateScrollMargin = () => {
if (!parentRef.current) {
return;
}
const rect = parentRef.current.getBoundingClientRect();
setScrollMargin(rect.top + window.scrollY);
};
updateScrollMargin();
window.addEventListener('resize', updateScrollMargin);
return () => {
window.removeEventListener('resize', updateScrollMargin);
};
}, [isHydrated]);
const rowVirtualizer = useWindowVirtualizer({
count: records.length,
estimateSize: () => ROW_ESTIMATE_PX,
overscan: 8,
scrollMargin,
});
if (!isHydrated) {
return (
<div role="rowgroup" className={tcls('flex', 'flex-col', tableWidth)}>
{records.map((record) => (
<RecordRow key={record[0]} record={record} records={records} {...rest} />
))}
</div>
);
}
return (
<div
ref={parentRef}
role="rowgroup"
className={tcls('relative', 'flex', 'flex-col', tableWidth)}
style={{ height: rowVirtualizer.getTotalSize() }}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const record = records[virtualRow.index];
if (!record) {
return null;
}
return (
<div
key={record[0]}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start - scrollMargin}px)`,
}}
>
<RecordRow
records={records}
record={record}
className={virtualRow.index === 0 ? undefined : 'border-t'}
{...rest}
/>
</div>
);
})}
</div>
);
}
@@ -3,6 +3,7 @@ import type { DocumentTableViewGrid } from '@gitbook/api';
import { tcls } from '@/lib/tailwind';
import { RecordRow } from './RecordRow';
import { RowGroupVirtualized, VIRTUALIZATION_THRESHOLD } from './RowGroupVirtualized';
import type { TableViewProps } from './Table';
import styles from './table.module.css';
import { getColumnAlignment } from './utils';
@@ -13,7 +14,7 @@ import { getColumnAlignment } from './utils';
3. Auto-size is turned off without setting a width, we then default to a fixed width of 100px
*/
export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
const { block, view, records, style, context } = props;
const { block, view, records, style, context, isOffscreen } = props;
/* Calculate how many columns are auto-sized vs fixed width */
const columnWidths = context.mode === 'print' ? undefined : view.columnWidths;
@@ -29,6 +30,9 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
(columnId) => (block.data.definition[columnId]?.title.trim().length ?? 0) > 0
);
const shouldVirtualize =
context.mode !== 'print' && !isOffscreen && records.length >= VIRTUALIZATION_THRESHOLD;
return (
<div className={tcls(style, styles.tableWrapper)}>
{/* Table */}
@@ -73,20 +77,29 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
</div>
</div>
)}
<div
role="rowgroup"
className={tcls('flex', 'flex-col', tableWidth, '[&>*+*]:border-t')}
>
{records.map((record) => (
<RecordRow
key={record[0]}
record={record}
autoSizedColumns={autoSizedColumns}
fixedColumns={fixedColumns}
{...props}
/>
))}
</div>
{shouldVirtualize ? (
<RowGroupVirtualized
autoSizedColumns={autoSizedColumns}
fixedColumns={fixedColumns}
tableWidth={tableWidth}
{...props}
/>
) : (
<div
role="rowgroup"
className={tcls('flex', 'flex-col', tableWidth, '[&>*+*]:border-t')}
>
{records.map((record) => (
<RecordRow
key={record[0]}
record={record}
autoSizedColumns={autoSizedColumns}
fixedColumns={fixedColumns}
{...props}
/>
))}
</div>
)}
</div>
</div>
);