diff --git a/examples/scripts/example31-isScrolling.js b/examples/scripts/example31-isScrolling.js
index 6547f7ac64..c26491f7d5 100644
--- a/examples/scripts/example31-isScrolling.js
+++ b/examples/scripts/example31-isScrolling.js
@@ -1,6 +1,6 @@
+import React, { useRef, useState } from 'react';
import ReactDataGrid from 'react-data-grid';
import exampleWrapper from '../components/exampleWrapper';
-import React from 'react';
import { AreaChart, Area } from 'Recharts';
const getRandom = (min, max) => {
@@ -10,10 +10,17 @@ const getRandom = (min, max) => {
};
const ExpensiveFormatter = ({ isScrolling }) => {
- if (isScrolling) {
+ const isReady = useRef(!isScrolling);
+ const [items] = useState(() => {
+ return [...Array(1000).keys()].map(i => ({ name: `Page ${i}`, uv: getRandom(0, 4000), pv: getRandom(0, 4000), amt: getRandom(0, 4000) })).slice(0, 50);
+ });
+
+ if (isScrolling && !isReady.current) {
return
is scrolling
;
}
- const items = [...Array(1000).keys()].map(i => ({ name: `Page ${i}`, uv: getRandom(0, 4000), pv: getRandom(0, 4000), amt: getRandom(0, 4000) })).slice(0, 50);
+
+ isReady.current = true;
+
return (
= Pick,
+type SharedGridProps = Pick,
| 'rowKey'
| 'rowGetter'
| 'rowsCount'
+| 'columnMetrics'
| 'selectedRows'
-| 'onSelectedRowsChange'
+| 'onRowSelectionChange'
| 'rowRenderer'
| 'cellMetaData'
| 'rowHeight'
@@ -30,204 +31,294 @@ type SharedViewportProps = Pick,
| 'RowsContainer'
| 'editorPortalTarget'
| 'interactionMasksMetaData'
+| 'overscanRowCount'
+| 'overscanColumnCount'
+| 'enableIsScrolling'
+| 'onCanvasKeydown'
+| 'onCanvasKeyup'
>;
-type SharedViewportState = ScrollPosition & HorizontalRangeToRender & VerticalRangeToRender;
-
-export interface CanvasProps extends SharedViewportProps, SharedViewportState {
- columns: CalculatedColumn[];
+export interface CanvasProps extends SharedGridProps {
height: number;
- width: number;
- lastFrozenColumnIndex: number;
- isScrolling?: boolean;
- onScroll(position: ScrollPosition): void;
+ onScroll(position: ScrollState): void;
}
-type RendererProps = Pick, 'columns' | 'cellMetaData' | 'colOverscanEndIdx' | 'colOverscanStartIdx' | 'lastFrozenColumnIndex' | 'isScrolling'> & {
+interface RendererProps extends Pick, 'cellMetaData' | 'onRowSelectionChange'> {
ref(row: (RowRenderer & React.Component>) | null): void;
key: number;
idx: number;
+ columns: CalculatedColumn[];
+ lastFrozenColumnIndex: number;
row: R;
subRowDetails?: SubRowDetails;
height: number;
isRowSelected: boolean;
- onRowSelectionChange(rowIdx: number, row: R, checked: boolean, isShiftClick: boolean): void;
scrollLeft: number;
-};
+ isScrolling: boolean;
+ colOverscanStartIdx: number;
+ colOverscanEndIdx: number;
+}
+
+export default function Canvas({
+ cellMetaData,
+ cellNavigationMode,
+ columnMetrics,
+ contextMenu,
+ editorPortalTarget,
+ enableCellAutoFocus,
+ enableCellSelect,
+ enableIsScrolling,
+ eventBus,
+ getSubRowDetails,
+ height,
+ interactionMasksMetaData,
+ onCanvasKeydown,
+ onCanvasKeyup,
+ onRowSelectionChange,
+ onScroll,
+ overscanColumnCount,
+ overscanRowCount,
+ rowGetter,
+ rowGroupRenderer,
+ rowHeight,
+ rowKey,
+ rowRenderer,
+ RowsContainer = Fragment,
+ rowsCount,
+ scrollToRowIndex,
+ selectedRows
+}: CanvasProps) {
+ const [scrollTop, setScrollTop] = useState(0);
+ const [scrollLeft, setScrollLeft] = useState(0);
+ const [scrollDirection, setScrollDirection] = useState(SCROLL_DIRECTION.NONE);
+ const [isScrolling, setIsScrolling] = useState(false);
+ const canvas = useRef(null);
+ const interactionMasks = useRef>(null);
+ const prevScrollToRowIndex = useRef();
+ const resetScrollStateTimeoutId = useRef(null);
+ const [rows] = useState(() => new Map & React.Component>>());
+ const clientHeight = getClientHeight();
+
+ const { rowOverscanStartIdx, rowOverscanEndIdx } = useMemo(() => {
+ return getVerticalRangeToRender({
+ height: clientHeight,
+ rowHeight,
+ scrollTop,
+ rowsCount,
+ scrollDirection,
+ overscanRowCount
+ });
+ }, [clientHeight, overscanRowCount, rowHeight, rowsCount, scrollDirection, scrollTop]);
+
+ const { colOverscanStartIdx, colOverscanEndIdx, colVisibleStartIdx, colVisibleEndIdx } = useMemo(() => {
+ return getHorizontalRangeToRender({
+ columnMetrics,
+ scrollLeft,
+ scrollDirection,
+ overscanColumnCount
+ });
+ }, [columnMetrics, overscanColumnCount, scrollDirection, scrollLeft]);
+
+ useEffect(() => {
+ return eventBus.subscribe(EventTypes.SCROLL_TO_COLUMN, idx => scrollToColumn(idx, columnMetrics.columns));
+ }, [columnMetrics.columns, eventBus]);
+
+ useEffect(() => {
+ if (prevScrollToRowIndex.current === scrollToRowIndex) return;
+ prevScrollToRowIndex.current = scrollToRowIndex;
+ const { current } = canvas;
+ if (typeof scrollToRowIndex === 'number' && current) {
+ current.scrollTop = scrollToRowIndex * rowHeight;
+ }
+ }, [rowHeight, scrollToRowIndex]);
-export default class Canvas extends React.PureComponent> {
- static displayName = 'Canvas';
+ function handleScroll(e: React.UIEvent) {
+ const { scrollLeft: newScrollLeft, scrollTop: newScrollTop } = e.currentTarget;
+ // Freeze columns on legacy browsers
+ setComponentsScrollLeft(newScrollLeft);
- private readonly canvas = React.createRef();
- private readonly interactionMasks = React.createRef>();
- private readonly rows = new Map & React.Component>>();
- private lastSelectedRowIdx = -1;
- private unsubscribeScrollToColumn?(): void;
+ if (enableIsScrolling) {
+ setIsScrolling(true);
+ resetScrollStateAfterDelay();
+ }
- componentDidMount() {
- this.unsubscribeScrollToColumn = this.props.eventBus.subscribe(EventTypes.SCROLL_TO_COLUMN, this.scrollToColumn);
+ const scrollDirection = getScrollDirection(
+ { scrollLeft, scrollTop },
+ { scrollLeft: newScrollLeft, scrollTop: newScrollTop }
+ );
+ setScrollLeft(newScrollLeft);
+ setScrollTop(newScrollTop);
+ setScrollDirection(scrollDirection);
+ onScroll({ scrollLeft: newScrollLeft, scrollTop: newScrollTop, scrollDirection });
}
- componentWillUnmount() {
- this.unsubscribeScrollToColumn!();
+ function resetScrollStateAfterDelay() {
+ clearScrollTimer();
+ resetScrollStateTimeoutId.current = window.setTimeout(
+ resetScrollStateAfterDelayCallback,
+ 150
+ );
}
- componentDidUpdate(prevProps: CanvasProps) {
- const { scrollToRowIndex } = this.props;
- if (scrollToRowIndex && prevProps.scrollToRowIndex !== scrollToRowIndex) {
- this.scrollToRow(scrollToRowIndex);
+ function clearScrollTimer() {
+ if (resetScrollStateTimeoutId.current !== null) {
+ window.clearTimeout(resetScrollStateTimeoutId.current);
+ resetScrollStateTimeoutId.current = null;
}
}
- handleScroll = (e: React.UIEvent) => {
- const { scrollLeft, scrollTop } = e.currentTarget;
- // Freeze columns on legacy browsers
- this.setScrollLeft(scrollLeft);
- this.props.onScroll({ scrollLeft, scrollTop });
- };
+ function resetScrollStateAfterDelayCallback() {
+ resetScrollStateTimeoutId.current = null;
+ setIsScrolling(false);
+ }
- onHitBottomCanvas = () => {
- const { current } = this.canvas;
+ function getClientHeight() {
+ if (canvas.current) return canvas.current.clientHeight;
+ const scrollbarSize = columnMetrics.totalColumnWidth > columnMetrics.viewportWidth ? getScrollbarSize() : 0;
+ return height - scrollbarSize;
+ }
+
+ function onHitBottomCanvas({ rowIdx }: Position) {
+ const { current } = canvas;
if (current) {
- current.scrollTop += this.props.rowHeight + this.getClientScrollTopOffset(current);
+ // We do not need to check for the index being in range, as the scrollTop setter will adequately clamp the value.
+ current.scrollTop = (rowIdx + 1) * rowHeight - clientHeight;
}
- };
+ }
- onHitTopCanvas = () => {
- const { current } = this.canvas;
+ function onHitTopCanvas({ rowIdx }: Position) {
+ const { current } = canvas;
if (current) {
- current.scrollTop -= this.props.rowHeight - this.getClientScrollTopOffset(current);
+ current.scrollTop = rowIdx * rowHeight;
}
- };
-
- handleHitColummBoundary = ({ idx }: Position) => {
- this.scrollToColumn(idx);
- };
+ }
- scrollToRow(scrollToRowIndex: number) {
- const { current } = this.canvas;
- if (!current) return;
- const { rowHeight, rowsCount, height } = this.props;
- current.scrollTop = Math.min(
- scrollToRowIndex * rowHeight,
- rowsCount * rowHeight - height
- );
+ function handleHitColummBoundary({ idx }: Position) {
+ scrollToColumn(idx, columnMetrics.columns);
}
- scrollToColumn(idx: number) {
- const { current } = this.canvas;
+ function scrollToColumn(idx: number, columns: CalculatedColumn[]) {
+ const { current } = canvas;
if (!current) return;
const { scrollLeft, clientWidth } = current;
- const newScrollLeft = getColumnScrollPosition(this.props.columns, idx, scrollLeft, clientWidth);
+ const newScrollLeft = getColumnScrollPosition(columns, idx, scrollLeft, clientWidth);
if (newScrollLeft !== 0) {
current.scrollLeft = scrollLeft + newScrollLeft;
}
}
- getRows(rowOverscanStartIdx: number, rowOverscanEndIdx: number) {
+ function getRows() {
const rows = [];
- let i = rowOverscanStartIdx;
- while (i < rowOverscanEndIdx) {
- const row = this.props.rowGetter(i);
- let subRowDetails: SubRowDetails | undefined;
- if (this.props.getSubRowDetails) {
- subRowDetails = this.props.getSubRowDetails(row);
- }
- rows.push({ row, subRowDetails });
- i++;
- }
- return rows;
- }
- getClientScrollTopOffset(node: HTMLDivElement) {
- const { rowHeight } = this.props;
- const scrollVariation = node.scrollTop % rowHeight;
- return scrollVariation > 0 ? rowHeight - scrollVariation : 0;
- }
+ for (let idx = rowOverscanStartIdx; idx <= rowOverscanEndIdx; idx++) {
+ rows.push(renderRow(idx));
+ }
- isRowSelected(row: R): boolean {
- return this.props.selectedRows !== undefined && this.props.selectedRows.has(row[this.props.rowKey]);
+ return rows;
}
- handleRowSelectionChange = (rowIdx: number, row: R, checked: boolean, isShiftClick: boolean) => {
- if (!this.props.onSelectedRowsChange) return;
-
- const { rowKey } = this.props;
- const newSelectedRows = new Set(this.props.selectedRows);
-
- if (checked) {
- newSelectedRows.add(row[rowKey]);
- const previousRowIdx = this.lastSelectedRowIdx;
- this.lastSelectedRowIdx = rowIdx;
- if (isShiftClick && previousRowIdx !== -1 && previousRowIdx !== rowIdx) {
- const step = Math.sign(rowIdx - previousRowIdx);
- for (let i = previousRowIdx + step; i !== rowIdx; i += step) {
- newSelectedRows.add(this.props.rowGetter(i)[rowKey]);
+ function renderRow(idx: number) {
+ const row = rowGetter(idx);
+ const rendererProps: RendererProps = {
+ key: idx,
+ ref(row) {
+ if (row) {
+ rows.set(idx, row);
+ } else {
+ rows.delete(idx);
}
+ },
+ idx,
+ row,
+ height: rowHeight,
+ columns: columnMetrics.columns,
+ isRowSelected: isRowSelected(row),
+ onRowSelectionChange,
+ cellMetaData,
+ subRowDetails: getSubRowDetails ? getSubRowDetails(row) : undefined,
+ colOverscanStartIdx,
+ colOverscanEndIdx,
+ lastFrozenColumnIndex: columnMetrics.lastFrozenColumnIndex,
+ isScrolling,
+ scrollLeft
+ };
+ const { __metaData } = row as RowData;
+
+ if (__metaData) {
+ if (__metaData.getRowRenderer) {
+ return __metaData.getRowRenderer(rendererProps, idx);
+ }
+ if (__metaData.isGroup) {
+ return renderGroupRow(rendererProps);
}
- } else {
- newSelectedRows.delete(row[rowKey]);
- this.lastSelectedRowIdx = -1;
}
- this.props.onSelectedRowsChange(newSelectedRows);
- };
+ if (rowRenderer) {
+ return renderCustomRowRenderer(rendererProps);
+ }
+
+ return {...rendererProps} />;
+ }
+
+ function isRowSelected(row: R): boolean {
+ return selectedRows !== undefined && selectedRows.has(row[rowKey]);
+ }
- setScrollLeft(scrollLeft: number) {
+ function setComponentsScrollLeft(scrollLeft: number) {
if (isPositionStickySupported()) return;
- const { current } = this.interactionMasks;
+ const { current } = interactionMasks;
if (current) {
current.setScrollLeft(scrollLeft);
}
- this.rows.forEach((r, idx) => {
- const row = this.getRowByRef(idx);
+ rows.forEach((r, idx) => {
+ const row = getRowByRef(idx);
if (row && row.setScrollLeft) {
row.setScrollLeft(scrollLeft);
}
});
}
- getRowByRef = (i: number) => {
+ function getRowByRef(i: number) {
// check if wrapped with React DND drop target
- if (!this.rows.has(i)) return;
+ if (!rows.has(i)) return;
- const row = this.rows.get(i)!;
+ const row = rows.get(i)!;
const wrappedRow = row.getDecoratedComponentInstance ? row.getDecoratedComponentInstance(i) : null;
return wrappedRow ? wrappedRow.row : row;
- };
+ }
- getRowTop = (rowIdx: number) => {
- const row = this.getRowByRef(rowIdx);
+ function getRowTop(rowIdx: number) {
+ const row = getRowByRef(rowIdx);
if (row && row.getRowTop) {
return row.getRowTop();
}
- return this.props.rowHeight * rowIdx;
- };
+ return rowHeight * rowIdx;
+ }
- getRowHeight = (rowIdx: number) => {
- const row = this.getRowByRef(rowIdx);
+ function getRowHeight(rowIdx: number) {
+ const row = getRowByRef(rowIdx);
if (row && row.getRowHeight) {
return row.getRowHeight();
}
- return this.props.rowHeight;
- };
+ return rowHeight;
+ }
- getRowColumns = (rowIdx: number) => {
- const row = this.getRowByRef(rowIdx);
- return row && row.props ? row.props.columns : this.props.columns;
- };
+ function getRowColumns(rowIdx: number) {
+ const row = getRowByRef(rowIdx);
+ return row && row.props ? row.props.columns : columnMetrics.columns;
+ }
- renderCustomRowRenderer(props: RendererProps) {
- const { ref, ...otherProps } = props;
- const CustomRowRenderer = this.props.rowRenderer!;
+ function renderCustomRowRenderer(rowProps: RendererProps) {
+ const { ref, ...otherProps } = rowProps;
+ const CustomRowRenderer = rowRenderer!;
const customRowRendererProps = { ...otherProps, renderBaseRow: (p: RowRendererProps) =>
};
if (isElement(CustomRowRenderer)) {
if (CustomRowRenderer.type === Row) {
// In the case where Row is specified as the custom render, ensure the correct ref is passed
- return {...props} />;
+ return {...rowProps} />;
}
return React.cloneElement(CustomRowRenderer, customRowRendererProps);
}
@@ -235,9 +326,9 @@ export default class Canvas extends React.PureComponent> {
return ;
}
- renderGroupRow(props: RendererProps) {
- const { ref, columns, ...rowGroupProps } = props;
- const row = props.row as RowData;
+ function renderGroupRow(groupRowProps: RendererProps) {
+ const { ref, columns, ...rowGroupProps } = groupRowProps;
+ const row = groupRowProps.row as RowData;
return (
extends React.PureComponent> {
{...row.__metaData!}
columns={columns as CalculatedColumn[]}
name={row.name!}
- eventBus={this.props.eventBus}
- renderer={this.props.rowGroupRenderer}
+ eventBus={eventBus}
+ renderer={rowGroupRenderer}
renderBaseRow={(p: RowRendererProps) =>
}
/>
);
}
- renderRow(props: RendererProps) {
- const row = props.row as RowData;
-
- if (row.__metaData && row.__metaData.getRowRenderer) {
- return row.__metaData.getRowRenderer(this.props, props.idx);
- }
- if (row.__metaData && row.__metaData.isGroup) {
- return this.renderGroupRow(props);
- }
-
- if (this.props.rowRenderer) {
- return this.renderCustomRowRenderer(props);
- }
-
- return {...props} />;
- }
-
- renderPlaceholder(key: string, height: number) {
- return (
-
- );
- }
-
- render() {
- const { rowOverscanStartIdx, rowOverscanEndIdx, cellMetaData, columns, colOverscanStartIdx, colOverscanEndIdx, colVisibleStartIdx, colVisibleEndIdx, lastFrozenColumnIndex, rowHeight, rowsCount, width, height, rowGetter, contextMenu, isScrolling, scrollLeft } = this.props;
- const RowsContainer = this.props.RowsContainer || Fragment;
-
- const rows = this.getRows(rowOverscanStartIdx, rowOverscanEndIdx)
- .map(({ row, subRowDetails }, idx) => {
- const rowIdx = rowOverscanStartIdx + idx;
- return row && this.renderRow({
- key: rowIdx,
- ref: (row: (RowRenderer & React.Component>) | null) => {
- if (row) {
- this.rows.set(rowIdx, row);
- } else {
- this.rows.delete(rowIdx);
- }
- },
- idx: rowIdx,
- row,
- height: rowHeight,
- columns,
- isRowSelected: this.isRowSelected(row),
- onRowSelectionChange: this.handleRowSelectionChange,
- cellMetaData,
- subRowDetails,
- colOverscanStartIdx,
- colOverscanEndIdx,
- lastFrozenColumnIndex,
- isScrolling,
- scrollLeft
- });
- });
-
- if (rowOverscanStartIdx > 0) {
- rows.unshift(this.renderPlaceholder('top', rowOverscanStartIdx * rowHeight));
- }
-
- if (rowsCount - rowOverscanEndIdx > 0) {
- rows.push(this.renderPlaceholder('bottom', (rowsCount - rowOverscanEndIdx) * rowHeight));
- }
-
- return (
-
-
- ref={this.interactionMasks}
- rowGetter={rowGetter}
- rowsCount={rowsCount}
- rowHeight={rowHeight}
- columns={columns}
- rowVisibleStartIdx={this.props.rowVisibleStartIdx}
- rowVisibleEndIdx={this.props.rowVisibleEndIdx}
- colVisibleStartIdx={colVisibleStartIdx}
- colVisibleEndIdx={colVisibleEndIdx}
- enableCellSelect={this.props.enableCellSelect}
- enableCellAutoFocus={this.props.enableCellAutoFocus}
- cellNavigationMode={this.props.cellNavigationMode}
- eventBus={this.props.eventBus}
- contextMenu={this.props.contextMenu}
- onHitBottomBoundary={this.onHitBottomCanvas}
- onHitTopBoundary={this.onHitTopCanvas}
- onHitLeftBoundary={this.handleHitColummBoundary}
- onHitRightBoundary={this.handleHitColummBoundary}
- scrollLeft={scrollLeft}
- scrollTop={this.props.scrollTop}
- getRowHeight={this.getRowHeight}
- getRowTop={this.getRowTop}
- getRowColumns={this.getRowColumns}
- editorPortalTarget={this.props.editorPortalTarget}
- {...this.props.interactionMasksMetaData}
- />
-
-
- );
- }
+ const paddingTop = rowOverscanStartIdx * rowHeight;
+ const paddingBottom = (rowsCount - 1 - rowOverscanEndIdx) * rowHeight;
+
+ return (
+
+
+ ref={interactionMasks}
+ rowGetter={rowGetter}
+ rowsCount={rowsCount}
+ rowHeight={rowHeight}
+ columns={columnMetrics.columns}
+ height={clientHeight}
+ colVisibleStartIdx={colVisibleStartIdx}
+ colVisibleEndIdx={colVisibleEndIdx}
+ enableCellSelect={enableCellSelect}
+ enableCellAutoFocus={enableCellAutoFocus}
+ cellNavigationMode={cellNavigationMode}
+ eventBus={eventBus}
+ contextMenu={contextMenu}
+ onHitBottomBoundary={onHitBottomCanvas}
+ onHitTopBoundary={onHitTopCanvas}
+ onHitLeftBoundary={handleHitColummBoundary}
+ onHitRightBoundary={handleHitColummBoundary}
+ scrollLeft={scrollLeft}
+ scrollTop={scrollTop}
+ getRowHeight={getRowHeight}
+ getRowTop={getRowTop}
+ getRowColumns={getRowColumns}
+ editorPortalTarget={editorPortalTarget}
+ {...interactionMasksMetaData}
+ />
+
+
+ );
}
diff --git a/packages/react-data-grid/src/Grid.tsx b/packages/react-data-grid/src/Grid.tsx
index f013698a84..0df6f93906 100644
--- a/packages/react-data-grid/src/Grid.tsx
+++ b/packages/react-data-grid/src/Grid.tsx
@@ -2,8 +2,8 @@ import React, { useRef, createElement } from 'react';
import { isValidElementType } from 'react-is';
import Header, { HeaderHandle, HeaderProps } from './Header';
-import Viewport, { ScrollState } from './Viewport';
-import { HeaderRowData, CellMetaData, InteractionMasksMetaData, ColumnMetrics } from './common/types';
+import Canvas from './Canvas';
+import { HeaderRowData, CellMetaData, InteractionMasksMetaData, ColumnMetrics, ScrollState } from './common/types';
import { DEFINE_SORT } from './common/enums';
import { ReactDataGridProps } from './ReactDataGrid';
import { EventBus } from './masks';
@@ -47,18 +47,20 @@ export interface GridProps extends SharedDataGridProps {
eventBus: EventBus;
interactionMasksMetaData: InteractionMasksMetaData;
onSort?(columnKey: keyof R, direction: DEFINE_SORT): void;
- onViewportKeydown?(e: React.KeyboardEvent): void;
- onViewportKeyup?(e: React.KeyboardEvent): void;
+ onCanvasKeydown?(e: React.KeyboardEvent): void;
+ onCanvasKeyup?(e: React.KeyboardEvent): void;
onColumnResize(idx: number, width: number): void;
- viewportWidth: number;
+ onRowSelectionChange(rowIdx: number, row: R, checked: boolean, isShiftClick: boolean): void;
}
export default function Grid({
+ rowGetter,
rowKey,
+ rowOffsetHeight,
rowsCount,
+ columnMetrics,
emptyRowsView,
headerRows,
- viewportWidth,
selectedRows,
...props
}: GridProps) {
@@ -84,11 +86,11 @@ export default function Grid({
rowKey,
rowsCount,
ref: header,
- rowGetter: props.rowGetter,
- columnMetrics: props.columnMetrics,
+ rowGetter,
+ columnMetrics,
onColumnResize: props.onColumnResize,
headerRows,
- rowOffsetHeight: props.rowOffsetHeight,
+ rowOffsetHeight,
sortColumn: props.sortColumn,
sortDirection: props.sortDirection,
draggableHeaderCell: props.draggableHeaderCell,
@@ -105,19 +107,18 @@ export default function Grid({
{createElement(emptyRowsView)}
) : (
-
+