Skip to content
Merged
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
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);
const isGroupRowSelected = isSelectable && row.childRows.every(cr => selectedRows?.has(rowKeyGetter!(cr)));
Expand All @@ -895,6 +912,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 @@ -929,6 +947,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 @@ -970,7 +989,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 @@ -1005,7 +1023,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 @@ -58,7 +61,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 } from './style';
Expand All @@ -24,6 +24,7 @@ function Row<R, SR = unknown>({
setDraggedOverRowIdx,
onMouseEnter,
top,
height,
onRowChange,
selectCell,
selectRow,
Expand Down Expand Up @@ -99,7 +100,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
72 changes: 66 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,76 @@ export function useViewportRows<R>({
}
}, [expandedGroupIds, groupedRows, rawRows]);

const { totalRowHeight, getRowTop, getRowHeight, findRowIdx } = useMemo(() => {
if (typeof rowHeight === 'number') {
Copy link
Collaborator 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
Collaborator 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) => {
return Math.max(0, Math.min(rows.length - 1, rowIdx));
};

return {
totalRowHeight,
getRowTop: (rowIdx: number) => rowPositions[validateRowIdx(rowIdx)].top,
getRowHeight: (rowIdx: number) => rowPositions[validateRowIdx(rowIdx)].height,
findRowIdx(offset: number) {
Copy link
Collaborator 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 +172,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
Collaborator 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