Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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,
element: gridRef.current,
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
86 changes: 82 additions & 4 deletions src/hooks/useViewportRows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,33 @@

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;
}

interface ViewportRowsArgsStringHeight {
rowHeight: string;
element: HTMLElement | 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 { totalRowHeight, gridTemplateRows, getRowTop, getRowHeight, findRowIdx } = useMemo(() => {
if (typeof rowHeight === 'number') {
Expand All @@ -28,6 +41,71 @@
};
}

if (typeof rowHeight === 'string') {
const { element, gridHeight } = rest as ViewportRowsArgsStringHeight;
Comment thread
fastfrwrd marked this conversation as resolved.
Outdated
if (!element) {
throw new Error(
'props.element is required when rowHeight is a string. This is needed to calculate the position of the rows.'
);
}
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 ?? 0,
gridTemplateRows: ` repeat(${rows.length}, ${rowHeight})`,
getRowTop(rowIdx: number) {
if (!element) return -1;
const cell = getRowElementFirstCell(element, rowIdx);
if (!cell) return -1;
return cell.getBoundingClientRect().top + element.scrollTop;
},
getRowHeight(rowIdx: number) {
if (!element) return -1;
const cell = getRowElementFirstCell(element, rowIdx);
if (!cell) return -1;
return cell.clientHeight;
},
findRowIdx(offset: number) {
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,7 +180,7 @@
return 0;
}
};
}, [rowHeight, rows]);
}, [element, gridHeight, rowHeight, rows]);

Check failure on line 183 in src/hooks/useViewportRows.ts

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'gridHeight'.

Check failure on line 183 in src/hooks/useViewportRows.ts

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'element'. Did you mean 'Element'?

let rowOverscanStartIdx = 0;
let rowOverscanEndIdx = rows.length - 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,64 @@ 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
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.');

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