Skip to content

Commit

Permalink
Add support for variable row heights (adazzle#2384)
Browse files Browse the repository at this point in the history
* Change rowHeight to a function to support variable row heights

* Fix hooks order

* Remove memo

* Add a comment

* Fix tests

* Fix types

* Cleanup

* Use a single array

* Fix pageup/pagedown

* Update src/DataGrid.tsx

Co-authored-by: Nicolas Stepien <567105+nstepien@users.noreply.github.com>

* Update src/hooks/useViewportRows.ts

Co-authored-by: Nicolas Stepien <567105+nstepien@users.noreply.github.com>

* newScrollTop -> nextRowY

* move/deduplicate getRowTop(rowIdx) and getRowHeight(rowIdx) calls outside the ifs

* Validate rowIdx

* Update src/hooks/useViewportRows.ts

Co-authored-by: Nicolas Stepien <567105+nstepien@users.noreply.github.com>

* Fix typo

* Add rowHeight tests

* typo

Co-authored-by: Nicolas Stepien <567105+nstepien@users.noreply.github.com>
  • Loading branch information
2 people authored and gernotkogler committed May 13, 2021
1 parent e845d20 commit 2eb35ab
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 33 deletions.
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') {
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
// 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) {
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),
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

0 comments on commit 2eb35ab

Please sign in to comment.