Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ export const ROUTES: RouteInfo[] = [
title: 'Complex usage scenario',
description: `Example: a complex usage scenario for ${PRODUCT_NAME} featuring custom column definitions, asynchronous data loading with React Query, sorting, pagination, custom cell rendering, multiple row selection, and more`,
},
{
href: '/examples/virtualization',
title: 'Virtualization',
description: `Example: how to enable virtualization on ${PRODUCT_NAME}`,
},
{
href: '/type-definitions',
title: 'Type definitions',
Expand Down
26 changes: 26 additions & 0 deletions app/examples/virtualization/VirtualizationExample.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.buttons {
width: 100%;
display: flex;
gap: var(--mantine-spacing-md);
flex-direction: column;
@media (min-width: rem(400px)) {
flex-direction: row;
flex-wrap: wrap;
}
}

.button {
width: 100%;
@media (min-width: rem(400px)) {
width: calc(50% - (var(--mantine-spacing-md) / 2));
}
@media (min-width: rem(660px)) {
width: calc((100% - var(--mantine-spacing-md) * 3) / 4);
}
@media (min-width: $mantine-breakpoint-sm) {
width: calc(50% - (var(--mantine-spacing-md) / 2));
}
@media (min-width: $mantine-breakpoint-md) {
width: calc((100% - var(--mantine-spacing-md) * 3) / 4);
}
}
63 changes: 63 additions & 0 deletions app/examples/virtualization/VirtualizationExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use client';

import { faker } from '@faker-js/faker';
import { Button, Center, Paper } from '@mantine/core';
import { DataTable } from '__PACKAGE__';
import { useState } from 'react';
import classes from './VirtualizationExample.module.css';

type User = {
id: string;
name: string;
age: number;
};

const userData: User[] = Array.from({ length: 2000 }, () => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
age: faker.number.int({ min: 18, max: 65 }),
}));

export function VirtualizationExample() {
const [virtualized, setVirtualized] = useState(false);

const toggleVirtualized = () => setVirtualized((current) => !current);

// example-start
// ...

return (
<>
<DataTable
virtualize={virtualized}
borderRadius="sm"
withTableBorder
minHeight={200}
columns={[{ accessor: 'id' }, { accessor: 'name' }, { accessor: 'age' }]}
records={userData}
height={400}
rowExpansion={{
content: ({ record }) => (
<div style={{ padding: 10 }}>
<div>ID: {record.id}</div>
<div>Name: {record.name}</div>
<div>Age: {record.age}</div>
</div>
),
}}
/>
{/* example-skip */}
<Paper p="md" mt="sm" withBorder>
<Center>
<div className={classes.buttons}>
<Button className={classes.button} color="green" onClick={toggleVirtualized}>
{virtualized ? 'Disable' : 'Enable'} virtualization
</Button>
</div>
</Center>
</Paper>
{/* example-resume */}
</>
);
// example-end
}
35 changes: 35 additions & 0 deletions app/examples/virtualization/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Code } from '@mantine/core';
import type { Route } from 'next';
import { CodeBlock } from '~/components/CodeBlock';
import { ExternalLink } from '~/components/ExternalLink';
import { PageNavigation } from '~/components/PageNavigation';
import { PageTitle } from '~/components/PageTitle';
import { Txt } from '~/components/Txt';
import { readCodeFile } from '~/lib/code';
import { getRouteMetadata } from '~/lib/utils';
import { VirtualizationExample } from './VirtualizationExample';

const PATH: Route = '/examples/virtualization';

export const metadata = getRouteMetadata(PATH);

export default async function VirtualizationExamplePage() {
const code = await readCodeFile<string>(`${PATH}/VirtualizationExample.tsx`);

return (
<>
<PageTitle of={PATH} />
<Txt>
The <Code>DataTable</Code> component exposes a <Code>bodyRef</Code> property that can be used to pass a ref to
the underlying table <Code>tbody</Code> element. This ref can be passed to the <Code>useAutoAnimate()</Code>{' '}
hook from the excellent <ExternalLink to="https://auto-animate.formkit.com/">AutoAnimate</ExternalLink> library
to animate table rows when they are added, removed or reordered.
</Txt>
<VirtualizationExample />
<Txt>Here is the code:</Txt>
<CodeBlock code={code} />
<Txt>Head over to the next example to learn more.</Txt>
<PageNavigation of={PATH} />
</>
);
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,8 @@
"@mantine/hooks": ">=7.14",
"clsx": ">=2",
"react": ">=18.2"
},
"dependencies": {
"@tanstack/react-virtual": "^3.10.8"
}
}
58 changes: 49 additions & 9 deletions package/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Box, Table, type MantineSize } from '@mantine/core';
import { useDebouncedCallback, useMergedRef } from '@mantine/hooks';
import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { DataTableColumnsProvider } from './DataTableDragToggleProvider';
import { DataTableEmptyRow } from './DataTableEmptyRow';
import { DataTableEmptyState } from './DataTableEmptyState';
Expand All @@ -22,6 +22,7 @@ import {
import type { DataTableProps } from './types';
import { TEXT_SELECTION_DISABLED } from './utilityClasses';
import { differenceBy, getRecordId, uniqBy } from './utils';
import { useVirtualizer } from '@tanstack/react-virtual';

export function DataTable<T>({
withTableBorder,
Expand Down Expand Up @@ -129,6 +130,7 @@ export function DataTable<T>({
styles,
rowFactory,
tableWrapper,
virtualize,
...otherProps
}: DataTableProps<T>) {
const {
Expand Down Expand Up @@ -263,14 +265,27 @@ export function DataTable<T>({

const marginProperties = { m, my, mx, mt, mb, ml, mr };

const virtualizer = useVirtualizer({
count: recordsLength ?? 0,
enabled: virtualize,
getScrollElement: () => localScrollViewportRef.current,
estimateSize: () => 40,
overscan: 20,
});

const TableWrapper = useCallback(
({ children }: { children: React.ReactNode }) => {
if (virtualize) return <div style={{ height: virtualizer.getTotalSize() }}>{children}</div>;
if (tableWrapper) return tableWrapper({ children });
return children;
},
[tableWrapper]
[tableWrapper, virtualize, virtualizer]
);

useEffect(() => {
virtualizer.measure();
}, [records, virtualizer]);

return (
<DataTableColumnsProvider {...dragToggle}>
<Box
Expand Down Expand Up @@ -373,7 +388,8 @@ export function DataTable<T>({
)}
<tbody ref={bodyRef}>
{recordsLength ? (
records.map((record, index) => {
virtualizer.getVirtualItems().map((virtualRow, index) => {
const record = records[virtualRow.index];
const recordId = getRecordId(record, idAccessor);
const isSelected = selectedRecordIds?.includes(recordId) || false;

Expand All @@ -383,13 +399,13 @@ export function DataTable<T>({
handleSelectionChange = (e) => {
if (e.nativeEvent.shiftKey && lastSelectionChangeIndex !== null) {
const targetRecords = records.filter(
index > lastSelectionChangeIndex
virtualRow.index > lastSelectionChangeIndex
? (rec, idx) =>
idx >= lastSelectionChangeIndex &&
idx <= index &&
idx <= virtualRow.index &&
(isRecordSelectable ? isRecordSelectable(rec, idx) : true)
: (rec, idx) =>
idx >= index &&
idx >= virtualRow.index &&
idx <= lastSelectionChangeIndex &&
(isRecordSelectable ? isRecordSelectable(rec, idx) : true)
);
Expand All @@ -405,15 +421,39 @@ export function DataTable<T>({
: uniqBy([...selectedRecords, record], (rec) => getRecordId(rec, idAccessor))
);
}
setLastSelectionChangeIndex(index);
setLastSelectionChangeIndex(virtualRow.index);
};
}

const virtualRowStyle = virtualize
? () => {
return {
...(rowStyle ? rowStyle(record, virtualRow.index) : {}),
height: `${virtualRow.size}px`,

// transform: `translateY(${virtualRow.start}px)`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably remove the commented code that was left in.


transform: `translateY(${virtualRow.start - index * virtualRow.size}px)`,

// display: 'table',

// position: 'absolute',
// display: 'table',
// // top: 0,
// // left: 0,
// transform: `translateY(${virtualRow.start}px)`,
// width: '100%',
};
}
: rowStyle;

return (
<DataTableRow<T>
key={recordId as React.Key}
// key={virtualRow.key}
rowRef={virtualizer.measureElement}
record={record}
index={index}
index={virtualRow.index}
columns={effectiveColumns}
defaultColumnProps={defaultColumnProps}
defaultColumnRender={defaultColumnRender}
Expand All @@ -434,7 +474,7 @@ export function DataTable<T>({
color={rowColor}
backgroundColor={rowBackgroundColor}
className={rowClassName}
style={rowStyle}
style={virtualRowStyle}
customAttributes={customRowAttributes}
selectorCellShadowVisible={selectorCellShadowVisible}
selectionColumnClassName={selectionColumnClassName}
Expand Down
6 changes: 5 additions & 1 deletion package/DataTableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type DataTableRowProps<T> = {
selectionColumnClassName: string | undefined;
selectionColumnStyle: MantineStyleProp | undefined;
idAccessor: string;
rowRef: React.Ref<HTMLTableRowElement>;
} & Pick<DataTableProps<T>, 'rowFactory'>;

export function DataTableRow<T>({
Expand Down Expand Up @@ -81,6 +82,7 @@ export function DataTableRow<T>({
selectionColumnClassName,
selectionColumnStyle,
rowFactory,
rowRef,
}: Readonly<DataTableRowProps<T>>) {
const cols = (
<>
Expand Down Expand Up @@ -189,7 +191,9 @@ export function DataTableRow<T>({

return (
<>
<TableTr {...rowProps}>{cols}</TableTr>
<TableTr ref={rowRef} data-index={index} {...rowProps}>
{cols}
</TableTr>
{expandedElement}
</>
);
Expand Down
6 changes: 6 additions & 0 deletions package/types/DataTableProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ export type DataTableProps<T = Record<string, unknown>> = {
* Ref pointing to the table body element.
*/
bodyRef?: ((instance: HTMLTableSectionElement | null) => void) | React.RefObject<HTMLTableSectionElement>;

/**
* Determines whether the table should be virtualized. Note that virtualization is not compatible with
* using a tableWrapper function.
*/
virtualize?: boolean;
} & Omit<
TableProps,
| 'onScroll'
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,18 @@
dependencies:
"@tanstack/query-core" "5.62.8"

"@tanstack/react-virtual@^3.10.8":
version "3.10.8"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz#bf4b06f157ed298644a96ab7efc1a2b01ab36e3c"
integrity sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==
dependencies:
"@tanstack/virtual-core" "3.10.8"

"@tanstack/[email protected]":
version "3.10.8"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz#975446a667755222f62884c19e5c3c66d959b8b4"
integrity sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==

"@trysound/[email protected]":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
Expand Down