Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for variable row heights #2384

Merged
merged 20 commits into from
May 4, 2021
Merged
54 changes: 36 additions & 18 deletions src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ import type {
FillEvent,
PasteEvent,
CellNavigationMode,
SortDirection
SortDirection,
RowHeightArgs
} from './types';

interface SelectCellState extends Position {
Expand Down Expand Up @@ -100,7 +101,7 @@ export interface DataGridProps<R, SR = unknown> extends SharedDivProps {
* Dimensions props
*/
/** The height of each row in pixels */
rowHeight?: number;
rowHeight?: number | ((args: RowHeightArgs<R>) => number);
/** The height of the header row in pixels */
headerRowHeight?: number;
/** The height of the header filter row in pixels */
Expand Down Expand Up @@ -181,9 +182,9 @@ function DataGrid<R, SR>({
onRowsChange,
// Dimensions props
rowHeight = 35,
headerRowHeight = rowHeight,
headerRowHeight = typeof rowHeight === 'number' ? rowHeight : 35,
headerFiltersHeight = 45,
summaryRowHeight = rowHeight,
summaryRowHeight = typeof rowHeight === 'number' ? rowHeight : 35,
// Feature props
selectedRows,
onSelectedRowsChange,
Expand Down Expand Up @@ -280,7 +281,17 @@ function DataGrid<R, SR>({
enableVirtualization
});

const { rowOverscanStartIdx, rowOverscanEndIdx, rows, rowsCount, isGroupRow } = useViewportRows({
const {
rowOverscanStartIdx,
rowOverscanEndIdx,
rows,
rowsCount,
totalRowHeight,
isGroupRow,
getRowTop,
getRowHeight,
findRowIdx
} = useViewportRows({
rawRows,
groupBy,
rowGrouper,
Expand Down Expand Up @@ -335,7 +346,7 @@ function DataGrid<R, SR>({
const { current } = gridRef;
if (!current) return;
current.scrollTo({
top: rowIdx * rowHeight,
top: getRowTop(rowIdx),
behavior: 'smooth'
});
},
Expand Down Expand Up @@ -726,12 +737,14 @@ function DataGrid<R, SR>({
}

if (typeof rowIdx === 'number') {
if (rowIdx * rowHeight < scrollTop) {
const rowTop = getRowTop(rowIdx);
const rowHeight = getRowHeight(rowIdx);
if (rowTop < scrollTop) {
// at top boundary, scroll to the row's top
current.scrollTop = rowIdx * rowHeight;
} else if ((rowIdx + 1) * rowHeight > scrollTop + clientHeight) {
current.scrollTop = rowTop;
} else if (rowTop + rowHeight > scrollTop + clientHeight) {
// at bottom boundary, scroll the next row's top to the bottom of the viewport
current.scrollTop = (rowIdx + 1) * rowHeight - clientHeight;
current.scrollTop = rowTop + rowHeight - clientHeight;
}
}
}
Expand Down Expand Up @@ -784,10 +797,14 @@ function DataGrid<R, SR>({
// If row is selected then move focus to the last row.
if (isRowSelected) return { idx, rowIdx: rows.length - 1 };
return ctrlKey ? { idx: columns.length - 1, rowIdx: rows.length - 1 } : { idx: columns.length - 1, rowIdx };
case 'PageUp':
return { idx, rowIdx: rowIdx - Math.floor(clientHeight / rowHeight) };
case 'PageDown':
return { idx, rowIdx: rowIdx + Math.floor(clientHeight / rowHeight) };
case 'PageUp': {
const nextRowY = getRowTop(rowIdx) + getRowHeight(rowIdx) - clientHeight;
return { idx, rowIdx: nextRowY > 0 ? findRowIdx(nextRowY) : 0 };
}
case 'PageDown': {
const nextRowY = getRowTop(rowIdx) + clientHeight;
return { idx, rowIdx: nextRowY < totalRowHeight ? findRowIdx(nextRowY) : rows.length - 1 };
}
default:
return selectedPosition;
}
Expand Down Expand Up @@ -853,7 +870,7 @@ function DataGrid<R, SR>({
onKeyDown: handleKeyDown,
editorProps: {
editorPortalTarget,
rowHeight,
rowHeight: getRowHeight(selectedPosition.rowIdx),
row: selectedPosition.row,
onRowChange: handleEditorRowChange,
onClose: handleOnClose
Expand All @@ -877,7 +894,7 @@ function DataGrid<R, SR>({
let startRowIndex = 0;
for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) {
const row = rows[rowIdx];
const top = rowIdx * rowHeight + totalHeaderHeight;
const top = getRowTop(rowIdx) + totalHeaderHeight;
if (isGroupRow(row)) {
({ startRowIndex } = row);
rowElements.push(
Expand All @@ -893,6 +910,7 @@ function DataGrid<R, SR>({
childRows={row.childRows}
rowIdx={rowIdx}
top={top}
height={getRowHeight(rowIdx)}
level={row.level}
isExpanded={row.isExpanded}
selectedCellIdx={selectedPosition.rowIdx === rowIdx ? selectedPosition.idx : undefined}
Expand Down Expand Up @@ -927,6 +945,7 @@ function DataGrid<R, SR>({
onRowClick={onRowClick}
rowClass={rowClass}
top={top}
height={getRowHeight(rowIdx)}
copiedCellIdx={copiedCell !== null && copiedCell.row === row ? columns.findIndex(c => c.key === copiedCell.columnKey) : undefined}
draggedOverCellIdx={getDraggedOverCellIdx(rowIdx)}
setDraggedOverRowIdx={isDragging ? setDraggedOverRowIdx : undefined}
Expand Down Expand Up @@ -968,7 +987,6 @@ function DataGrid<R, SR>({
'--header-row-height': `${headerRowHeight}px`,
'--filter-row-height': `${headerFiltersHeight}px`,
'--row-width': `${totalColumnWidth}px`,
'--row-height': `${rowHeight}px`,
'--summary-row-height': `${summaryRowHeight}px`,
...layoutCssVars
} as unknown as React.CSSProperties}
Expand Down Expand Up @@ -1003,7 +1021,7 @@ function DataGrid<R, SR>({
onKeyDown={handleKeyDown}
onFocus={onGridFocus}
/>
<div style={{ height: Math.max(rows.length * rowHeight, clientHeight) }} />
<div style={{ height: Math.max(totalRowHeight, clientHeight) }} />
{getViewportRows()}
{summaryRows?.map((row, rowIdx) => (
<SummaryRow<R, SR>
Expand Down
8 changes: 7 additions & 1 deletion src/GroupRow.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CSSProperties } from 'react';
import { memo } from 'react';
import clsx from 'clsx';

Expand All @@ -13,6 +14,7 @@ export interface GroupRowRendererProps<R, SR = unknown> extends Omit<React.HTMLA
childRows: readonly R[];
rowIdx: number;
top: number;
height: number;
level: number;
selectedCellIdx?: number;
isExpanded: boolean;
Expand All @@ -29,6 +31,7 @@ function GroupedRow<R, SR>({
childRows,
rowIdx,
top,
height,
level,
isExpanded,
selectedCellIdx,
Expand Down Expand Up @@ -59,7 +62,10 @@ function GroupedRow<R, SR>({
}
)}
onClick={selectGroup}
style={{ top }}
style={{
top,
'--row-height': `${height}px`
} as unknown as CSSProperties}
{...props}
>
{viewportColumns.map(column => (
Expand Down
8 changes: 6 additions & 2 deletions src/Row.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { memo, forwardRef } from 'react';
import type { RefAttributes } from 'react';
import type { RefAttributes, CSSProperties } from 'react';
import clsx from 'clsx';

import { groupRowSelectedClassname, rowClassname, rowSelectedClassname } from './style';
Expand All @@ -24,6 +24,7 @@ function Row<R, SR = unknown>({
setDraggedOverRowIdx,
onMouseEnter,
top,
height,
onRowChange,
selectCell,
selectRow,
Expand Down Expand Up @@ -100,7 +101,10 @@ function Row<R, SR = unknown>({
ref={ref}
className={className}
onMouseEnter={handleDragEnter}
style={{ top }}
style={{
top,
'--row-height': `${height}px`
} as unknown as CSSProperties}
{...props}
>
{cells}
Expand Down
80 changes: 74 additions & 6 deletions src/hooks/useViewportRows.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useMemo } from 'react';
import type { GroupRow, GroupByDictionary } from '../types';
import type { GroupRow, GroupByDictionary, RowHeightArgs } from '../types';

const RENDER_BACTCH_SIZE = 8;

interface ViewportRowsArgs<R> {
rawRows: readonly R[];
rowHeight: number;
rowHeight: number | ((args: RowHeightArgs<R>) => number);
clientHeight: number;
scrollTop: number;
groupBy: readonly string[];
Expand Down Expand Up @@ -94,20 +94,84 @@ export function useViewportRows<R>({
}
}, [expandedGroupIds, groupedRows, rawRows]);

const { totalRowHeight, getRowTop, getRowHeight, findRowIdx } = useMemo(() => {
if (typeof rowHeight === 'number') {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change does not impact the performance if rowHeight is a number.

return {
totalRowHeight: rowHeight * rows.length,
getRowTop: (rowIdx: number) => rowIdx * rowHeight,
getRowHeight: () => rowHeight,
findRowIdx: (offset: number) => Math.floor(offset / rowHeight)
};
}

let totalRowHeight = 0;
// Calcule the height of all the rows upfront. This can cause performance issues
Copy link
Contributor Author

@amanmahajan7 amanmahajan7 May 3, 2021

Choose a reason for hiding this comment

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

I did not notice any performance issues even on a large grid. Tried editing, colspan and a few other features. This can be slow if rowHeight itself is expensive

// and we can consider using a similar approach as react-window
// https://github.com/bvaughn/react-window/blob/master/src/VariableSizeList.js#L68
const rowPositions = rows.map((row: R | GroupRow<R>) => {
const currentRowHeight = isGroupRow(row)
? rowHeight({ type: 'GROUP', row })
: rowHeight({ type: 'ROW', row });
const position = { top: totalRowHeight, height: currentRowHeight };
totalRowHeight += currentRowHeight;
return position;
});

const validateRowIdx = (rowIdx: number) => {
if (rowIdx < 0) {
return 0;
}

if (rowIdx >= rows.length) {
return rows.length - 1;
}

return rowIdx;
amanmahajan7 marked this conversation as resolved.
Show resolved Hide resolved
};

return {
totalRowHeight,
getRowTop: (rowIdx: number) => rowPositions[validateRowIdx(rowIdx)].top,
getRowHeight: (rowIdx: number) => rowPositions[validateRowIdx(rowIdx)].height,
findRowIdx(offset: number) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use binary search on a sorted array.

let start = 0;
let end = rowPositions.length - 1;
while (start <= end) {
const middle = start + Math.floor((end - start) / 2);
const currentOffset = rowPositions[middle].top;

if (currentOffset === offset) return middle;

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

if (start > end) return end;
}
return 0;
}
};
}, [isGroupRow, rowHeight, rows]);

if (!enableVirtualization) {
return {
rowOverscanStartIdx: 0,
rowOverscanEndIdx: rows.length - 1,
rows,
rowsCount,
isGroupRow
totalRowHeight,
isGroupRow,
getRowTop,
getRowHeight,
findRowIdx
};
}

const overscanThreshold = 4;
const rowVisibleStartIdx = Math.floor(scrollTop / rowHeight);
const rowVisibleEndIdx = Math.min(rows.length - 1, Math.floor((scrollTop + clientHeight) / rowHeight));
const rowVisibleStartIdx = findRowIdx(scrollTop);
const rowVisibleEndIdx = Math.min(rows.length - 1, findRowIdx(scrollTop + clientHeight));
const rowOverscanStartIdx = Math.max(0, Math.floor((rowVisibleStartIdx - overscanThreshold) / RENDER_BACTCH_SIZE) * RENDER_BACTCH_SIZE);
const rowOverscanEndIdx = Math.min(rows.length - 1, Math.ceil((rowVisibleEndIdx + overscanThreshold) / RENDER_BACTCH_SIZE) * RENDER_BACTCH_SIZE);

Expand All @@ -116,6 +180,10 @@ export function useViewportRows<R>({
rowOverscanEndIdx,
rows,
rowsCount,
isGroupRow
totalRowHeight,
isGroupRow,
getRowTop,
getRowHeight,
findRowIdx
};
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ export type {
PasteEvent,
CellNavigationMode,
SortDirection,
ColSpanArgs
ColSpanArgs,
RowHeightArgs
} from './types';
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export interface RowRendererProps<TRow, TSummaryRow = unknown> extends Omit<Reac
lastFrozenColumnIndex: number;
isRowSelected: boolean;
top: number;
height: number;
selectedCellProps?: EditCellProps<TRow> | SelectedCellProps;
onRowChange: (rowIdx: number, row: TRow) => void;
onRowClick?: (rowIdx: number, row: TRow, column: CalculatedColumn<TRow, TSummaryRow>) => void;
Expand Down Expand Up @@ -239,3 +240,11 @@ export type ColSpanArgs<R, SR> = {
type: 'SUMMARY';
row: SR;
};

export type RowHeightArgs<R> = {
type: 'ROW';
row: R;
} | {
type: 'GROUP';
row: GroupRow<R>;
};
10 changes: 5 additions & 5 deletions stories/demos/Grouping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ function createRows(): readonly Row[] {
for (let i = 1; i < 10000; i++) {
rows.push({
id: i,
year: 2015 + faker.random.number(3),
year: 2015 + faker.datatype.number(3),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

faker.random is deprecated

country: faker.address.country(),
sport: sports[faker.random.number(sports.length - 1)],
sport: sports[faker.datatype.number(sports.length - 1)],
athlete: faker.name.findName(),
gold: faker.random.number(5),
silver: faker.random.number(5),
bronze: faker.random.number(5)
gold: faker.datatype.number(5),
silver: faker.datatype.number(5),
bronze: faker.datatype.number(5)
});
}

Expand Down
Loading