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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ function MyGrid() {

Function to generate unique IDs for group rows. If not provided, a default implementation is used that concatenates parent and group keys with `__`.

###### `rowHeight?: Maybe<number | ((args: RowHeightArgs<R>) => number)>`
###### `rowHeight?: Maybe<number | string | ((args: RowHeightArgs<R>) => number)>`

**Note:** Unlike `DataGrid`, the `rowHeight` function receives [`RowHeightArgs<R>`](#rowheightargstrow) which includes a `type` property to distinguish between regular rows and group rows:

Expand Down
65 changes: 46 additions & 19 deletions src/DataGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import type { Key, KeyboardEvent } from 'react';
import { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { flushSync } from 'react-dom';

import {
type ActivePosition,
HeaderRowSelectionChangeContext,
HeaderRowSelectionContext,
type HeaderRowSelectionContextValue,
type PartialPosition,
RowSelectionChangeContext,
useActivePosition,
useCalculatedColumns,
Expand All @@ -14,10 +17,7 @@ import {
useScrollState,
useScrollToPosition,
useViewportColumns,
useViewportRows,
type ActivePosition,
type HeaderRowSelectionContextValue,
type PartialPosition
useViewportRows
} from './hooks';
import {
assertIsValidKeyGetter,
Expand Down Expand Up @@ -45,19 +45,19 @@ import type {
CellMouseEventHandler,
CellNavigationMode,
CellPasteArgs,
PositionChangeArgs,
Column,
ColumnOrColumnGroup,
ColumnWidths,
Direction,
FillEvent,
Maybe,
Position,
PositionChangeArgs,
Renderers,
RowsChangeData,
SetActivePositionOptions,
SelectHeaderRowEvent,
SelectRowEvent,
SetActivePositionOptions,
SortColumn
} from './types';
import { defaultRenderCell } from './Cell';
Expand All @@ -73,10 +73,10 @@ import { defaultRenderRow } from './Row';
import { default as defaultRenderSortStatus } from './sortStatus';
import { cellDragHandleClassname, cellDragHandleFrozenClassname } from './style/cell';
import {
rootClassname,
frozenColumnShadowClassname,
viewportDraggingClassname,
frozenColumnShadowTopClassname
frozenColumnShadowTopClassname,
rootClassname,
viewportDraggingClassname
} from './style/core';
import SummaryRow from './SummaryRow';

Expand Down Expand Up @@ -136,7 +136,7 @@ export interface DataGridProps<R, SR = unknown, K extends Key = Key> extends Sha
* Height of each row in pixels
* @default 35
*/
rowHeight?: Maybe<number | ((row: NoInfer<R>) => number)>;
rowHeight?: Maybe<number | string | ((row: NoInfer<R>) => number)>;
/**
* Height of the header row in pixels
* @default 35
Expand Down Expand Up @@ -301,9 +301,13 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
const renderCheckbox =
renderers?.renderCheckbox ?? defaultRenderers?.renderCheckbox ?? defaultRenderCheckbox;
const noRowsFallback = renderers?.noRowsFallback ?? defaultRenderers?.noRowsFallback;
const enableVirtualization = rawEnableVirtualization ?? true;
const enableVirtualization = rawEnableVirtualization ?? typeof rawRowHeight !== 'string';
const direction = rawDirection ?? 'ltr';

if (enableVirtualization && typeof rowHeight === 'string') {
throw new Error('`rowHeight` cannot be a string when `enableVirtualization` is true.');
}

/**
* ref
*/
Expand Down Expand Up @@ -404,7 +408,9 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
maxRowIdx,
setDraggedOverRowIdx
});
const { setScrollToPosition, scrollToPositionElement } = useScrollToPosition({ gridRef });
const { setScrollToPosition, scrollToPositionElement } = useScrollToPosition({
gridRef
});

const defaultGridComponents = useMemo(
() => ({
Expand Down Expand Up @@ -448,10 +454,16 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
findRowIdx
} = useViewportRows({
rows,
rowHeight,
clientHeight,
scrollTop,
enableVirtualization
enableVirtualization,
...(typeof rowHeight === 'string'
? {
rowHeight,
gridRef,
gridHeight
}
: { rowHeight })
});

const {
Expand Down Expand Up @@ -674,7 +686,11 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
if (isSelectable && shiftKey && key === ' ') {
assertIsValidKeyGetter<R, K>(rowKeyGetter);
const rowKey = rowKeyGetter(row);
selectRow({ row, checked: !selectedRows.has(rowKey), isShiftClick: false });
selectRow({
row,
checked: !selectedRows.has(rowKey),
isShiftClick: false
});
// prevent scrolling
event.preventDefault();
return;
Expand Down Expand Up @@ -760,7 +776,11 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
const indexes: number[] = [];
for (let i = startRowIdx; i < endRowIdx; i++) {
if (isCellEditable({ rowIdx: i, idx })) {
const updatedRow = onFill!({ columnKey: column.key, sourceRow, targetRow: rows[i] });
const updatedRow = onFill!({
columnKey: column.key,
sourceRow,
targetRow: rows[i]
});
if (updatedRow !== rows[i]) {
updatedRows[i] = updatedRow;
indexes.push(i);
Expand Down Expand Up @@ -978,10 +998,17 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr

const { row } = activePosition;
const column = getActiveColumn();
const colSpan = getColSpan(column, lastFrozenColumnIndex, { type: 'ROW', row });
const colSpan = getColSpan(column, lastFrozenColumnIndex, {
type: 'ROW',
row
});

function closeEditor(shouldFocus: boolean) {
const newPosition: ActivePosition = { idx: activePosition.idx, rowIdx, mode: 'ACTIVE' };
const newPosition: ActivePosition = {
idx: activePosition.idx,
rowIdx,
mode: 'ACTIVE'
};
setActivePosition(newPosition);
if (shouldFocus) {
setPositionToFocus(newPosition);
Expand Down
93 changes: 88 additions & 5 deletions src/hooks/useViewportRows.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
import { useMemo } from 'react';
import { useMemo, type RefObject } from 'react';

import { floor, max, min } from '../utils';

interface ViewportRowsArgs<R> {
interface ViewportRowsBaseArgs<R> {
rows: readonly R[];
rowHeight: number | ((row: R) => number);
clientHeight: number;
scrollTop: number;
enableVirtualization: boolean;
gridHeight?: number;
}

interface ViewportRowsArgsStringHeight {
rowHeight: string;
gridRef: RefObject<HTMLDivElement | null>;
gridHeight: number;
}

interface ViewportRowsArgsRegularHeight<R> {
rowHeight: number | ((row: R) => number);
}

type ViewportRowsArgs<R> = ViewportRowsBaseArgs<R> &
(ViewportRowsArgsStringHeight | ViewportRowsArgsRegularHeight<R>);

export function useViewportRows<R>({
rows,
rowHeight,
clientHeight,
scrollTop,
enableVirtualization
enableVirtualization,
...rest
}: ViewportRowsArgs<R>) {
const { gridRef, gridHeight } = rest as Partial<ViewportRowsArgsStringHeight>;
const { totalRowHeight, gridTemplateRows, getRowTop, getRowHeight, findRowIdx } = useMemo(() => {
if (typeof rowHeight === 'number') {
return {
Expand All @@ -28,6 +43,68 @@ export function useViewportRows<R>({
};
}

if (typeof rowHeight === 'string') {
if (!gridHeight) {
throw new Error(
'props.gridHeight is required when rowHeight is a string. This is needed to calculate the total height of the rows.'
);
}

const getRowElementFirstCell = (element: Element, rowIdx: number): Element | null => {
const nth = element.querySelector('.rdg-header-row') ? rowIdx + 2 : rowIdx + 1;
return element.querySelector(`[role="row"][aria-rowindex="${nth}"] > [role="gridcell"]`);
};

const getRowYTop = (element: Element, rowIdx: number) => {
const cell = getRowElementFirstCell(element, rowIdx);
if (!cell) return -1;
return cell.getBoundingClientRect().top + element.scrollTop;
};

return {
totalRowHeight: gridHeight,
gridTemplateRows: ` repeat(${rows.length}, ${rowHeight})`,
getRowTop(rowIdx: number) {
const element = gridRef?.current;
if (!element) return -1;
const cell = getRowElementFirstCell(element, rowIdx);
if (!cell) return -1;
return cell.getBoundingClientRect().top + element.scrollTop;
},
getRowHeight(rowIdx: number) {
const element = gridRef?.current;
if (!element) return -1;
const cell = getRowElementFirstCell(element, rowIdx);
if (!cell) return -1;
return cell.clientHeight;
},
findRowIdx(offset: number) {
const element = gridRef?.current;
if (!element) return -1;
let start = 0;
let end = rows.length - 1;

while (start <= end) {
const middle = start + floor((end - start) / 2);
const currentScrollTop = getRowYTop(element, middle);
const prevScrollTop = getRowYTop(element, middle - 1);

if (currentScrollTop >= offset && prevScrollTop < offset) return middle;

if (currentScrollTop < offset) {
start = middle + 1;
} else if (currentScrollTop > offset) {
end = middle - 1;
}

if (start > end) return end;
}

return -1;
}
};
}

// Calcule the height of all the rows upfront. This can cause performance issues
// and we can consider using a similar approach as react-window
// https://github.com/bvaughn/react-window/blob/b0a470cc264e9100afcaa1b78ed59d88f7914ad4/src/VariableSizeList.js#L68
Expand Down Expand Up @@ -102,15 +179,21 @@ export function useViewportRows<R>({
return 0;
}
};
}, [rowHeight, rows]);
}, [rowHeight, rows, gridRef, gridHeight]);

let rowOverscanStartIdx = 0;
let rowOverscanEndIdx = rows.length - 1;

if (enableVirtualization) {
const overscanThreshold = 4;
// `findRowIdx` only reads `gridRef.current` in the string-rowHeight branch,
// which is unreachable here because `enableVirtualization` is forced off when
// `rowHeight` is a string (see DataGrid.tsx).
/* eslint-disable react-hooks/refs */
const rowVisibleStartIdx = findRowIdx(scrollTop);
const rowVisibleEndIdx = findRowIdx(scrollTop + clientHeight);
/* eslint-enable react-hooks/refs */

rowOverscanStartIdx = max(0, rowVisibleStartIdx - overscanThreshold);
rowOverscanEndIdx = min(rows.length - 1, rowVisibleEndIdx + overscanThreshold);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,66 @@ test('rowHeight with unique first height', async () => {
return row === 0 ? 45 : 50;
}, 'repeat(1, 35px) 45px repeat(49, 50px)');
});

test('rowHeight is "auto" sets gridTemplateRows to repeat(N, auto)', async () => {
await setupGrid('auto');

expect(grid.element().style.gridTemplateRows).toBe('repeat(1, 35px) repeat(50, auto)');
});

test('rowHeight as a string auto-disables virtualization and renders all rows', async () => {
await setupGrid('auto');

// virtualization is off by default when `rowHeight` is a string,
// so every row is rendered regardless of viewport size
await testRowCount(50);
});

test('rowHeight accepts arbitrary CSS track values', async () => {
await setupGrid('min-content');

expect(grid.element().style.gridTemplateRows).toBe('repeat(1, 35px) repeat(50, min-content)');
await testRowCount(50);
});

test('rowHeight is "auto" sizes rows to fit their content', async () => {
const columns: Column<{ id: number; content: string }>[] = [
{ key: 'id', name: 'ID', width: 80 },
{
key: 'content',
name: 'Content',
width: 200,
renderCell: ({ row }) => <div style={{ whiteSpace: 'pre' }}>{row.content}</div>
}
];
const rows = [
{ id: 0, content: 'short' },
{ id: 1, content: 'line one\nline two\nline three\nline four' }
];

await setup({ columns, rows, rowHeight: 'auto' });

const row0 = page.getRow().nth(0).element() as HTMLElement;
const row1 = page.getRow().nth(1).element() as HTMLElement;

// multi-line cell must render taller than the single-line cell
expect(row1.clientHeight).toBeGreaterThan(row0.clientHeight);
});

test('rowHeight string + explicit enableVirtualization=true throws', async () => {
// Suppress React's error logging for this expected render error
// eslint-disable-next-line no-console
vi.mocked(console.error).mockImplementation(() => {});

await expect(
setup({
columns: [{ key: 'id', name: 'ID' }],
rows: [{ id: 0 }],
rowHeight: 'auto',
enableVirtualization: true
})
).rejects.toThrow('`rowHeight` cannot be a string when `enableVirtualization` is true.');

// eslint-disable-next-line no-console
vi.mocked(console.error).mockClear();
});
1 change: 1 addition & 0 deletions website/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default function Nav({ direction, onDirectionChange }: Props) {
<Link to="/ColumnsReordering">Columns Reordering</Link>
<Link to="/ContextMenu">Context Menu</Link>
<Link to="/CustomizableRenderers">Customizable Renderers</Link>
<Link to="/DynamicHeightCells">Dynamic Height Cells</Link>
<Link to="/RowGrouping">Row Grouping</Link>
<Link to="/HeaderFilters">Header Filters</Link>
<Link to="/InfiniteScrolling">Infinite Scrolling</Link>
Expand Down
Loading
Loading