diff --git a/examples/__tests__/Grid.spec.js b/examples/__tests__/Grid.spec.js deleted file mode 100644 index b722b58358..0000000000 --- a/examples/__tests__/Grid.spec.js +++ /dev/null @@ -1,35 +0,0 @@ -import TestUtils from 'react-dom/test-utils'; -import GridRunner from './GridRunner'; - -describe('Grid Integration', () => { - let gridRunner; - let grid; - - beforeEach(() => { - gridRunner = new GridRunner({}); - grid = gridRunner.grid; - }); - - afterEach(() => { - gridRunner.dispose(); - gridRunner = null; - grid = null; - }); - - describe('Setup', () => { - it('Creates the grid', () => { - expect(grid).toBeDefined(); - }); - - it('Renders the grid', () => { - TestUtils.isDOMComponent(grid); - }); - - it('Renders the expected number of rows', () => { - const { rowOverscanStartIdx, rowOverscanEndIdx } = gridRunner.getDisplayInfo(); - const expectedNumberOfRows = rowOverscanEndIdx - rowOverscanStartIdx; - - expect(gridRunner.getRenderedRows().length).toEqual(expectedNumberOfRows); - }); - }); -}); diff --git a/examples/scripts/example03-frozen-cols.js b/examples/scripts/example03-frozen-cols.js index 24de851b97..b22952bb99 100644 --- a/examples/scripts/example03-frozen-cols.js +++ b/examples/scripts/example03-frozen-cols.js @@ -6,9 +6,9 @@ import exampleWrapper from '../components/exampleWrapper'; class Example extends React.Component { constructor(props, context) { super(props, context); - this.createRows(); + const extraColumns = [...Array(500).keys()].map(i => ({ key: `col${i}`, name: `col${i}` })); + this.createRows(extraColumns); - const extraColumns = [...Array(50).keys()].map(i => ({ key: `col${i}`, name: `col${i}` })); const columns = [ { key: 'id', @@ -59,7 +59,7 @@ class Example extends React.Component { return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toLocaleDateString(); }; - createRows = () => { + createRows = (extraColumns) => { const rows = []; for (let i = 1; i < 1000; i++) { rows.push({ @@ -71,6 +71,10 @@ class Example extends React.Component { startDate: this.getRandomDate(new Date(2015, 3, 1), new Date()), completeDate: this.getRandomDate(new Date(), new Date(2016, 0, 1)) }); + + for (const extraColumn of extraColumns) { + rows[rows.length - 1][extraColumn.key] = `${extraColumn.key}`; + } } this._rows = rows; diff --git a/examples/scripts/example31-isScrolling.js b/examples/scripts/example31-isScrolling.js index a3e3d4c278..6547f7ac64 100644 --- a/examples/scripts/example31-isScrolling.js +++ b/examples/scripts/example31-isScrolling.js @@ -61,6 +61,7 @@ class Example extends React.Component { rowsCount={this.state.rows.length} minHeight={800} onScroll={this.onScroll} + enableIsScrolling /> ); diff --git a/package.json b/package.json index 2e505b9edd..bfdcfabdc7 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "docs": "node docs/utils/docsGen.js", "test": "jest", - "test-dev": "jest --watch", + "test:watch": "jest --watch", "postinstall": "lerna bootstrap --no-ci", "prebuild": "node tools/buildStylesheets.js", "build": "lerna run build", @@ -25,8 +25,8 @@ "@types/classnames": "^2.2.9", "@types/enzyme": "^3.10.1", "@types/jest": "^24.0.15", - "@types/react": "^16.8.23", - "@types/react-dom": "^16.8.3", + "@types/react": "^16.9.2", + "@types/react-dom": "^16.9.0", "@types/react-is": "^16.7.1", "@types/shallowequal": "^1.1.1", "@typescript-eslint/eslint-plugin": "^1.11.0", @@ -49,13 +49,13 @@ "less-loader": "^5.0.0", "markdown": "^0.5.0", "node-dir": "^0.1.12", - "react": "^16.8.6", + "react": "^16.9.0", "react-contextmenu": "^2.10.0", "react-data-grid": "file:packages/react-data-grid", "react-data-grid-addons": "file:packages/react-data-grid-addons", "react-dnd-test-backend": "^2.6.0", "react-docgen": "^2.21.0", - "react-dom": "^16.8.6", + "react-dom": "^16.9.0", "react-router-dom": "^5.0.1", "recharts": "^1.6.2", "style-loader": "^0.23.1", diff --git a/packages/react-data-grid-addons/src/__tests__/Grid.spec.js b/packages/react-data-grid-addons/src/__tests__/Grid.spec.js index 8b987d373e..f9620704c2 100644 --- a/packages/react-data-grid-addons/src/__tests__/Grid.spec.js +++ b/packages/react-data-grid-addons/src/__tests__/Grid.spec.js @@ -45,7 +45,7 @@ describe('Grid', () => { }; const getBaseGrid = (wrapper) => { - return wrapper.instance().base.current; + return wrapper.find('Grid'); }; const buildFakeEvent = (addedData) => { @@ -57,7 +57,7 @@ describe('Grid', () => { }; const simulateGridKeyDownWithKeyCode = (wrapper, keyCode) => { - getBaseGrid(wrapper).props.onViewportKeydown(buildFakeEvent({ keyCode })); + getBaseGrid(wrapper).props().onViewportKeydown(buildFakeEvent({ keyCode })); }; it('should create a new instance of Grid', () => { @@ -87,18 +87,19 @@ describe('Grid', () => { beforeEach(() => { wrapper = setup({ toolbar: }).wrapper; wrapper.find(ToolBarStub).props().onToggleFilter(); + wrapper.update(); }); it('uses the appropriate default for the grid row height', () => { - expect(getBaseGrid(wrapper).props.rowHeight).toEqual(35); + expect(getBaseGrid(wrapper).props().rowHeight).toEqual(35); }); it('uses the appropriate default for the header row height', () => { - expect(getBaseGrid(wrapper).props.headerRows[0].height).toEqual(35); + expect(getBaseGrid(wrapper).props().headerRows[0].height).toEqual(35); }); it('uses the appropriate default for the header filter row height', () => { - expect(getBaseGrid(wrapper).props.headerRows[1].height).toEqual(45); + expect(getBaseGrid(wrapper).props().headerRows[1].height).toEqual(45); }); }); @@ -108,18 +109,19 @@ describe('Grid', () => { beforeEach(() => { wrapper = setup({ toolbar: , rowHeight: 40 }).wrapper; wrapper.find(ToolBarStub).props().onToggleFilter(); + wrapper.update(); }); it('passes the correct heigth to the grid rows', () => { - expect(getBaseGrid(wrapper).props.rowHeight).toEqual(40); + expect(getBaseGrid(wrapper).props().rowHeight).toEqual(40); }); it('passes the grid row heigth to the header row when no height to the specific header row is provided', () => { - expect(getBaseGrid(wrapper).props.headerRows[0].height).toEqual(40); + expect(getBaseGrid(wrapper).props().headerRows[0].height).toEqual(40); }); it('uses the default prop height for the filter row when none is provided', () => { - expect(getBaseGrid(wrapper).props.headerRows[1].height).toEqual(45); + expect(getBaseGrid(wrapper).props().headerRows[1].height).toEqual(45); }); }); @@ -134,18 +136,19 @@ describe('Grid', () => { headerFiltersHeight: 60 }).wrapper; wrapper.find(ToolBarStub).props().onToggleFilter(); + wrapper.update(); }); it('passes the correct heigth to the grid rows', () => { - expect(getBaseGrid(wrapper).props.rowHeight).toEqual(40); + expect(getBaseGrid(wrapper).props().rowHeight).toEqual(40); }); it('passes the correct heigth to the header row', () => { - expect(getBaseGrid(wrapper).props.headerRows[0].height).toEqual(50); + expect(getBaseGrid(wrapper).props().headerRows[0].height).toEqual(50); }); it('passes the correct heigth to the header filter row', () => { - expect(getBaseGrid(wrapper).props.headerRows[1].height).toEqual(60); + expect(getBaseGrid(wrapper).props().headerRows[1].height).toEqual(60); }); }); }); @@ -169,7 +172,7 @@ describe('Grid', () => { it('should set filter state of grid and render a filterable header row', () => { expect(wrapper.instance().state.canFilter).toBe(true); - expect(getBaseGrid(wrapper).props.headerRows.length).toEqual(2); + expect(getBaseGrid(wrapper).props().headerRows.length).toEqual(2); }); }); }); @@ -182,11 +185,11 @@ describe('Grid', () => { beforeEach(() => { ({ wrapper, columns, rows } = setup({ enableRowSelect: true })); - selectRowCol = getBaseGrid(wrapper).props.columnMetrics.columns[0]; + selectRowCol = getBaseGrid(wrapper).props().columnMetrics.columns[0]; }); it('should render an additional Select Row column', () => { - expect(getBaseGrid(wrapper).props.columnMetrics.columns.length).toEqual(columns.length + 1); + expect(getBaseGrid(wrapper).props().columnMetrics.columns.length).toEqual(columns.length + 1); expect(selectRowCol.key).toEqual('select-row'); expect(TestUtils.isElementOfType(selectRowCol.formatter, CheckboxEditor)).toBe(true); }); @@ -232,7 +235,7 @@ describe('Grid', () => { describe('when selected is false', () => { beforeEach(() => { wrapper.instance().setState({ selectedRows: [{ id: 0, isSelected: false }, { id: 1, isSelected: false }, { id: 2, isSelected: false }, { id: 3, isSelected: false }] }); - const selectRowCol = getBaseGrid(wrapper).props.columnMetrics.columns[0]; + const selectRowCol = getBaseGrid(wrapper).props().columnMetrics.columns[0]; selectRowCol.onCellChange(3, 'select-row', rows[3], buildFakeEvent()); }); @@ -244,7 +247,7 @@ describe('Grid', () => { describe('when selected is null', () => { beforeEach(() => { wrapper.instance().setState({ selectedRows: [{ id: 0, isSelected: null }, { id: 1, isSelected: null }, { id: 2, isSelected: null }, { id: 3, isSelected: null }] }); - const selectRowCol = getBaseGrid(wrapper).props.columnMetrics.columns[0]; + const selectRowCol = getBaseGrid(wrapper).props().columnMetrics.columns[0]; selectRowCol.onCellChange(2, 'select-row', rows[2], buildFakeEvent()); }); @@ -256,7 +259,7 @@ describe('Grid', () => { describe('when selected is true', () => { beforeEach(() => { wrapper.instance().setState({ selectedRows: [{ id: 0, isSelected: null }, { id: 1, isSelected: true }, { id: 2, isSelected: true }, { id: 3, isSelected: true }] }); - const selectRowCol = getBaseGrid(wrapper).props.columnMetrics.columns[0]; + const selectRowCol = getBaseGrid(wrapper).props().columnMetrics.columns[0]; selectRowCol.onCellChange(3, 'select-row', rows[3], buildFakeEvent()); }); @@ -292,7 +295,7 @@ describe('Grid', () => { } } }).wrapper; - selectRowCol = getBaseGrid(wrapper).props.columnMetrics.columns[0]; + selectRowCol = getBaseGrid(wrapper).props().columnMetrics.columns[0]; }); it('should call rowSelection.onRowsSelected when row selected', () => { @@ -348,7 +351,7 @@ describe('Grid', () => { } }); - const selectRowCol = getBaseGrid(wrapper).props.columnMetrics.columns[0]; + const selectRowCol = getBaseGrid(wrapper).props().columnMetrics.columns[0]; // header checkbox const checkboxWrapper = document.createElement('div'); @@ -381,7 +384,7 @@ describe('Grid', () => { } }); - const selectRowCol = getBaseGrid(wrapper).props.columnMetrics.columns[0]; + const selectRowCol = getBaseGrid(wrapper).props().columnMetrics.columns[0]; // header checkbox const checkboxWrapper = document.createElement('div'); @@ -417,12 +420,6 @@ describe('Grid', () => { }); describe('Table width', () => { - it('should generate the width based on the container size', () => { - const { wrapper } = setup(); - const tableElement = ReactDOM.findDOMNode(wrapper.instance()); - expect(tableElement.style.width).toEqual('100%'); - }); - describe('providing table width as prop', () => { it('should set the width of the table', () => { const { wrapper } = setup(); @@ -451,7 +448,7 @@ describe('Grid', () => { } }); - getBaseGrid(wrapper).props.cellMetaData.onCellClick({ idx: 1, rowIdx: 1 }); + getBaseGrid(wrapper).props().cellMetaData.onCellClick({ idx: 1, rowIdx: 1 }); expect(rowClicks).toBe(1); const { row, column } = rowClicked; expect(row).toMatchObject(rows[1]); diff --git a/packages/react-data-grid-addons/src/__tests__/data/MockStateObject.js b/packages/react-data-grid-addons/src/__tests__/data/MockStateObject.js index 89e39e7316..b26f1363bb 100644 --- a/packages/react-data-grid-addons/src/__tests__/data/MockStateObject.js +++ b/packages/react-data-grid-addons/src/__tests__/data/MockStateObject.js @@ -31,7 +31,8 @@ export default function(stateValues, events) { width: 400, totalWidth: 0, totalColumnWidth: 400, - minColumnWidth: 80 + minColumnWidth: 80, + lastFrozenColumnIndex: -1 }, selectedRows: [], canFilter: false, diff --git a/packages/react-data-grid/src/Canvas.tsx b/packages/react-data-grid/src/Canvas.tsx index 06b8f2a9eb..21f84b43fd 100644 --- a/packages/react-data-grid/src/Canvas.tsx +++ b/packages/react-data-grid/src/Canvas.tsx @@ -6,14 +6,14 @@ import RowsContainerDefault from './RowsContainer'; import RowGroup from './RowGroup'; import { InteractionMasks } from './masks'; import * as rowUtils from './RowUtils'; -import { getColumnScrollPosition } from './utils/canvasUtils'; +import { getColumnScrollPosition, isPositionStickySupported } from './utils'; import { EventTypes } from './common/enums'; import { CalculatedColumn, Position, ScrollPosition, SubRowDetails, RowRenderer, RowRendererProps, RowData } from './common/types'; -import { ViewportProps, ViewportState } from './Viewport'; +import { ViewportProps } from './Viewport'; +import { HorizontalRangeToRender, VerticalRangeToRender } from './utils/viewportUtils'; type SharedViewportProps = Pick, 'rowKey' -| 'totalWidth' | 'rowGetter' | 'rowsCount' | 'selectedRows' @@ -34,28 +34,19 @@ type SharedViewportProps = Pick, | 'interactionMasksMetaData' >; -type SharedViewportState = Pick; +type SharedViewportState = ScrollPosition & HorizontalRangeToRender & VerticalRangeToRender; export interface CanvasProps extends SharedViewportProps, SharedViewportState { columns: CalculatedColumn[]; + height: number; width: number; totalColumnWidth: number; + lastFrozenColumnIndex: number; + isScrolling?: boolean; onScroll(position: ScrollPosition): void; } -type RendererProps = Pick, 'rowVisibleStartIdx' | 'rowVisibleEndIdx' | 'columns' | 'cellMetaData' | 'colVisibleStartIdx' | 'colVisibleEndIdx' | 'colOverscanEndIdx' | 'colOverscanStartIdx' | 'lastFrozenColumnIndex' | 'isScrolling'> & { +type RendererProps = Pick, 'columns' | 'cellMetaData' | 'colVisibleStartIdx' | 'colVisibleEndIdx' | 'colOverscanEndIdx' | 'colOverscanStartIdx' | 'lastFrozenColumnIndex' | 'isScrolling'> & { ref(row: (RowRenderer & React.Component>) | null): void; key: number; idx: number; @@ -73,7 +64,6 @@ export default class Canvas extends React.PureComponent> { private readonly interactionMasks = React.createRef>(); private readonly rows = new Map & React.Component>>(); private unsubscribeScrollToColumn?(): void; - private _scroll = { scrollTop: 0, scrollLeft: 0 }; componentDidMount() { this.unsubscribeScrollToColumn = this.props.eventBus.subscribe(EventTypes.SCROLL_TO_COLUMN, this.scrollToColumn); @@ -92,10 +82,9 @@ export default class Canvas extends React.PureComponent> { handleScroll = (e: React.UIEvent) => { const { scrollLeft, scrollTop } = e.currentTarget; - this._scroll = { scrollTop, scrollLeft }; - if (this.props.onScroll) { - this.props.onScroll(this._scroll); - } + // Freeze columns on legacy browsers + this.setScrollLeft(scrollLeft); + this.props.onScroll({ scrollLeft, scrollTop }); }; onHitBottomCanvas = () => { @@ -152,11 +141,6 @@ export default class Canvas extends React.PureComponent> { return rows; } - getScroll() { - const { scrollTop, scrollLeft } = this.canvas.current!; - return { scrollTop, scrollLeft }; - } - getClientScrollTopOffset(node: HTMLDivElement) { const { rowHeight } = this.props; const scrollVariation = node.scrollTop % rowHeight; @@ -183,6 +167,7 @@ export default class Canvas extends React.PureComponent> { } setScrollLeft(scrollLeft: number) { + if (isPositionStickySupported()) return; const { current } = this.interactionMasks; if (current) { current.setScrollLeft(scrollLeft); @@ -277,17 +262,13 @@ export default class Canvas extends React.PureComponent> { } renderPlaceholder(key: string, height: number) { - // just renders empty cells - // if we wanted to show gridlines, we'd need classes and position as with renderScrollingPlaceholder return ( -
- {this.props.columns.map(column =>
)} -
+
); } render() { - const { rowOverscanStartIdx, rowOverscanEndIdx, cellMetaData, columns, colOverscanStartIdx, colOverscanEndIdx, colVisibleStartIdx, colVisibleEndIdx, lastFrozenColumnIndex, rowHeight, rowsCount, totalColumnWidth, totalWidth, height, rowGetter, contextMenu } = this.props; + const { rowOverscanStartIdx, rowOverscanEndIdx, cellMetaData, columns, colOverscanStartIdx, colOverscanEndIdx, colVisibleStartIdx, colVisibleEndIdx, lastFrozenColumnIndex, rowHeight, rowsCount, totalColumnWidth, height, rowGetter, contextMenu } = this.props; const RowsContainer = this.props.RowsContainer || RowsContainerDefault; const rows = this.getRows(rowOverscanStartIdx, rowOverscanEndIdx) @@ -303,8 +284,6 @@ export default class Canvas extends React.PureComponent> { } }, idx: rowIdx, - rowVisibleStartIdx: this.props.rowVisibleStartIdx, - rowVisibleEndIdx: this.props.rowVisibleEndIdx, row, height: rowHeight, columns, @@ -317,7 +296,7 @@ export default class Canvas extends React.PureComponent> { colOverscanEndIdx, lastFrozenColumnIndex, isScrolling: this.props.isScrolling, - scrollLeft: this._scroll.scrollLeft + scrollLeft: this.props.scrollLeft }); }); @@ -332,7 +311,7 @@ export default class Canvas extends React.PureComponent> { return (
@@ -355,8 +334,8 @@ export default class Canvas extends React.PureComponent> { onHitTopBoundary={this.onHitTopCanvas} onHitLeftBoundary={this.handleHitColummBoundary} onHitRightBoundary={this.handleHitColummBoundary} - scrollLeft={this._scroll.scrollLeft} - scrollTop={this._scroll.scrollTop} + scrollLeft={this.props.scrollLeft} + scrollTop={this.props.scrollTop} getRowHeight={this.getRowHeight} getRowTop={this.getRowTop} getRowColumns={this.getRowColumns} @@ -364,7 +343,8 @@ export default class Canvas extends React.PureComponent> { {...this.props.interactionMasksMetaData} /> -
{rows}
+ {/* Set minHeight to show horizontal scrollbar when there are no rows */} +
{rows}
); diff --git a/packages/react-data-grid/src/Cell.tsx b/packages/react-data-grid/src/Cell.tsx index b4087db848..09fdf41b2c 100644 --- a/packages/react-data-grid/src/Cell.tsx +++ b/packages/react-data-grid/src/Cell.tsx @@ -1,5 +1,6 @@ import React from 'react'; import classNames from 'classnames'; +import shallowEqual from 'shallowequal'; import { SubRowOptions, ColumnEventInfo, CellRenderer, CellRendererProps } from './common/types'; import CellActions from './Cell/CellActions'; @@ -7,29 +8,39 @@ import CellExpand from './Cell/CellExpander'; import CellContent from './Cell/CellContent'; import { isFrozen } from './ColumnUtils'; -function getSubRowOptions({ rowIdx, idx, rowData, expandableOptions: expandArgs }: Props): SubRowOptions { +function getSubRowOptions({ rowIdx, idx, rowData, expandableOptions: expandArgs }: CellProps): SubRowOptions { return { rowIdx, idx, rowData, expandArgs }; } -export interface Props extends CellRendererProps { +export interface CellProps extends CellRendererProps { // TODO: Check if these props are required or not. These are most likely set by custom cell renderer className?: string; tooltip?: string | null; cellControls?: unknown; } -export default class Cell extends React.PureComponent> implements CellRenderer { +export default class Cell extends React.Component> implements CellRenderer { static defaultProps = { value: '' }; private readonly cell = React.createRef(); + shouldComponentUpdate(nextProps: CellProps) { + const { scrollLeft, ...rest } = this.props; + const { scrollLeft: nextScrollLeft, ...nextRest } = nextProps; + + return !shallowEqual(rest, nextRest); + } + componentDidMount() { - this.checkScroll(); + const { scrollLeft } = this.props; + if (scrollLeft !== undefined) { + this.setScrollLeft(scrollLeft); + } } - componentDidUpdate(prevProps: Props) { + componentDidUpdate(prevProps: CellProps) { if (isFrozen(prevProps.column) && !isFrozen(this.props.column)) { this.removeScroll(); } @@ -85,28 +96,19 @@ export default class Cell extends React.PureComponent> implements Ce } getCellClass() { - const { idx, column, lastFrozenColumnIndex, isRowSelected, tooltip, expandableOptions } = this.props; + const { idx, column, lastFrozenColumnIndex, tooltip, expandableOptions } = this.props; return classNames( column.cellClass, 'react-grid-Cell', this.props.className, { 'react-grid-Cell--frozen': isFrozen(column), 'rdg-last--frozen': lastFrozenColumnIndex === idx, - 'row-selected': isRowSelected, 'has-tooltip': !!tooltip, 'rdg-child-cell': expandableOptions && expandableOptions.subRowDetails && expandableOptions.treeDepth > 0 } ); } - checkScroll() { - const { scrollLeft, column } = this.props; - const node = this.cell.current; - if (isFrozen(column) && node && node.style.transform != null) { - this.setScrollLeft(scrollLeft); - } - } - setScrollLeft(scrollLeft: number) { const node = this.cell.current; if (node) { @@ -117,7 +119,7 @@ export default class Cell extends React.PureComponent> implements Ce removeScroll() { const node = this.cell.current; if (node) { - node.style.transform = null; + node.style.transform = 'none'; } } @@ -164,14 +166,28 @@ export default class Cell extends React.PureComponent> implements Ce } render() { - const { column, children, expandableOptions, cellMetaData, rowData } = this.props; + const { idx, rowIdx, column, value, tooltip, children, height, cellControls, expandableOptions, cellMetaData, rowData, isScrolling } = this.props; if (column.hidden) { return null; } const style = this.getStyle(); const className = this.getCellClass(); - const cellContent = children || {...this.props} />; + const cellContent = children || ( + + idx={idx} + rowIdx={rowIdx} + column={column} + rowData={rowData} + value={value} + tooltip={tooltip} + expandableOptions={expandableOptions} + height={height} + onDeleteSubRow={cellMetaData.onDeleteSubRow} + cellControls={cellControls} + isScrolling={isScrolling} + /> + ); const events = this.getEvents(); const cellExpander = expandableOptions && expandableOptions.canExpand && ( extends React.PureComponent> implements Ce column={column} rowData={rowData} - cellMetaData={cellMetaData} + getCellActions={cellMetaData.getCellActions} /> {cellExpander} {cellContent} diff --git a/packages/react-data-grid/src/Cell/CellActions.tsx b/packages/react-data-grid/src/Cell/CellActions.tsx index 7937723286..7e2ef32619 100644 --- a/packages/react-data-grid/src/Cell/CellActions.tsx +++ b/packages/react-data-grid/src/Cell/CellActions.tsx @@ -1,17 +1,14 @@ import React from 'react'; +import { CellMetaData } from '../common/types'; import CellAction from './CellAction'; -import { Props as CellProps } from '../Cell'; +import { CellProps } from '../Cell'; -type CellActionsProps = Pick, -'cellMetaData' -| 'column' -| 'rowData' ->; +type CellActionsProps = Pick, 'column' | 'rowData'> & Pick, 'getCellActions'>; -export default function CellActions({ cellMetaData, column, rowData }: CellActionsProps) { - if (cellMetaData.getCellActions) { - const cellActionButtons = cellMetaData.getCellActions(column, rowData); +export default function CellActions({ getCellActions, column, rowData }: CellActionsProps) { + if (getCellActions) { + const cellActionButtons = getCellActions(column, rowData); if (cellActionButtons && cellActionButtons.length > 0) { const actionButtons = cellActionButtons.map((action, index) => { return ; diff --git a/packages/react-data-grid/src/Cell/CellContent.tsx b/packages/react-data-grid/src/Cell/CellContent.tsx index 290928575d..232dd50dba 100644 --- a/packages/react-data-grid/src/Cell/CellContent.tsx +++ b/packages/react-data-grid/src/Cell/CellContent.tsx @@ -1,8 +1,9 @@ import React from 'react'; import classNames from 'classnames'; +import { CellMetaData } from '../common/types'; import ChildRowDeleteButton from '../ChildRowDeleteButton'; -import { Props as CellProps } from '../Cell'; +import { CellProps } from '../Cell'; import CellValue from './CellValue'; export type CellContentProps = Pick, @@ -16,22 +17,34 @@ export type CellContentProps = Pick, | 'tooltip' | 'height' | 'cellControls' -| 'cellMetaData' +> & Pick, +'onDeleteSubRow' >; -export default function CellContent({ idx, tooltip, expandableOptions, height, cellMetaData, cellControls, ...props }: CellContentProps) { - const { column } = props; +export default function CellContent({ + idx, + rowIdx, + column, + rowData, + value, + tooltip, + expandableOptions, + height, + onDeleteSubRow, + cellControls, + isScrolling +}: CellContentProps) { const isExpandCell = expandableOptions ? expandableOptions.field === column.key : false; const treeDepth = expandableOptions ? expandableOptions.treeDepth : 0; const marginLeft = expandableOptions && isExpandCell ? expandableOptions.treeDepth * 30 : 0; function handleDeleteSubRow() { - if (cellMetaData.onDeleteSubRow) { - cellMetaData.onDeleteSubRow({ + if (onDeleteSubRow) { + onDeleteSubRow({ idx, - rowIdx: props.rowIdx, - rowData: props.rowData, + rowIdx, + rowData, expandArgs: expandableOptions }); } @@ -42,7 +55,7 @@ export default function CellContent({ idx, tooltip, expandableOptions, height treeDepth={treeDepth} cellHeight={height} onDeleteSubRow={handleDeleteSubRow} - isDeleteSubRowEnabled={!!cellMetaData.onDeleteSubRow} + isDeleteSubRowEnabled={!!onDeleteSubRow} /> ); @@ -54,7 +67,15 @@ export default function CellContent({ idx, tooltip, expandableOptions, height
{cellDeleter}
- {...props} /> + + + rowIdx={rowIdx} + rowData={rowData} + column={column} + value={value} + isScrolling={isScrolling} + /> + {cellControls}
{tooltip && {tooltip}} diff --git a/packages/react-data-grid/src/ColumnMetrics.ts b/packages/react-data-grid/src/ColumnMetrics.ts index 8beb010077..64f57ca1d7 100644 --- a/packages/react-data-grid/src/ColumnMetrics.ts +++ b/packages/react-data-grid/src/ColumnMetrics.ts @@ -1,6 +1,6 @@ export { sameColumn } from './ColumnComparer'; import { getSize, isFrozen } from './ColumnUtils'; -import getScrollbarSize from './getScrollbarSize'; +import { getScrollbarSize } from './utils'; import { isColumnsImmutable } from './common/utils'; import { Column, CalculatedColumn, ColumnList, ColumnMetrics } from './common/types'; @@ -57,7 +57,8 @@ export function recalculate(metrics: Metrics): ColumnMetrics { setColumnWidths(columns, metrics.totalWidth); const width = getTotalColumnWidth(columns); - const unallocatedWidth = metrics.totalWidth - width - getScrollbarSize(); + const borderWidth = 2; + const unallocatedWidth = metrics.totalWidth - width - getScrollbarSize() - borderWidth; // compute width for columns which doesn't specify width setDefferedColumnWidths(columns, unallocatedWidth, metrics.minColumnWidth); @@ -72,6 +73,7 @@ export function recalculate(metrics: Metrics): ColumnMetrics { return { width, columns: calculatedColumns, + lastFrozenColumnIndex: frozenColumns.length - 1, totalWidth: metrics.totalWidth, totalColumnWidth: getTotalColumnWidth(columns), minColumnWidth: metrics.minColumnWidth diff --git a/packages/react-data-grid/src/Grid.tsx b/packages/react-data-grid/src/Grid.tsx index e4bc05dc2c..d3718a6f8f 100644 --- a/packages/react-data-grid/src/Grid.tsx +++ b/packages/react-data-grid/src/Grid.tsx @@ -1,9 +1,8 @@ -import React from 'react'; +import React, { useRef, createElement, useEffect } from 'react'; import { isValidElementType } from 'react-is'; import Header from './Header'; import Viewport, { ScrollState } from './Viewport'; -import { isFrozen } from './ColumnUtils'; import { HeaderRowData, CellMetaData, RowSelection, InteractionMasksMetaData, SelectedRow } from './common/types'; import { DEFINE_SORT } from './common/enums'; import { DataGridProps, DataGridState } from './ReactDataGrid'; @@ -30,6 +29,9 @@ type SharedDataGridProps = Pick, | 'onHeaderDrop' | 'getSubRowDetails' | 'editorPortalTarget' +| 'overscanRowCount' +| 'overscanColumnCount' +| 'enableIsScrolling' >; type SharedDataGridState = Pick, @@ -39,120 +41,99 @@ type SharedDataGridState = Pick, >; export interface GridProps extends SharedDataGridProps, SharedDataGridState { - headerRows: HeaderRowData[]; + headerRows: [HeaderRowData, HeaderRowData | undefined]; cellMetaData: CellMetaData; selectedRows?: SelectedRow[]; rowSelection?: RowSelection; rowOffsetHeight: number; + eventBus: EventBus; + interactionMasksMetaData: InteractionMasksMetaData; onSort(columnKey: keyof R, sortDirection: DEFINE_SORT): void; - totalWidth: number | string; onViewportKeydown(e: React.KeyboardEvent): void; onViewportKeyup(e: React.KeyboardEvent): void; onColumnResize(idx: number, width: number): void; - eventBus: EventBus; - interactionMasksMetaData: InteractionMasksMetaData; } -export default class Grid extends React.Component> { - static displayName = 'Grid'; - - private readonly header = React.createRef>(); - private readonly viewport = React.createRef>(); - private _scrollLeft?: number = undefined; - - _onScroll() { - if (this._scrollLeft !== undefined) { - this.header.current!.setScrollLeft(this._scrollLeft); - if (this.viewport.current) { - this.viewport.current.setScrollLeft(this._scrollLeft); - } - } - } - - areFrozenColumnsScrolledLeft(scrollLeft: number) { - return scrollLeft > 0 && this.props.columnMetrics.columns.some(c => isFrozen(c)); - } +export default function Grid({ emptyRowsView, headerRows, ...props }: GridProps) { + const isWidthInitialized = useRef(false); + const grid = useRef(null); + const header = useRef>(null); + const scrollLeft = useRef(0); - onScroll = (scrollState: ScrollState) => { - if (this.props.onScroll) { - this.props.onScroll(scrollState); + function onScroll(scrollState: ScrollState) { + if (header.current && scrollLeft.current !== scrollState.scrollLeft) { + scrollLeft.current = scrollState.scrollLeft; + header.current.setScrollLeft(scrollState.scrollLeft); } - const { scrollLeft } = scrollState; - if (this._scrollLeft !== scrollLeft || this.areFrozenColumnsScrolledLeft(scrollLeft)) { - this._scrollLeft = scrollLeft; - this._onScroll(); + if (props.onScroll) { + props.onScroll(scrollState); } - }; - - componentDidMount() { - this._scrollLeft = this.viewport.current ? this.viewport.current.getScroll().scrollLeft : 0; - this._onScroll(); } - componentDidUpdate() { - this._onScroll(); - } - - render() { - const { headerRows } = this.props; - const EmptyRowsView = this.props.emptyRowsView; + useEffect(() => { + // Delay rendering until width is initialized + // Width is needed to calculate the number of displayed columns + isWidthInitialized.current = true; + }, []); - return ( -
- - ref={this.header} - columnMetrics={this.props.columnMetrics} - onColumnResize={this.props.onColumnResize} - rowHeight={this.props.rowHeight} - totalWidth={this.props.totalWidth} - headerRows={headerRows} - sortColumn={this.props.sortColumn} - sortDirection={this.props.sortDirection} - draggableHeaderCell={this.props.draggableHeaderCell} - onSort={this.props.onSort} - onHeaderDrop={this.props.onHeaderDrop} - getValidFilterValues={this.props.getValidFilterValues} - cellMetaData={this.props.cellMetaData} - /> - {this.props.rowsCount === 0 && isValidElementType(EmptyRowsView) ? ( -
- -
- ) : ( -
- - ref={this.viewport} - rowKey={this.props.rowKey} - rowHeight={this.props.rowHeight} - rowRenderer={this.props.rowRenderer} - rowGetter={this.props.rowGetter} - rowsCount={this.props.rowsCount} - selectedRows={this.props.selectedRows} - columnMetrics={this.props.columnMetrics} - totalWidth={this.props.totalWidth} - onScroll={this.onScroll} - cellMetaData={this.props.cellMetaData} - rowOffsetHeight={this.props.rowOffsetHeight || this.props.rowHeight * headerRows.length} - minHeight={this.props.minHeight} - scrollToRowIndex={this.props.scrollToRowIndex} - contextMenu={this.props.contextMenu} - rowSelection={this.props.rowSelection} - getSubRowDetails={this.props.getSubRowDetails} - rowGroupRenderer={this.props.rowGroupRenderer} - enableCellSelect={this.props.enableCellSelect} - enableCellAutoFocus={this.props.enableCellAutoFocus} - cellNavigationMode={this.props.cellNavigationMode} - eventBus={this.props.eventBus} - interactionMasksMetaData={this.props.interactionMasksMetaData} - RowsContainer={this.props.RowsContainer} - editorPortalTarget={this.props.editorPortalTarget} - /> -
- )} -
- ); - } + return ( +
+ + ref={header} + columnMetrics={props.columnMetrics} + onColumnResize={props.onColumnResize} + headerRows={headerRows} + rowOffsetHeight={props.rowOffsetHeight} + sortColumn={props.sortColumn} + sortDirection={props.sortDirection} + draggableHeaderCell={props.draggableHeaderCell} + onSort={props.onSort} + onHeaderDrop={props.onHeaderDrop} + getValidFilterValues={props.getValidFilterValues} + cellMetaData={props.cellMetaData} + /> + {props.rowsCount === 0 && isValidElementType(emptyRowsView) ? ( +
+ {createElement(emptyRowsView)} +
+ ) : ( + isWidthInitialized.current && ( + + rowKey={props.rowKey} + rowHeight={props.rowHeight} + rowRenderer={props.rowRenderer} + rowGetter={props.rowGetter} + rowsCount={props.rowsCount} + selectedRows={props.selectedRows} + columnMetrics={props.columnMetrics} + onScroll={onScroll} + cellMetaData={props.cellMetaData} + rowOffsetHeight={props.rowOffsetHeight} + minHeight={props.minHeight} + scrollToRowIndex={props.scrollToRowIndex} + contextMenu={props.contextMenu} + rowSelection={props.rowSelection} + getSubRowDetails={props.getSubRowDetails} + rowGroupRenderer={props.rowGroupRenderer} + enableCellSelect={props.enableCellSelect} + enableCellAutoFocus={props.enableCellAutoFocus} + cellNavigationMode={props.cellNavigationMode} + eventBus={props.eventBus} + interactionMasksMetaData={props.interactionMasksMetaData} + RowsContainer={props.RowsContainer} + editorPortalTarget={props.editorPortalTarget} + overscanRowCount={props.overscanRowCount} + overscanColumnCount={props.overscanColumnCount} + enableIsScrolling={props.enableIsScrolling} + onViewportKeydown={props.onViewportKeydown} + onViewportKeyup={props.onViewportKeyup} + viewportWidth={grid.current ? grid.current.offsetWidth : 0} + /> + ) + )} +
+ ); } diff --git a/packages/react-data-grid/src/Header.tsx b/packages/react-data-grid/src/Header.tsx index 7662d7ea44..05dbf923f7 100644 --- a/packages/react-data-grid/src/Header.tsx +++ b/packages/react-data-grid/src/Header.tsx @@ -1,20 +1,17 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import classNames from 'classnames'; import HeaderRow from './HeaderRow'; import { resizeColumn } from './ColumnMetrics'; -import getScrollbarSize from './getScrollbarSize'; -import { HeaderRowType } from './common/enums'; -import { CalculatedColumn, ColumnMetrics } from './common/types'; +import { getScrollbarSize } from './utils'; +import { CalculatedColumn, ColumnMetrics, HeaderRowData } from './common/types'; import { GridProps } from './Grid'; type SharedGridProps = Pick, 'columnMetrics' | 'onColumnResize' -| 'rowHeight' -| 'totalWidth' | 'headerRows' +| 'rowOffsetHeight' | 'sortColumn' | 'sortDirection' | 'draggableHeaderCell' @@ -67,45 +64,38 @@ export default class Header extends React.Component, State> this.props.onColumnResize(pos, width || column.width); }; - getHeaderRows() { + getHeaderRow = (row: HeaderRowData, ref: React.RefObject>) => { const columnMetrics = this.getColumnMetrics(); - return this.props.headerRows.map((row, index) => { - // To allow header filters to be visible - const isFilterRow = row.rowType === HeaderRowType.FILTER; - const rowHeight = isFilterRow ? '500px' : 'auto'; - const scrollbarSize = getScrollbarSize() > 0 ? getScrollbarSize() : 0; - const updatedWidth = typeof this.props.totalWidth === 'number' - ? this.props.totalWidth - scrollbarSize - : this.props.totalWidth; - const headerRowStyle: React.CSSProperties = { - top: this.getCombinedHeaderHeights(index), - width: updatedWidth, - minHeight: rowHeight - }; - - return ( - - key={row.rowType} - ref={isFilterRow ? this.filterRow : this.row} - rowType={row.rowType} - style={headerRowStyle} - onColumnResize={this.onColumnResize} - onColumnResizeEnd={this.onColumnResizeEnd} - width={columnMetrics.width} - height={row.height || this.props.rowHeight} - columns={columnMetrics.columns} - draggableHeaderCell={this.props.draggableHeaderCell} - filterable={row.filterable} - onFilterChange={row.onFilterChange} - onHeaderDrop={this.props.onHeaderDrop} - sortColumn={this.props.sortColumn} - sortDirection={this.props.sortDirection} - onSort={this.props.onSort} - getValidFilterValues={this.props.getValidFilterValues} - /> - ); - }); + return ( + + key={row.rowType} + ref={ref} + rowType={row.rowType} + onColumnResize={this.onColumnResize} + onColumnResizeEnd={this.onColumnResizeEnd} + height={row.height} + columns={columnMetrics.columns} + draggableHeaderCell={this.props.draggableHeaderCell} + filterable={row.filterable} + onFilterChange={row.onFilterChange} + onHeaderDrop={this.props.onHeaderDrop} + sortColumn={this.props.sortColumn} + sortDirection={this.props.sortDirection} + onSort={this.props.onSort} + getValidFilterValues={this.props.getValidFilterValues} + /> + ); + }; + + getHeaderRows() { + const { headerRows } = this.props; + const rows = [this.getHeaderRow(headerRows[0], this.row)]; + if (headerRows[1]) { + rows.push(this.getHeaderRow(headerRows[1], this.filterRow)); + } + + return rows; } getColumnMetrics(): ColumnMetrics { @@ -121,25 +111,9 @@ export default class Header extends React.Component, State> return idx === -1 ? null : idx; } - getCombinedHeaderHeights(until?: number): number { - const stopAt = typeof until === 'number' - ? until - : this.props.headerRows.length; - - let height = 0; - for (let index = 0; index < stopAt; index++) { - height += this.props.headerRows[index].height || this.props.rowHeight; - } - return height; - } - setScrollLeft(scrollLeft: number): void { - const node = ReactDOM.findDOMNode(this.row.current) as Element; - node.scrollLeft = scrollLeft; this.row.current!.setScrollLeft(scrollLeft); if (this.filterRow.current) { - const nodeFilters = ReactDOM.findDOMNode(this.filterRow.current) as Element; - nodeFilters.scrollLeft = scrollLeft; this.filterRow.current.setScrollLeft(scrollLeft); } } @@ -156,7 +130,10 @@ export default class Header extends React.Component, State> return (
diff --git a/packages/react-data-grid/src/HeaderCell.tsx b/packages/react-data-grid/src/HeaderCell.tsx index ad2c8bbda8..7d905a1df6 100644 --- a/packages/react-data-grid/src/HeaderCell.tsx +++ b/packages/react-data-grid/src/HeaderCell.tsx @@ -130,7 +130,7 @@ export default class HeaderCell extends React.Component> { removeScroll() { const node = this.cell.current; if (node) { - node.style.transform = null; + node.style.transform = 'none'; } } diff --git a/packages/react-data-grid/src/HeaderRow.tsx b/packages/react-data-grid/src/HeaderRow.tsx index e90c0188b6..823073b06f 100644 --- a/packages/react-data-grid/src/HeaderRow.tsx +++ b/packages/react-data-grid/src/HeaderRow.tsx @@ -1,10 +1,8 @@ import React from 'react'; -import shallowEqual from 'shallowequal'; import HeaderCell from './HeaderCell'; import SortableHeaderCell from './common/cells/headerCells/SortableHeaderCell'; import FilterableHeaderCell from './common/cells/headerCells/FilterableHeaderCell'; -import getScrollbarSize from './getScrollbarSize'; import { isFrozen } from './ColumnUtils'; import { HeaderRowType, HeaderCellType, DEFINE_SORT } from './common/enums'; import { CalculatedColumn, AddFilterEvent } from './common/types'; @@ -20,33 +18,21 @@ type SharedHeaderProps = Pick, >; export interface HeaderRowProps extends SharedHeaderProps { - width?: number; height: number; columns: CalculatedColumn[]; onColumnResize(column: CalculatedColumn, width: number): void; onColumnResizeEnd(column: CalculatedColumn, width: number): void; - style?: React.CSSProperties; filterable?: boolean; onFilterChange?(args: AddFilterEvent): void; rowType: HeaderRowType; } -export default class HeaderRow extends React.Component> { +export default class HeaderRow extends React.PureComponent> { static displayName = 'HeaderRow'; + private readonly headerRow = React.createRef(); private readonly cells = new Map>(); - shouldComponentUpdate(nextProps: HeaderRowProps) { - return ( - nextProps.width !== this.props.width - || nextProps.height !== this.props.height - || nextProps.columns !== this.props.columns - || !shallowEqual(nextProps.style, this.props.style) - || this.props.sortColumn !== nextProps.sortColumn - || this.props.sortDirection !== nextProps.sortDirection - ); - } - getHeaderCellType(column: CalculatedColumn): HeaderCellType { if (column.filterable && this.props.filterable) { return HeaderCellType.FILTERABLE; @@ -134,6 +120,7 @@ export default class HeaderRow extends React.Component> { } setScrollLeft(scrollLeft: number): void { + this.headerRow.current!.scrollLeft = scrollLeft; this.props.columns.forEach(column => { const { key } = column; if (!this.cells.has(key)) return; @@ -147,20 +134,17 @@ export default class HeaderRow extends React.Component> { } render() { - const cellsStyle: React.CSSProperties = { - width: this.props.width ? this.props.width + getScrollbarSize() : '100%', - height: this.props.height - }; + const { height, rowType } = this.props; - // FIXME: do we need 2 wrapping divs? return (
-
- {this.getCells()} -
+ {this.getCells()}
); } diff --git a/packages/react-data-grid/src/ReactDataGrid.tsx b/packages/react-data-grid/src/ReactDataGrid.tsx index a9e1516332..abb7c17639 100644 --- a/packages/react-data-grid/src/ReactDataGrid.tsx +++ b/packages/react-data-grid/src/ReactDataGrid.tsx @@ -156,6 +156,21 @@ export interface DataGridProps { onCellDeSelected?(position: Position): void; /** called before cell is set active, returns a boolean to determine whether cell is editable */ onCheckCellIsEditable?(event: CheckCellIsEditableEvent): boolean; + /** + * The number of rows to render outside of the visible area + * Note that overscanning too much can negatively impact performance. By default, grid overscans by two items. + */ + overscanRowCount?: number; + /** + * The number of columns to render outside of the visible area + * Note that overscanning too much can negatively impact performance. By default, grid overscans by two items. + */ + overscanColumnCount?: number; + /** + * Provides an additional isScrolling parameter to formatters. This parameter can be used to show a placeholder row or column while the list is being scrolled. + * Note that using this parameter will result in an additional render call after scrolling has stopped (when isScrolling changes from true to false). + */ + enableIsScrolling?: boolean; } type DefaultProps = Pick, @@ -212,7 +227,6 @@ export default class ReactDataGrid extends React.Component(); - private readonly base = React.createRef>(); private readonly selectAllCheckbox = React.createRef(); private readonly eventBus = new EventBus(); private readonly _keysDown = new Set(); @@ -264,14 +278,10 @@ export default class ReactDataGrid extends React.Component extends React.Component { const columnMetrics = this.createColumnMetrics(); this.setState({ columnMetrics }); @@ -562,22 +568,17 @@ export default class ReactDataGrid extends React.Component offsetHeight += row.height, 0); - } - - getHeaderRows() { + getHeaderRows(): [HeaderRowData, HeaderRowData | undefined] { const { headerRowHeight, rowHeight, onAddFilter, headerFiltersHeight } = this.props; - const rows: HeaderRowData[] = [{ height: headerRowHeight || rowHeight, rowType: HeaderRowType.HEADER }]; - if (this.state.canFilter === true) { - rows.push({ + return [ + { height: headerRowHeight || rowHeight, rowType: HeaderRowType.HEADER }, + this.state.canFilter ? { rowType: HeaderRowType.FILTER, filterable: true, onFilterChange: onAddFilter, - height: headerFiltersHeight - }); - } - return rows; + height: headerFiltersHeight || headerRowHeight || rowHeight + } : undefined + ]; } getRowSelectionProps() { @@ -668,21 +669,14 @@ export default class ReactDataGrid extends React.Component @@ -692,9 +686,8 @@ export default class ReactDataGrid extends React.Component - ref={this.base} rowKey={this.props.rowKey} - headerRows={this.getHeaderRows()} + headerRows={headerRows} draggableHeaderCell={this.props.draggableHeaderCell} getValidFilterValues={this.props.getValidFilterValues} columnMetrics={this.state.columnMetrics} @@ -706,12 +699,11 @@ export default class ReactDataGrid extends React.Component extends React.Component
); diff --git a/packages/react-data-grid/src/Row.tsx b/packages/react-data-grid/src/Row.tsx index a16523d25e..f3a0725304 100644 --- a/packages/react-data-grid/src/Row.tsx +++ b/packages/react-data-grid/src/Row.tsx @@ -5,6 +5,7 @@ import rowComparer from './common/utils/RowComparer'; import Cell from './Cell'; import { isFrozen } from './ColumnUtils'; import * as rowUtils from './RowUtils'; +import { isPositionStickySupported } from './utils'; import { RowRenderer, RowRendererProps, CellRenderer, CellRendererProps, CalculatedColumn } from './common/types'; export default class Row extends React.Component> implements RowRenderer { @@ -43,7 +44,7 @@ export default class Row extends React.Component> impleme getCell(column: CalculatedColumn) { const Renderer = this.props.cellRenderer!; - const { idx, cellMetaData, isScrolling, row, isSelected, scrollLeft, lastFrozenColumnIndex } = this.props; + const { idx, cellMetaData, isScrolling, row, lastFrozenColumnIndex, scrollLeft } = this.props; const { key } = column; const cellProps: CellRendererProps & { ref: (cell: CellRenderer | null) => void } = { @@ -55,10 +56,9 @@ export default class Row extends React.Component> impleme cellMetaData, value: this.getCellValue(key || String(column.idx) as keyof R) as R[keyof R], // FIXME: fix types rowData: row, - isRowSelected: isSelected, expandableOptions: this.getExpandableOptions(key), isScrolling, - scrollLeft, + scrollLeft: isFrozen(column) && !isPositionStickySupported() ? scrollLeft : undefined, lastFrozenColumnIndex }; @@ -66,8 +66,8 @@ export default class Row extends React.Component> impleme } getCells() { - const { colOverscanStartIdx, colOverscanEndIdx, columns } = this.props; - const frozenColumns = columns.filter(c => isFrozen(c)); + const { colOverscanStartIdx, colOverscanEndIdx, columns, lastFrozenColumnIndex } = this.props; + const frozenColumns = columns.slice(0, lastFrozenColumnIndex + 1); const nonFrozenColumn = columns.slice(colOverscanStartIdx, colOverscanEndIdx + 1).filter(c => !isFrozen(c)); return nonFrozenColumn.concat(frozenColumns).map(c => this.getCell(c)); } diff --git a/packages/react-data-grid/src/RowGroup.tsx b/packages/react-data-grid/src/RowGroup.tsx index d092de587f..ac72b14ffb 100644 --- a/packages/react-data-grid/src/RowGroup.tsx +++ b/packages/react-data-grid/src/RowGroup.tsx @@ -20,7 +20,7 @@ interface Props { colVisibleEndIdx: number; colOverscanStartIdx: number; colOverscanEndIdx: number; - isScrolling: boolean; + isScrolling?: boolean; columnGroupDisplayName: string; columnGroupName: string; isExpanded: boolean; diff --git a/packages/react-data-grid/src/Viewport.tsx b/packages/react-data-grid/src/Viewport.tsx index 7d251d2364..0055197b0a 100644 --- a/packages/react-data-grid/src/Viewport.tsx +++ b/packages/react-data-grid/src/Viewport.tsx @@ -1,45 +1,15 @@ -import React from 'react'; +import React, { useRef, useState, useMemo } from 'react'; import Canvas from './Canvas'; -import { - getGridState, - getColOverscanEndIdx, - getVisibleBoundaries, - getScrollDirection, - getRowOverscanStartIdx, - getRowOverscanEndIdx, - getColOverscanStartIdx, - getNonFrozenVisibleColStartIdx, - getNonFrozenRenderedColumnCount, - findLastFrozenColumnIndex -} from './utils/viewportUtils'; +import { getVerticalRangeToRender, getHorizontalRangeToRender, getScrollDirection } from './utils/viewportUtils'; import { GridProps } from './Grid'; import { ScrollPosition } from './common/types'; import { SCROLL_DIRECTION } from './common/enums'; -interface ScrollParams { - height: number; - scrollTop: number; - scrollLeft: number; - rowsCount: number; - rowHeight: number; -} - export interface ScrollState { - height: number; scrollTop: number; scrollLeft: number; - rowVisibleStartIdx: number; - rowVisibleEndIdx: number; - rowOverscanStartIdx: number; - rowOverscanEndIdx: number; - colVisibleStartIdx: number; - colVisibleEndIdx: number; - colOverscanStartIdx: number; - colOverscanEndIdx: number; scrollDirection: SCROLL_DIRECTION; - lastFrozenColumnIndex: number; - isScrolling: boolean; } type SharedGridProps = Pick, @@ -50,7 +20,6 @@ type SharedGridProps = Pick, | 'rowsCount' | 'selectedRows' | 'columnMetrics' -| 'totalWidth' | 'cellMetaData' | 'rowOffsetHeight' | 'minHeight' @@ -66,233 +35,134 @@ type SharedGridProps = Pick, | 'interactionMasksMetaData' | 'RowsContainer' | 'editorPortalTarget' +| 'overscanRowCount' +| 'overscanColumnCount' +| 'enableIsScrolling' +| 'onViewportKeydown' +| 'onViewportKeyup' >; export interface ViewportProps extends SharedGridProps { onScroll(scrollState: ScrollState): void; + viewportWidth: number; } -export interface ViewportState { - rowOverscanStartIdx: number; - rowOverscanEndIdx: number; - rowVisibleStartIdx: number; - rowVisibleEndIdx: number; - height: number; - scrollTop: number; - scrollLeft: number; - colVisibleStartIdx: number; - colVisibleEndIdx: number; - colOverscanStartIdx: number; - colOverscanEndIdx: number; - isScrolling: boolean; - lastFrozenColumnIndex: number; -} - -export default class Viewport extends React.Component, ViewportState> { - static displayName = 'Viewport'; - - readonly state: Readonly = getGridState(this.props); - private readonly canvas = React.createRef>(); - private readonly viewport = React.createRef(); - private resetScrollStateTimeoutId: number | null = null; - - onScroll = ({ scrollTop, scrollLeft }: ScrollPosition) => { - const { rowHeight, rowsCount, onScroll } = this.props; - const nextScrollState = this.updateScroll({ - scrollTop, - scrollLeft, - height: this.state.height, - rowHeight, - rowsCount - }); - - onScroll(nextScrollState); - }; - - getScroll() { - return this.canvas.current!.getScroll(); - } - - setScrollLeft(scrollLeft: number) { - this.canvas.current!.setScrollLeft(scrollLeft); - } - - getDOMNodeOffsetWidth() { - return this.viewport.current ? this.viewport.current.offsetWidth : 0; - } - - clearScrollTimer() { - if (this.resetScrollStateTimeoutId !== null) { - window.clearTimeout(this.resetScrollStateTimeoutId); +export default function Viewport({ + minHeight, + rowHeight, + rowOffsetHeight, + rowsCount, + onScroll: handleScroll, + columnMetrics, + overscanRowCount, + overscanColumnCount, + enableIsScrolling, + viewportWidth, + ...props +}: ViewportProps) { + const resetScrollStateTimeoutId = useRef(null); + const [scrollLeft, setScrollLeft] = useState(0); + const [scrollTop, setScrollTop] = useState(0); + const [scrollDirection, setScrollDirection] = useState(SCROLL_DIRECTION.NONE); + const [isScrolling, setIsScrolling] = useState(undefined); + + function clearScrollTimer() { + if (resetScrollStateTimeoutId.current !== null) { + window.clearTimeout(resetScrollStateTimeoutId.current); + resetScrollStateTimeoutId.current = null; } } - getNextScrollState({ scrollTop, scrollLeft, height, rowHeight, rowsCount }: ScrollParams): ScrollState { - const isScrolling = true; - const { columns } = this.props.columnMetrics; - const scrollDirection = getScrollDirection(this.state, scrollTop, scrollLeft); - const { rowVisibleStartIdx, rowVisibleEndIdx } = getVisibleBoundaries(height, rowHeight, scrollTop, rowsCount); - const rowOverscanStartIdx = getRowOverscanStartIdx(scrollDirection, rowVisibleStartIdx); - const rowOverscanEndIdx = getRowOverscanEndIdx(scrollDirection, rowVisibleEndIdx, rowsCount); - const totalNumberColumns = columns.length; - const lastFrozenColumnIndex = findLastFrozenColumnIndex(columns); - const nonFrozenColVisibleStartIdx = getNonFrozenVisibleColStartIdx(columns, scrollLeft); - const nonFrozenRenderedColumnCount = getNonFrozenRenderedColumnCount(this.props.columnMetrics, this.getDOMNodeOffsetWidth(), scrollLeft); - const colVisibleEndIdx = Math.min(nonFrozenColVisibleStartIdx + nonFrozenRenderedColumnCount, totalNumberColumns); - const colOverscanStartIdx = getColOverscanStartIdx(scrollDirection, nonFrozenColVisibleStartIdx, lastFrozenColumnIndex); - const colOverscanEndIdx = getColOverscanEndIdx(scrollDirection, colVisibleEndIdx, totalNumberColumns); - return { - height, - scrollTop, - scrollLeft, - rowVisibleStartIdx, - rowVisibleEndIdx, - rowOverscanStartIdx, - rowOverscanEndIdx, - colVisibleStartIdx: nonFrozenColVisibleStartIdx, - colVisibleEndIdx, - colOverscanStartIdx, - colOverscanEndIdx, - scrollDirection, - lastFrozenColumnIndex, - isScrolling - }; - } - - resetScrollStateAfterDelay() { - this.clearScrollTimer(); - this.resetScrollStateTimeoutId = window.setTimeout( - this.resetScrollStateAfterDelayCallback, - 500 + function resetScrollStateAfterDelay() { + clearScrollTimer(); + resetScrollStateTimeoutId.current = window.setTimeout( + resetScrollStateAfterDelayCallback, + 150 ); } - resetScrollStateAfterDelayCallback = () => { - this.resetScrollStateTimeoutId = null; - this.setState({ isScrolling: false }); - }; - - updateScroll(scrollParams: ScrollParams) { - this.resetScrollStateAfterDelay(); - const nextScrollState = this.getNextScrollState(scrollParams); - this.setState(nextScrollState); - return nextScrollState; + function resetScrollStateAfterDelayCallback() { + resetScrollStateTimeoutId.current = null; + setIsScrolling(false); } - metricsUpdated = () => { - if (!this.viewport.current) { - return; + function onScroll({ scrollLeft: newScrollLeft, scrollTop: newScrollTop }: ScrollPosition) { + if (enableIsScrolling) { + setIsScrolling(true); + resetScrollStateAfterDelay(); } - const { height } = this.viewport.current.getBoundingClientRect(); - - if (height) { - const { scrollTop, scrollLeft } = this.state; - const { rowHeight, rowsCount } = this.props; - this.updateScroll({ - scrollTop, - scrollLeft, - height, - rowHeight, - rowsCount - }); - } - }; - - componentWillReceiveProps(nextProps: ViewportProps) { - const { rowHeight, rowsCount } = nextProps; - if (this.props.rowHeight !== nextProps.rowHeight - || this.props.minHeight !== nextProps.minHeight) { - const { scrollTop, scrollLeft, height } = getGridState(nextProps); - this.updateScroll({ - scrollTop, - scrollLeft, - height, - rowHeight, - rowsCount - }); - } else if (this.props.columnMetrics.columns.length !== nextProps.columnMetrics.columns.length) { - this.setState(getGridState(nextProps)); - } else if (this.props.rowsCount !== nextProps.rowsCount) { - const { scrollTop, scrollLeft, height } = this.state; - this.updateScroll({ - scrollTop, - scrollLeft, - height, - rowHeight, - rowsCount - }); - // Added to fix the hiding of the bottom scrollbar when showing the filters. - } else if (this.props.rowOffsetHeight !== nextProps.rowOffsetHeight) { - const { scrollTop, scrollLeft } = this.state; - // The value of height can be positive or negative and will be added to the current height to cater for changes in the header height (due to the filer) - const height = this.state.height + this.props.rowOffsetHeight - nextProps.rowOffsetHeight; - this.updateScroll({ - scrollTop, - scrollLeft, - height, - rowHeight, - rowsCount - }); - } + const newScrollDirection = getScrollDirection({ scrollLeft, scrollTop }, { scrollLeft: newScrollLeft, scrollTop: newScrollTop }); + setScrollLeft(newScrollLeft); + setScrollTop(newScrollTop); + setScrollDirection(newScrollDirection); + handleScroll({ + scrollLeft: newScrollLeft, + scrollTop: newScrollTop, + scrollDirection: newScrollDirection + }); } - componentDidMount() { - window.addEventListener('resize', this.metricsUpdated); - this.metricsUpdated(); - } + const canvasHeight = minHeight - rowOffsetHeight; - componentWillUnmount() { - window.removeEventListener('resize', this.metricsUpdated); - this.clearScrollTimer(); - } + const verticalRangeToRender = useMemo(() => { + return getVerticalRangeToRender({ + height: canvasHeight, + rowHeight, + scrollTop, + rowsCount, + scrollDirection, + overscanRowCount + }); + }, [canvasHeight, overscanRowCount, rowHeight, rowsCount, scrollDirection, scrollTop]); - render() { - return ( -
- - ref={this.canvas} - rowKey={this.props.rowKey} - totalWidth={this.props.totalWidth} - width={this.props.columnMetrics.width} - totalColumnWidth={this.props.columnMetrics.totalColumnWidth} - rowGetter={this.props.rowGetter} - rowsCount={this.props.rowsCount} - selectedRows={this.props.selectedRows} - columns={this.props.columnMetrics.columns} - rowRenderer={this.props.rowRenderer} - rowOverscanStartIdx={this.state.rowOverscanStartIdx} - rowOverscanEndIdx={this.state.rowOverscanEndIdx} - rowVisibleStartIdx={this.state.rowVisibleStartIdx} - rowVisibleEndIdx={this.state.rowVisibleEndIdx} - colVisibleStartIdx={this.state.colVisibleStartIdx} - colVisibleEndIdx={this.state.colVisibleEndIdx} - colOverscanStartIdx={this.state.colOverscanStartIdx} - colOverscanEndIdx={this.state.colOverscanEndIdx} - lastFrozenColumnIndex={this.state.lastFrozenColumnIndex} - cellMetaData={this.props.cellMetaData} - height={this.state.height} - rowHeight={this.props.rowHeight} - onScroll={this.onScroll} - scrollToRowIndex={this.props.scrollToRowIndex} - contextMenu={this.props.contextMenu} - rowSelection={this.props.rowSelection} - getSubRowDetails={this.props.getSubRowDetails} - rowGroupRenderer={this.props.rowGroupRenderer} - isScrolling={this.state.isScrolling} - enableCellSelect={this.props.enableCellSelect} - enableCellAutoFocus={this.props.enableCellAutoFocus} - cellNavigationMode={this.props.cellNavigationMode} - eventBus={this.props.eventBus} - RowsContainer={this.props.RowsContainer} - editorPortalTarget={this.props.editorPortalTarget} - interactionMasksMetaData={this.props.interactionMasksMetaData} - /> -
- ); - } + const horizontalRangeToRender = useMemo(() => { + return getHorizontalRangeToRender({ + columnMetrics, + scrollLeft, + viewportWidth, + scrollDirection, + overscanColumnCount + }); + }, [columnMetrics, overscanColumnCount, scrollDirection, scrollLeft, viewportWidth]); + + return ( +
+ + {...verticalRangeToRender} + {...horizontalRangeToRender} + rowKey={props.rowKey} + width={columnMetrics.width} + totalColumnWidth={columnMetrics.totalColumnWidth} + columns={columnMetrics.columns} + lastFrozenColumnIndex={columnMetrics.lastFrozenColumnIndex} + rowGetter={props.rowGetter} + rowsCount={rowsCount} + selectedRows={props.selectedRows} + rowRenderer={props.rowRenderer} + scrollTop={scrollTop} + scrollLeft={scrollLeft} + cellMetaData={props.cellMetaData} + height={canvasHeight} + rowHeight={rowHeight} + onScroll={onScroll} + scrollToRowIndex={props.scrollToRowIndex} + contextMenu={props.contextMenu} + rowSelection={props.rowSelection} + getSubRowDetails={props.getSubRowDetails} + rowGroupRenderer={props.rowGroupRenderer} + isScrolling={isScrolling} + enableCellSelect={props.enableCellSelect} + enableCellAutoFocus={props.enableCellAutoFocus} + cellNavigationMode={props.cellNavigationMode} + eventBus={props.eventBus} + RowsContainer={props.RowsContainer} + editorPortalTarget={props.editorPortalTarget} + interactionMasksMetaData={props.interactionMasksMetaData} + /> +
+ ); } diff --git a/packages/react-data-grid/src/__tests__/Canvas.spec.tsx b/packages/react-data-grid/src/__tests__/Canvas.spec.tsx index 0d2c3b4470..6b45f39dd3 100644 --- a/packages/react-data-grid/src/__tests__/Canvas.spec.tsx +++ b/packages/react-data-grid/src/__tests__/Canvas.spec.tsx @@ -53,11 +53,12 @@ const testProps: CanvasProps = { eventBus: new EventBus(), editorPortalTarget: document.body, width: 1000, - totalWidth: 1000, totalColumnWidth: 1000, onScroll() { }, lastFrozenColumnIndex: 0, - RowsContainer: RowsContainer as CanvasProps['RowsContainer'] + RowsContainer: RowsContainer as CanvasProps['RowsContainer'], + scrollTop: 0, + scrollLeft: 0 }; function renderComponent(extraProps?: Partial>) { diff --git a/packages/react-data-grid/src/__tests__/Cell.spec.tsx b/packages/react-data-grid/src/__tests__/Cell.spec.tsx index 16fa658e8c..32f096838e 100644 --- a/packages/react-data-grid/src/__tests__/Cell.spec.tsx +++ b/packages/react-data-grid/src/__tests__/Cell.spec.tsx @@ -1,7 +1,7 @@ import React, { PropsWithChildren } from 'react'; import { mount } from 'enzyme'; -import Cell, { Props } from '../Cell'; +import Cell, { CellProps } from '../Cell'; import helpers, { Row } from '../helpers/test/GridPropHelpers'; import CellAction from '../Cell/CellAction'; import { SimpleCellFormatter } from '../formatters'; @@ -36,7 +36,7 @@ const expandableOptions = { const defaultColumn: CalculatedColumn = { idx: 0, key: 'description', name: 'Desciption', width: 100, left: 0 }; -const testProps: Props = { +const testProps: CellProps = { rowIdx: 0, idx: 1, column: defaultColumn, @@ -45,10 +45,11 @@ const testProps: Props = { rowData: { id: 1, description: 'Wicklow' }, height: 40, isScrolling: false, - scrollLeft: 0 + scrollLeft: 0, + lastFrozenColumnIndex: -1 }; -const renderComponent = (extraProps?: PropsWithChildren>>) => { +const renderComponent = (extraProps?: PropsWithChildren>>) => { return mount( {...testProps} {...extraProps} />); }; @@ -78,11 +79,11 @@ describe('Cell Tests', () => { }); describe('Rendering Cell component', () => { - function shallowRenderComponent(props: Props) { + function shallowRenderComponent(props: CellProps) { return mount( {...props} />); } - const requiredProperties: Props = { + const requiredProperties: CellProps = { rowIdx: 18, idx: 19, height: 60, @@ -92,7 +93,8 @@ describe('Cell Tests', () => { rowData: helpers.rowGetter(11), expandableOptions, isScrolling: false, - scrollLeft: 0 + scrollLeft: 0, + lastFrozenColumnIndex: -1 }; it('passes classname property', () => { @@ -113,8 +115,8 @@ describe('Cell Tests', () => { }); describe('CellActions', () => { - const setup = (propsOverride: Partial> = {}) => { - const props: Props = { + const setup = (propsOverride: Partial> = {}) => { + const props: CellProps = { rowIdx: 18, idx: 19, height: 60, @@ -125,6 +127,7 @@ describe('Cell Tests', () => { expandableOptions, isScrolling: false, scrollLeft: 0, + lastFrozenColumnIndex: -1, ...propsOverride }; diff --git a/packages/react-data-grid/src/__tests__/ColumnMetrics.spec.ts b/packages/react-data-grid/src/__tests__/ColumnMetrics.spec.ts index 902c3d40ff..b82169bd1c 100644 --- a/packages/react-data-grid/src/__tests__/ColumnMetrics.spec.ts +++ b/packages/react-data-grid/src/__tests__/ColumnMetrics.spec.ts @@ -1,4 +1,4 @@ -import getScrollbarSize from '../getScrollbarSize'; +import { getScrollbarSize } from '../utils'; import * as ColumnMetrics from '../ColumnMetrics'; import { Column } from '../common/types'; @@ -12,7 +12,7 @@ interface Row { } function getAvailableWidthPerColumn(totalWidth: number, consumedWidth: number, numberOfcolumns: number): number { - let availableWidth = totalWidth - getScrollbarSize() - consumedWidth; + let availableWidth = totalWidth - getScrollbarSize() - consumedWidth - 2; // 2 for border width availableWidth = availableWidth % numberOfcolumns === 0 ? availableWidth : availableWidth - 1; return availableWidth / numberOfcolumns; diff --git a/packages/react-data-grid/src/__tests__/Grid.spec.js b/packages/react-data-grid/src/__tests__/Grid.spec.js index ffa18788c9..cf189452a2 100644 --- a/packages/react-data-grid/src/__tests__/Grid.spec.js +++ b/packages/react-data-grid/src/__tests__/Grid.spec.js @@ -103,11 +103,6 @@ describe('Rendering Grid component', () => { const draggableDiv = wrapper.find('div').at(0); expect(draggableDiv.hasClass('react-grid-Grid')); }); - it('passes style property', () => { - const wrapper = renderComponent(allProperties()); - const draggableDiv = wrapper.find('div').at(0); - expect(draggableDiv.props().style).toBeDefined(); - }); it('does not pass unknown properties to the div', () => { const wrapper = renderComponent(allProperties()); const draggableDiv = wrapper.find('div').at(0); diff --git a/packages/react-data-grid/src/__tests__/Header.spec.tsx b/packages/react-data-grid/src/__tests__/Header.spec.tsx index 10321d6fbb..7ba678c2c7 100644 --- a/packages/react-data-grid/src/__tests__/Header.spec.tsx +++ b/packages/react-data-grid/src/__tests__/Header.spec.tsx @@ -4,7 +4,7 @@ import { shallow } from 'enzyme'; import Header, { HeaderProps } from '../Header'; import HeaderRow from '../HeaderRow'; import helpers, { fakeCellMetaData, Row } from '../helpers/test/GridPropHelpers'; -import * as GetScrollbarSize from '../getScrollbarSize'; +import * as utils from '../utils'; import { HeaderRowType, DEFINE_SORT } from '../common/enums'; const SCROLL_BAR_SIZE = 17; @@ -17,25 +17,25 @@ describe('Header Unit Tests', () => { minColumnWidth: 80, totalColumnWidth: 2600, totalWidth: 2600, - width: 2600 + width: 2600, + lastFrozenColumnIndex: 0 }, cellMetaData: fakeCellMetaData, - totalWidth: 1000, - rowHeight: 50, headerRows: [{ height: 50, rowType: HeaderRowType.HEADER, onFilterChange() { } - }], + }, undefined], onColumnResize: jest.fn(), onSort: () => null, onHeaderDrop() { }, - draggableHeaderCell: () => null + draggableHeaderCell: () => null, + rowOffsetHeight: 30 }; } beforeEach(() => { - jest.spyOn(GetScrollbarSize, 'default').mockReturnValue(SCROLL_BAR_SIZE); + jest.spyOn(utils, 'getScrollbarSize').mockReturnValue(SCROLL_BAR_SIZE); }); it('should render a default header row', () => { @@ -68,20 +68,20 @@ describe('Header Unit Tests', () => { minColumnWidth: 81, totalColumnWidth: 2600, totalWidth: 2600, - width: 2601 + width: 2601, + lastFrozenColumnIndex: 0 }, - rowHeight: 51, - totalWidth: 2600, headerRows: [{ height: 51, rowType: HeaderRowType.HEADER, onFilterChange() { } - }], + }, undefined], onSort: jest.fn(), onHeaderDrop() { }, cellMetaData: fakeCellMetaData, draggableHeaderCell: () => null, - onColumnResize() { } + onColumnResize() { }, + rowOffsetHeight: 30 }; const testAllProps: HeaderProps = { columnMetrics: { @@ -89,15 +89,14 @@ describe('Header Unit Tests', () => { minColumnWidth: 80, totalColumnWidth: 2600, totalWidth: 2600, - width: 2600 + width: 2600, + lastFrozenColumnIndex: 0 }, - totalWidth: 1000, - rowHeight: 50, headerRows: [{ height: 50, rowType: HeaderRowType.HEADER, onFilterChange() { } - }], + }, undefined], sortColumn: 'count', sortDirection: DEFINE_SORT.DESC, onSort: jest.fn(), @@ -105,7 +104,8 @@ describe('Header Unit Tests', () => { draggableHeaderCell: jest.fn(), getValidFilterValues: jest.fn(), cellMetaData: fakeCellMetaData, - onHeaderDrop() { } + onHeaderDrop() { }, + rowOffsetHeight: 30 }; it('passes classname property', () => { const wrapper = renderComponent(testAllProps); @@ -120,8 +120,8 @@ describe('Header Unit Tests', () => { it('should account for scrollbar size in header', () => { const wrapper = renderComponent(testAllProps); - const headerRow = wrapper.find('.react-grid-Header').childAt(0); - expect(headerRow.props().style.width).toBe(testAllProps.totalWidth as number - SCROLL_BAR_SIZE); + const headerDiv = wrapper.find('div'); + expect(headerDiv.props().style!.paddingRight).toBe(SCROLL_BAR_SIZE); }); it('does not pass onScroll properties if it is not available from props', () => { const wrapper = renderComponent(testRequiredProps); diff --git a/packages/react-data-grid/src/__tests__/HeaderRow.spec.tsx b/packages/react-data-grid/src/__tests__/HeaderRow.spec.tsx index 035704a846..0a48bd62ef 100644 --- a/packages/react-data-grid/src/__tests__/HeaderRow.spec.tsx +++ b/packages/react-data-grid/src/__tests__/HeaderRow.spec.tsx @@ -139,28 +139,6 @@ describe('Header Row Unit Tests', () => { draggableHeaderCell: () =>
}; - const allProperties: HeaderRowProps = { - height: 35, - columns: helpers.columns, - onSort: jest.fn(), - rowType: HeaderRowType.HEADER, - onColumnResize: jest.fn(), - onColumnResizeEnd: jest.fn(), - width: 200, - style: { - overflow: 'scroll', - width: 201, - height: 36, - position: 'relative' - }, - sortColumn: 'count', - sortDirection: DEFINE_SORT.NONE, - filterable: true, - onFilterChange() { }, - onHeaderDrop() { }, - draggableHeaderCell: () =>
- }; - it('passes classname property', () => { const wrapper = renderComponent(requiredProps); const headerRowDiv = wrapper.find('div').at(0); @@ -171,15 +149,10 @@ describe('Header Row Unit Tests', () => { const headerRowDiv = wrapper.find('div').at(0); expect(headerRowDiv.props().width).toBeUndefined(); }); - it('passes style property, if available from props', () => { - const wrapper = renderComponent(allProperties); - const headerRowDiv = wrapper.find('div').at(0); - expect(headerRowDiv.props().style).toBe(allProperties.style); - }); - it('does not pass style if not available from props', () => { + it('does pass the height if available from props', () => { const wrapper = renderComponent(requiredProps); const headerRowDiv = wrapper.find('div').at(0); - expect(headerRowDiv.props().style).toBeUndefined(); + expect(headerRowDiv.props().style).toEqual({ height: 35 }); }); }); }); diff --git a/packages/react-data-grid/src/__tests__/Row.spec.tsx b/packages/react-data-grid/src/__tests__/Row.spec.tsx index 9a12449269..22279d6d8e 100644 --- a/packages/react-data-grid/src/__tests__/Row.spec.tsx +++ b/packages/react-data-grid/src/__tests__/Row.spec.tsx @@ -47,7 +47,8 @@ describe('Row', () => { colOverscanStartIdx: 0, colOverscanEndIdx: 20, isScrolling: true, - scrollLeft: 0 + scrollLeft: 0, + lastFrozenColumnIndex: -1 }; it('passes classname property', () => { @@ -73,7 +74,7 @@ describe('Row', () => { it('should render all frozen and visible and overscan cells', () => { const columns = lockColumns(); - const { cells } = setup({ ...requiredProperties, columns }); + const { cells } = setup({ ...requiredProperties, columns, lastFrozenColumnIndex: LAST_LOCKED_CELL_IDX }); const { colOverscanStartIdx, colOverscanEndIdx } = requiredProperties; const renderedRange = colOverscanEndIdx - colOverscanStartIdx + 1; expect(cells.length).toBe(renderedRange); @@ -81,7 +82,7 @@ describe('Row', () => { it('first frozen cell should be rendered after the unfrozen cells', () => { const columns = lockColumns(); - const { cells } = setup({ ...requiredProperties, columns }); + const { cells } = setup({ ...requiredProperties, columns, lastFrozenColumnIndex: LAST_LOCKED_CELL_IDX }); const firstFrozenColumn = columns.filter(c => c.frozen === true)[0]; expect(cells.at(cells.length - LAST_LOCKED_CELL_IDX - 1).props().column).toBe(firstFrozenColumn); }); diff --git a/packages/react-data-grid/src/__tests__/Viewport.spec.tsx b/packages/react-data-grid/src/__tests__/Viewport.spec.tsx deleted file mode 100644 index 0ac36152ee..0000000000 --- a/packages/react-data-grid/src/__tests__/Viewport.spec.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import Viewport, { ViewportProps } from '../Viewport'; -import Canvas from '../Canvas'; -import helpers, { Row } from '../helpers/test/GridPropHelpers'; -import { SCROLL_DIRECTION, CellNavigationMode } from '../common/enums'; -import EventBus from '../masks/EventBus'; -import { CalculatedColumn } from '../common/types'; - -const viewportProps: ViewportProps = { - rowOffsetHeight: 0, - totalWidth: 400, - columnMetrics: { - columns: helpers.columns, - minColumnWidth: 80, - totalWidth: 2600, - totalColumnWidth: 2600, - width: 2600 - }, - rowGetter() { return {}; }, - rowsCount: 50, - rowHeight: 35, - onScroll() { }, - minHeight: 500, - cellMetaData: { - onCellClick() { }, - onCellDoubleClick() { }, - rowKey: 'id', - onCellMouseDown() { }, - onCellMouseEnter() { }, - onCellContextMenu() { }, - onDragEnter() { }, - onRowExpandToggle() { }, - onCellExpand() { } - }, - interactionMasksMetaData: { - onGridRowsUpdated() { }, - onDragHandleDoubleClick() { }, - onCommit() { } - }, - rowKey: 'id', - enableCellSelect: true, - enableCellAutoFocus: true, - cellNavigationMode: CellNavigationMode.NONE, - eventBus: new EventBus(), - editorPortalTarget: document.body -}; - -const viewportPropsNoColumns: ViewportProps = { // when creating anew plan copying from an existing one the viewport got initialised with 0 columns rendered - rowOffsetHeight: 0, - totalWidth: 400, - columnMetrics: { - columns: helpers.columns, - minColumnWidth: 80, - totalColumnWidth: 0, - totalWidth: 0, - width: 2010 - }, - rowGetter() { return {}; }, - rowsCount: 50, - rowHeight: 35, - onScroll() { }, - minHeight: 500, - cellMetaData: { - onCellClick() { }, - onCellDoubleClick() { }, - rowKey: 'id', - onCellMouseDown() { }, - onCellMouseEnter() { }, - onCellContextMenu() { }, - onDragEnter() { }, - onRowExpandToggle() { }, - onCellExpand() { } - }, - interactionMasksMetaData: { - onGridRowsUpdated() { }, - onDragHandleDoubleClick() { }, - onCommit() { } - }, - rowKey: 'id', - enableCellSelect: true, - enableCellAutoFocus: true, - cellNavigationMode: CellNavigationMode.NONE, - eventBus: new EventBus(), - editorPortalTarget: document.body -}; - -describe('', () => { - it('renders a Canvas component', () => { - const wrapper = shallow(); - const canvas = wrapper.find(Canvas); - expect(canvas).toBeDefined(); - }); - - it('should update scroll state onScroll', () => { - const scrollLeft = 0; - const scrollTop = 200; - const wrapper = shallow(); - const canvas = wrapper.find(Canvas); - canvas.props().onScroll({ scrollTop, scrollLeft }); - expect(wrapper.state()).toEqual({ - colOverscanEndIdx: helpers.columns.length, - colOverscanStartIdx: 0, - colVisibleEndIdx: helpers.columns.length, - colVisibleStartIdx: 0, - rowOverscanEndIdx: 23, - rowOverscanStartIdx: 6, - height: viewportProps.minHeight, - scrollLeft, - scrollTop, - rowVisibleEndIdx: 21, - rowVisibleStartIdx: 6, - isScrolling: true, - lastFrozenColumnIndex: -1, - scrollDirection: SCROLL_DIRECTION.DOWN - }); - }); - - it('should set the max number of columns when column rendered are zeroed', () => { - const wrapper = shallow>(); - expect(wrapper.state().colVisibleEndIdx).toEqual(helpers.columns.length); - }); - - it('should update when given different number of columns', () => { - const wrapper = shallow>(); - const extraColumn: CalculatedColumn = { - key: 'description', - name: 'Description', - idx: 3, - width: 100, - left: 0 - }; - const updatedColumns = helpers.columns.concat(extraColumn); - const newProps = { ...viewportProps, columnMetrics: { ...viewportProps.columnMetrics, columns: updatedColumns } }; - wrapper.setProps(newProps); - expect(wrapper.state()).toEqual({ - colOverscanEndIdx: updatedColumns.length, - colOverscanStartIdx: 0, - colVisibleEndIdx: updatedColumns.length, - colVisibleStartIdx: 0, - rowOverscanEndIdx: 28, - rowOverscanStartIdx: 0, - height: viewportProps.minHeight, - scrollLeft: 0, - scrollTop: 0, - rowVisibleEndIdx: 14, - rowVisibleStartIdx: 0, - lastFrozenColumnIndex: 0, - isScrolling: false - }); - }); - - it('should update when given height changed', () => { - const wrapper = shallow>(); - const newHeight = 1000; - const newProps = { ...viewportProps, minHeight: newHeight }; - wrapper.setProps(newProps); - expect(wrapper.state()).toEqual({ - colOverscanEndIdx: helpers.columns.length, - colOverscanStartIdx: 0, - colVisibleEndIdx: helpers.columns.length, - colVisibleStartIdx: 0, - rowOverscanEndIdx: 29, - rowOverscanStartIdx: 0, - height: newHeight, - scrollLeft: 0, - scrollTop: 0, - rowVisibleEndIdx: 29, - rowVisibleStartIdx: 0, - isScrolling: true, - scrollDirection: SCROLL_DIRECTION.NONE, - lastFrozenColumnIndex: -1 - }); - }); -}); diff --git a/packages/react-data-grid/src/common/types.ts b/packages/react-data-grid/src/common/types.ts index fb8bf03dd7..a7dda61b74 100644 --- a/packages/react-data-grid/src/common/types.ts +++ b/packages/react-data-grid/src/common/types.ts @@ -62,6 +62,7 @@ export type ColumnList = Column[] | List>; export interface ColumnMetrics { columns: CalculatedColumn[]; + lastFrozenColumnIndex: number; width: number; totalColumnWidth: number; totalWidth: number; @@ -129,7 +130,7 @@ export interface FormatterProps { value: TValue; column: CalculatedColumn; row: TRow; - isScrolling: boolean; + isScrolling?: boolean; dependentValues?: TDependentValue; } @@ -158,11 +159,10 @@ export interface CellRendererProps { column: CalculatedColumn; rowData: TRow; cellMetaData: CellMetaData; - isScrolling: boolean; - scrollLeft: number; - isRowSelected?: boolean; + isScrolling?: boolean; + scrollLeft?: number; expandableOptions?: ExpandableOptions; - lastFrozenColumnIndex?: number; + lastFrozenColumnIndex: number; } export interface RowRendererProps { @@ -177,9 +177,9 @@ export interface RowRendererProps { subRowDetails?: SubRowDetails; colOverscanStartIdx: number; colOverscanEndIdx: number; - isScrolling: boolean; + isScrolling?: boolean; scrollLeft: number; - lastFrozenColumnIndex?: number; + lastFrozenColumnIndex: number; } export interface FilterRendererProps { diff --git a/packages/react-data-grid/src/common/utils/__tests__/RowComparer.spec.ts b/packages/react-data-grid/src/common/utils/__tests__/RowComparer.spec.ts index 2c72456967..db4156e0cd 100644 --- a/packages/react-data-grid/src/common/utils/__tests__/RowComparer.spec.ts +++ b/packages/react-data-grid/src/common/utils/__tests__/RowComparer.spec.ts @@ -40,7 +40,8 @@ const defaultProps: RowRendererProps = { treeDepth: 0, siblingIndex: 0, numberSiblings: 0 - } + }, + lastFrozenColumnIndex: 0 }; describe('RowComparer shouldRowUpdate', () => { diff --git a/packages/react-data-grid/src/getScrollbarSize.ts b/packages/react-data-grid/src/getScrollbarSize.ts deleted file mode 100644 index 239e3fff6d..0000000000 --- a/packages/react-data-grid/src/getScrollbarSize.ts +++ /dev/null @@ -1,29 +0,0 @@ -let size: number | undefined; - -export default function getScrollbarSize(): number { - if (size === undefined) { - const outer = document.createElement('div'); - outer.style.width = '50px'; - outer.style.height = '50px'; - outer.style.position = 'absolute'; - outer.style.top = '-200px'; - outer.style.left = '-200px'; - - const inner = document.createElement('div'); - inner.style.height = '100px'; - inner.style.width = '100%'; - - outer.appendChild(inner); - document.body.appendChild(outer); - - const outerWidth = outer.clientWidth; - outer.style.overflowY = 'scroll'; - const innerWidth = inner.clientWidth; - - document.body.removeChild(outer); - - size = outerWidth - innerWidth; - } - - return size; -} diff --git a/packages/react-data-grid/src/utils/__tests__/viewportUtils.spec.ts b/packages/react-data-grid/src/utils/__tests__/viewportUtils.spec.ts index ad7c59add5..aa516725c9 100644 --- a/packages/react-data-grid/src/utils/__tests__/viewportUtils.spec.ts +++ b/packages/react-data-grid/src/utils/__tests__/viewportUtils.spec.ts @@ -1,373 +1,180 @@ import { - getGridState, - getNonFrozenRenderedColumnCount, - getVisibleBoundaries, - getNonFrozenVisibleColStartIdx, getScrollDirection, - getRowOverscanStartIdx, - getRowOverscanEndIdx, - OVERSCAN_ROWS, - getColOverscanStartIdx, - getColOverscanEndIdx, - VisibleBoundaries + getVerticalRangeToRender, + VerticalRangeToRenderParams, + getHorizontalRangeToRender, + HorizontalRangeToRenderParams } from '../viewportUtils'; import { SCROLL_DIRECTION } from '../../common/enums'; -import { CalculatedColumn } from '../../common/types'; +import { ColumnMetrics } from '../../common/types'; interface Row { - col1?: string; - col2?: string; - col3?: string; - col4?: string; - col5?: string; - col6?: string; - col7?: string; - col8?: string; - col9?: string; + [key: string]: unknown; } describe('viewportUtils', () => { - const getColumns = (): CalculatedColumn[] => [ - { idx: 0, key: 'col1', name: 'col1', width: 100, left: 0 }, - { idx: 1, key: 'col2', name: 'col2', width: 100, left: 200 }, - { idx: 2, key: 'col3', name: 'col3', width: 100, left: 300 }, - { idx: 3, key: 'col4', name: 'col4', width: 100, left: 400 }, - { idx: 4, key: 'col5', name: 'col5', width: 100, left: 500 }, - { idx: 5, key: 'col6', name: 'col6', width: 100, left: 600 } - ]; - - describe('getGridState', () => { - const getState = (propsOverrides = {}) => { - const props = { - columnMetrics: { - columns: [ - { idx: 0, key: 'col1', name: 'col1', width: 100, left: 0 }, - { idx: 1, key: 'col2', name: 'col2', width: 100, left: 100 } - ], - width: 0, - totalColumnWidth: 0, - totalWidth: 0, - minColumnWidth: 0 - }, - minHeight: 100, - rowOffsetHeight: 5, - rowHeight: 20, - rowsCount: 100, - ...propsOverrides - }; - const state = getGridState(props); + describe('getVerticalRangeToRender', () => { + function getRange(overrides: Pick) { + return getVerticalRangeToRender({ + height: 500, + rowHeight: 50, + scrollTop: 200, + rowsCount: 1000, + scrollDirection: SCROLL_DIRECTION.DOWN, + ...overrides + }); + } + + it('should use rowHeight to calculate the range', () => { + expect(getRange({ rowHeight: 50 })).toEqual({ + rowVisibleStartIdx: 4, + rowVisibleEndIdx: 14, + rowOverscanStartIdx: 2, + rowOverscanEndIdx: 16 + }); + }); - return { props, state }; - }; + it('should use height to calculate the range', () => { + expect(getRange({ height: 250 })).toEqual({ + rowVisibleStartIdx: 4, + rowVisibleEndIdx: 9, + rowOverscanStartIdx: 2, + rowOverscanEndIdx: 11 + }); + }); - it('should correctly set canvas height', () => { - const { state } = getState(); - expect(state.height).toBe(95); + it('should use scrollTop to calculate the range', () => { + expect(getRange({ scrollTop: 500 })).toEqual({ + rowVisibleStartIdx: 10, + rowVisibleEndIdx: 20, + rowOverscanStartIdx: 8, + rowOverscanEndIdx: 22 + }); }); - it('should correctly set visible rows count when rowsCount is greater than the rendered rows count', () => { - const { state } = getState(); - expect(state.rowOverscanEndIdx).toBe(8); + it('should use rowsCount to calculate the range', () => { + expect(getRange({ rowsCount: 5, scrollTop: 0 })).toEqual({ + rowVisibleStartIdx: 0, + rowVisibleEndIdx: 5, + rowOverscanStartIdx: 0, + rowOverscanEndIdx: 5 + }); }); - it('should correctly set visible rows count when rowsCount is less than the rendered rows count', () => { - const { state } = getState({ rowsCount: 8 }); - expect(state.rowOverscanEndIdx).toBe(8); + it('should use overscanRowCount to calculate the range', () => { + expect(getRange({ overscanRowCount: 10 })).toEqual({ + rowVisibleStartIdx: 4, + rowVisibleEndIdx: 14, + rowOverscanStartIdx: 2, + rowOverscanEndIdx: 24 + }); }); - it('should correctly set visible column count', () => { - const { state, props } = getState(); - expect(state.colVisibleEndIdx).toBe(props.columnMetrics.columns.length); + it('should use scrollDirection to calculate the range', () => { + expect(getRange({ overscanRowCount: 10, scrollDirection: SCROLL_DIRECTION.UP })).toEqual({ + rowVisibleStartIdx: 4, + rowVisibleEndIdx: 14, + rowOverscanStartIdx: 0, + rowOverscanEndIdx: 16 + }); }); }); - describe('getNonFrozenRenderedColumnCount', () => { - const viewportWidth = 100; - - const getCols = (): CalculatedColumn[] => [ - { idx: 0, key: 'col1', name: 'col1', width: 20, left: 0 }, - { idx: 1, key: 'col2', name: 'col2', width: 20, left: 20 }, - { idx: 2, key: 'col3', name: 'col3', width: 20, left: 40 }, - { idx: 3, key: 'col4', name: 'col4', width: 20, left: 60 }, - { idx: 4, key: 'col5', name: 'col5', width: 20, left: 80 }, - { idx: 5, key: 'col6', name: 'col6', width: 1, left: 100 }, - { idx: 6, key: 'col7', name: 'col7', width: 1, left: 101 }, - { idx: 7, key: 'col8', name: 'col8', width: 1, left: 102 }, - { idx: 8, key: 'col9', name: 'col9', width: 1, left: 103 } - ]; - - const verifyNonFrozenRenderedColumnCount = (width = viewportWidth, columns = getCols(), scrollLeft = 0) => { - const columnMetrics = { + describe('getHorizontalRangeToRender', () => { + function getColumnMetrics(): ColumnMetrics { + const columns = [...Array(500).keys()].map(i => ({ idx: i, key: `col${i}`, name: `col${i}`, width: 100, left: i * 100 })); + return { columns, width: 0, - totalColumnWidth: 0, + totalColumnWidth: 200, totalWidth: 0, - minColumnWidth: 0 + minColumnWidth: 80, + lastFrozenColumnIndex: -1 }; - return getNonFrozenRenderedColumnCount(columnMetrics, width, scrollLeft); - }; - - it('correctly set rendered columns count for a set width', () => { - const count = verifyNonFrozenRenderedColumnCount(); - expect(count).toBe(5); // col1, col2, col3, col4, col5 - }); - - it('should return all columns that fit width after last frozen column', () => { - const columns = getCols(); - columns[1].frozen = true; - const count = verifyNonFrozenRenderedColumnCount(100, columns); - expect(count).toBe(3); // col3, col4, col5 - }); - - it('should return all columns that fit width after last frozen column when grid is scrolled', () => { - // 10 px of first non frozen column are hidden, with remaining 10 px visible - const scrollLeft = 50; - // This means that there are 10px of extra space to fill with columns - const columns = getCols(); - columns[1].frozen = true; - const count = verifyNonFrozenRenderedColumnCount(100, columns, scrollLeft); - expect(count).toBe(5); // col5, col6, col7, col8, col9 - }); - }); - - describe('getVisibleBoundaries', () => { - const GRID_HEIGHT = 350; - const ROW_HEIGHT = 35; - const TOTAL_ROWS = 100; - const EXPECTED_NUMBER_VISIBLE_ROWS = 10; + } - describe('When scroll top is 0', () => { - it('should set the rowVisibleStartIdx to be 0', () => { - const scrollTop = 0; - const { rowVisibleStartIdx } = getVisibleBoundaries(GRID_HEIGHT, ROW_HEIGHT, scrollTop, TOTAL_ROWS); - expect(rowVisibleStartIdx).toBe(0); + function getRange>(overrides: Pick, K>) { + return getHorizontalRangeToRender({ + columnMetrics: getColumnMetrics(), + scrollLeft: 200, + viewportWidth: 1000, + scrollDirection: SCROLL_DIRECTION.RIGHT, + ...overrides }); + } - it('should set the rowVisibleEndIdx to be last rendered row', () => { - const scrollTop = 0; - const { rowVisibleEndIdx } = getVisibleBoundaries(GRID_HEIGHT, ROW_HEIGHT, scrollTop, TOTAL_ROWS); - expect(rowVisibleEndIdx).toBe(EXPECTED_NUMBER_VISIBLE_ROWS); + it('should use scrollLeft to calculate the range', () => { + expect(getRange({ scrollLeft: 300 })).toEqual({ + colVisibleStartIdx: 3, + colVisibleEndIdx: 13, + colOverscanStartIdx: 1, + colOverscanEndIdx: 15 }); }); - describe('When scrolling', () => { - const scrollDown = (getScrollTop: (n: number) => number, assert: (n: number, boundaries: VisibleBoundaries) => void) => { - const NUMBER_OF_TESTS = 10; - for (let n = 1; n < NUMBER_OF_TESTS; n++) { - const boundaries = getVisibleBoundaries(GRID_HEIGHT, ROW_HEIGHT, getScrollTop(n), TOTAL_ROWS); - assert(n, boundaries); - } - }; - describe('When incrementing scroll by n*rowHeight', () => { - it('should increase rowVisibleStartIdx by n rows', () => { - const getScrollTop = (n: number) => n * ROW_HEIGHT; - scrollDown(getScrollTop, (n, { rowVisibleStartIdx }) => { - expect(rowVisibleStartIdx).toBe(n); - }); - }); - - it('should increase rowVisibleEndIdx by (n + total rendered rows)', () => { - const getScrollTop = (n: number) => n * ROW_HEIGHT; - scrollDown(getScrollTop, (n, { rowVisibleEndIdx }) => { - expect(rowVisibleEndIdx).toBe(EXPECTED_NUMBER_VISIBLE_ROWS + n); - }); - }); - }); - - describe('When incrementing scroll by a decimal number within 0.5 buffer of n*rowHeight', () => { - it('should increase rowVisibleEndIdx by (n + total rendered rows)', () => { - const clientScrollError = 0.5; - const getScrollTop = (n: number) => (n * ROW_HEIGHT) - clientScrollError; - scrollDown(getScrollTop, (n, { rowVisibleEndIdx }) => { - expect(rowVisibleEndIdx).toBe(EXPECTED_NUMBER_VISIBLE_ROWS + n); - }); - }); - - it('should increase rowVisibleEndIdx by n rows', () => { - const clientScrollError = 0.5; - const getScrollTop = (n: number) => (n * ROW_HEIGHT) - clientScrollError; - scrollDown(getScrollTop, (n, { rowVisibleStartIdx }) => { - expect(rowVisibleStartIdx).toBe(n); - }); - }); + it('should use viewportWidth to calculate the range', () => { + expect(getRange({ viewportWidth: 500 })).toEqual({ + colVisibleStartIdx: 2, + colVisibleEndIdx: 7, + colOverscanStartIdx: 0, + colOverscanEndIdx: 9 }); }); - describe('getNonFrozenVisibleColStartIdx', () => { - it('should return 0 if no frozen columns and grid not scrolled left', () => { - const scrollLeft = 0; - const colVisibleStartIdx = getNonFrozenVisibleColStartIdx(getColumns(), scrollLeft); - expect(colVisibleStartIdx).toBe(0); - }); - - it('should return first fully visible column when scrolled left', () => { - const scrollLeft = 100; - const colVisibleStartIdx = getNonFrozenVisibleColStartIdx(getColumns(), scrollLeft); - expect(colVisibleStartIdx).toBe(1); - }); - - it('should return first partially visible column when scrolled left (left bound)', () => { - const scrollLeft = 99; - const colVisibleStartIdx = getNonFrozenVisibleColStartIdx(getColumns(), scrollLeft); - expect(colVisibleStartIdx).toBe(0); - }); - - it('should return first partially visible column when scrolled left (right bound)', () => { - const scrollLeft = 101; - const colVisibleStartIdx = getNonFrozenVisibleColStartIdx(getColumns(), scrollLeft); - expect(colVisibleStartIdx).toBe(1); - }); - - const expectIdxWhenColumsFrozen = (scrollLeft: number) => { - const columns = getColumns(); - columns[1].frozen = true; - const colVisibleStartIdx = getNonFrozenVisibleColStartIdx(columns, scrollLeft); - return expect(colVisibleStartIdx); - }; - - it('should return first non frozen column that appears after last frozen column', () => { - const scrollLeft = 0; - expectIdxWhenColumsFrozen(scrollLeft).toBe(2); - }); - - it('should return first fully visible non frozen column that appears after last frozen column when scrolled left', () => { - const scrollLeft = 200; - expectIdxWhenColumsFrozen(scrollLeft).toBe(4); - }); - - it('should return first partially visible non frozen column that appears after last frozen column when scrolled left', () => { - const scrollLeft = 201; - expectIdxWhenColumsFrozen(scrollLeft).toBe(4); - }); - - it('should return first partially visible non frozen column that appears after last frozen column when scrolled left', () => { - const scrollLeft = 199; - expectIdxWhenColumsFrozen(scrollLeft).toBe(3); + it('should use overscanColumnCount to calculate the range', () => { + expect(getRange({ overscanColumnCount: 5 })).toEqual({ + colVisibleStartIdx: 2, + colVisibleEndIdx: 12, + colOverscanStartIdx: 0, + colOverscanEndIdx: 17 }); }); - describe('getScrollDirection', () => { - it('should return SCROLL_DIRECTION.DOWN iF previous scrollTop is less than current scrollTop', () => { - const prevScroll = { scrollTop: 100 }; - const currentScrollTop = 200; - const currentScrollLeft = 0; - expect(getScrollDirection(prevScroll, currentScrollTop, currentScrollLeft)).toBe(SCROLL_DIRECTION.DOWN); - }); - - it('should return SCROLL_DIRECTION.UP iF previous scrollTop is greater than current scrollTop', () => { - const prevScroll = { scrollTop: 200 }; - const currentScrollTop = 0; - const currentScrollLeft = 0; - expect(getScrollDirection(prevScroll, currentScrollTop, currentScrollLeft)).toBe(SCROLL_DIRECTION.UP); - }); - - it('should return SCROLL_DIRECTION.RIGHT iF previous scrollLeft is less than current scrollLeft', () => { - const prevScroll = { scrollLeft: 200 }; - const currentScrollTop = 0; - const currentScrollLeft = 400; - expect(getScrollDirection(prevScroll, currentScrollTop, currentScrollLeft)).toBe(SCROLL_DIRECTION.RIGHT); - }); - - it('should return SCROLL_DIRECTION.LEFT iF previous scrollLeft is greater than current scrollLeft', () => { - const prevScroll = { scrollLeft: 200 }; - const currentScrollTop = 0; - const currentScrollLeft = 0; - expect(getScrollDirection(prevScroll, currentScrollTop, currentScrollLeft)).toBe(SCROLL_DIRECTION.LEFT); - }); - - it('should return SCROLL_DIRECTION.NONE if current scroll is equal to previous scroll', () => { - const prevScroll = { scrollLeft: 200, scrollTop: 0 }; - const currentScrollTop = 0; - const currentScrollLeft = 200; - expect(getScrollDirection(prevScroll, currentScrollTop, currentScrollLeft)).toBe(SCROLL_DIRECTION.NONE); + it('should use overscanColumnCount to calculate the range', () => { + expect(getRange({ overscanColumnCount: 5, scrollDirection: SCROLL_DIRECTION.LEFT, scrollLeft: 1000 })).toEqual({ + colVisibleStartIdx: 10, + colVisibleEndIdx: 20, + colOverscanStartIdx: 5, + colOverscanEndIdx: 22 }); }); - describe('getRowOverscanStartIdx', () => { - const rowVisibleStartIdx = 2; - it('should return rowVisibleStartIdx - OVERSCAN_ROWS if scroll direction is upwards', () => { - expect(getRowOverscanStartIdx(SCROLL_DIRECTION.UP, rowVisibleStartIdx)).toBe(rowVisibleStartIdx - OVERSCAN_ROWS); - }); - - it('should return rowVisibleStartIdx if scroll direction is left', () => { - expect(getRowOverscanStartIdx(SCROLL_DIRECTION.LEFT, rowVisibleStartIdx)).toBe(rowVisibleStartIdx); - }); - - it('should return rowVisibleStartIdx if scroll direction is right', () => { - expect(getRowOverscanStartIdx(SCROLL_DIRECTION.RIGHT, rowVisibleStartIdx)).toBe(rowVisibleStartIdx); - }); - - it('should return rowVisibleStartIdx if scroll direction is downwards', () => { - expect(getRowOverscanStartIdx(SCROLL_DIRECTION.DOWN, rowVisibleStartIdx)).toBe(rowVisibleStartIdx); - }); + it('should use frozen columns to calculate the range', () => { + const columnMetrics = getColumnMetrics(); + columnMetrics.columns[0].frozen = true; + columnMetrics.columns[1].frozen = true; + columnMetrics.columns[2].frozen = true; + columnMetrics.lastFrozenColumnIndex = 2; - it('should return 0 if rowVisibleStartIdx negative', () => { - expect(getRowOverscanStartIdx(SCROLL_DIRECTION.DOWN, -1)).toBe(0); + expect(getRange({ scrollLeft: 500, columnMetrics })).toEqual({ + colVisibleStartIdx: 8, + colVisibleEndIdx: 15, + colOverscanStartIdx: 6, + colOverscanEndIdx: 17 }); }); + }); - describe('getRowOverscanEndIdx', () => { - const rowVisibleEndIdx = 20; - const totalNumberOfRows = 30; - it('should return rowVisibleStartIdx + OVERSCAN_ROWS if scroll direction is downward', () => { - expect(getRowOverscanEndIdx(SCROLL_DIRECTION.DOWN, rowVisibleEndIdx, totalNumberOfRows)).toBe(rowVisibleEndIdx + OVERSCAN_ROWS); - }); - - it('should return totalNumberOfRows if scroll direction is downward and rowVisibleEndIdx == totalNumberRows', () => { - expect(getRowOverscanEndIdx(SCROLL_DIRECTION.DOWN, totalNumberOfRows, totalNumberOfRows)).toBe(totalNumberOfRows); - }); - - it('should return rowVisibleEndIdx if scroll direction is left', () => { - expect(getRowOverscanEndIdx(SCROLL_DIRECTION.LEFT, rowVisibleEndIdx, totalNumberOfRows)).toBe(rowVisibleEndIdx); - }); - - it('should return rowVisibleEndIdx if scroll direction is right', () => { - expect(getRowOverscanEndIdx(SCROLL_DIRECTION.RIGHT, rowVisibleEndIdx, totalNumberOfRows)).toBe(rowVisibleEndIdx); - }); - - it('should return rowVisibleEndIdx if scroll direction is upwards', () => { - expect(getRowOverscanEndIdx(SCROLL_DIRECTION.UP, rowVisibleEndIdx, totalNumberOfRows)).toBe(rowVisibleEndIdx); - }); + describe('getScrollDirection', () => { + const prevScroll = { scrollTop: 100, scrollLeft: 100 }; + it('should return SCROLL_DIRECTION.DOWN iF previous scrollTop is less than current scrollTop', () => { + expect(getScrollDirection(prevScroll, { ...prevScroll, scrollTop: 101 })).toBe(SCROLL_DIRECTION.DOWN); }); - describe('getColOverscanStartIdx', () => { - const colVisibleStartIdx = 5; - const lastFrozenColumnIdx = -1; - it('should return colVisibleStartIdx if scroll direction is downward', () => { - expect(getColOverscanStartIdx(SCROLL_DIRECTION.DOWN, colVisibleStartIdx, lastFrozenColumnIdx)).toBe(colVisibleStartIdx); - }); - - it('should return colVisibleStartIdx if scroll direction is upwards', () => { - expect(getColOverscanStartIdx(SCROLL_DIRECTION.UP, colVisibleStartIdx, lastFrozenColumnIdx)).toBe(colVisibleStartIdx); - }); - - it('should return 0 if scroll direction is left', () => { - expect(getColOverscanStartIdx(SCROLL_DIRECTION.LEFT, colVisibleStartIdx, lastFrozenColumnIdx)).toBe(0); - }); - - it('should return 0 if scroll direction is right', () => { - expect(getColOverscanStartIdx(SCROLL_DIRECTION.RIGHT, colVisibleStartIdx, lastFrozenColumnIdx)).toBe(0); - }); + it('should return SCROLL_DIRECTION.UP iF previous scrollTop is greater than current scrollTop', () => { + expect(getScrollDirection(prevScroll, { ...prevScroll, scrollTop: 99 })).toBe(SCROLL_DIRECTION.UP); }); - describe('getColOverscanEndIdx', () => { - const colVisibleEndIdx = 20; - const totalNumberOfColumns = 30; - it('should return colVisibleStartIdx if scroll direction is downward', () => { - expect(getColOverscanEndIdx(SCROLL_DIRECTION.DOWN, colVisibleEndIdx, totalNumberOfColumns)).toBe(colVisibleEndIdx); - }); - - it('should return colVisibleEndIdx if scroll direction is upwards', () => { - expect(getColOverscanEndIdx(SCROLL_DIRECTION.UP, colVisibleEndIdx, totalNumberOfColumns)).toBe(colVisibleEndIdx); - }); + it('should return SCROLL_DIRECTION.RIGHT iF previous scrollLeft is less than current scrollLeft', () => { + expect(getScrollDirection(prevScroll, { ...prevScroll, scrollLeft: 101 })).toBe(SCROLL_DIRECTION.RIGHT); + }); - it('should return totalNumberOfColumns if scroll direction is left', () => { - expect(getColOverscanEndIdx(SCROLL_DIRECTION.LEFT, colVisibleEndIdx, totalNumberOfColumns)).toBe(totalNumberOfColumns); - }); + it('should return SCROLL_DIRECTION.LEFT iF previous scrollLeft is greater than current scrollLeft', () => { + expect(getScrollDirection(prevScroll, { ...prevScroll, scrollLeft: 99 })).toBe(SCROLL_DIRECTION.LEFT); + }); - it('should return totalNumberOfColumns if scroll direction is right', () => { - expect(getColOverscanEndIdx(SCROLL_DIRECTION.RIGHT, colVisibleEndIdx, totalNumberOfColumns)).toBe(totalNumberOfColumns); - }); + it('should return SCROLL_DIRECTION.NONE if current scroll is equal to previous scroll', () => { + expect(getScrollDirection(prevScroll, { ...prevScroll })).toBe(SCROLL_DIRECTION.NONE); }); }); }); diff --git a/packages/react-data-grid/src/utils/domUtils.tsx b/packages/react-data-grid/src/utils/domUtils.tsx new file mode 100644 index 0000000000..798a701a80 --- /dev/null +++ b/packages/react-data-grid/src/utils/domUtils.tsx @@ -0,0 +1,32 @@ +let size: number | undefined; + +export function getScrollbarSize(): number { + if (size === undefined) { + const scrollDiv = document.createElement('div'); + + scrollDiv.style.position = 'absolute'; + scrollDiv.style.top = '-9999px'; + scrollDiv.style.width = '50px'; + scrollDiv.style.height = '50px'; + scrollDiv.style.overflow = 'scroll'; + + document.body.appendChild(scrollDiv); + size = scrollDiv.offsetWidth - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + } + + return size; +} + +let positionSticky: boolean | undefined; + +export function isPositionStickySupported(): boolean { + if (positionSticky === undefined) { + const el = document.createElement('a'); + const { style } = el; + style.cssText = 'position:-webkit-sticky;position:sticky'; + + positionSticky = style.position ? style.position.includes('sticky') : false; + } + return positionSticky; +} diff --git a/packages/react-data-grid/src/utils/index.tsx b/packages/react-data-grid/src/utils/index.tsx new file mode 100644 index 0000000000..67f8cabce4 --- /dev/null +++ b/packages/react-data-grid/src/utils/index.tsx @@ -0,0 +1,2 @@ +export * from './domUtils'; +export * from './canvasUtils'; diff --git a/packages/react-data-grid/src/utils/viewportUtils.ts b/packages/react-data-grid/src/utils/viewportUtils.ts index d6d02ce366..ad4b0c34a5 100644 --- a/packages/react-data-grid/src/utils/viewportUtils.ts +++ b/packages/react-data-grid/src/utils/viewportUtils.ts @@ -1,40 +1,7 @@ -import { isFrozen } from '../ColumnUtils'; import { SCROLL_DIRECTION } from '../common/enums'; -import { CalculatedColumn, ColumnMetrics } from '../common/types'; +import { CalculatedColumn, ScrollPosition, ColumnMetrics } from '../common/types'; -export const OVERSCAN_ROWS = 2; - -const { min, max, ceil, round } = Math; - -export function getGridState(props: { columnMetrics: ColumnMetrics; rowsCount: number; minHeight: number; rowHeight: number; rowOffsetHeight: number }) { - const totalNumberColumns = props.columnMetrics.columns.length; - const canvasHeight = props.minHeight - props.rowOffsetHeight; - const renderedRowsCount = ceil((props.minHeight - props.rowHeight) / props.rowHeight); - const rowOverscanEndIdx = min(props.rowsCount, renderedRowsCount * 2); - - return { - rowOverscanStartIdx: 0, - rowOverscanEndIdx, - rowVisibleStartIdx: 0, - rowVisibleEndIdx: renderedRowsCount, - height: canvasHeight, - scrollTop: 0, - scrollLeft: 0, - colVisibleStartIdx: 0, - colVisibleEndIdx: totalNumberColumns, - colOverscanStartIdx: 0, - colOverscanEndIdx: totalNumberColumns, - isScrolling: false, - lastFrozenColumnIndex: 0 - }; -} - -export function findLastFrozenColumnIndex(columns: CalculatedColumn[]): number { - return columns.findIndex(c => isFrozen(c)); -} - -function getTotalFrozenColumnWidth(columns: CalculatedColumn[]): number { - const lastFrozenColumnIndex = findLastFrozenColumnIndex(columns); +function getTotalFrozenColumnWidth(columns: CalculatedColumn[], lastFrozenColumnIndex: number): number { if (lastFrozenColumnIndex === -1) { return 0; } @@ -47,7 +14,7 @@ function getColumnCountForWidth(columns: CalculatedColumn[], initialWidth: let count = 0; columns.forEach((column, idx) => { - if (idx! >= colVisibleStartIdx) { + if (idx >= colVisibleStartIdx) { width -= column.width; if (width >= 0) { count++; @@ -58,84 +25,87 @@ function getColumnCountForWidth(columns: CalculatedColumn[], initialWidth: return count; } -export function getNonFrozenVisibleColStartIdx(columns: CalculatedColumn[], scrollLeft: number): number { - let remainingScroll = scrollLeft; - const lastFrozenColumnIndex = findLastFrozenColumnIndex(columns); - const nonFrozenColumns = columns.slice(lastFrozenColumnIndex + 1); - let columnIndex = lastFrozenColumnIndex; - while (remainingScroll >= 0 && columnIndex < nonFrozenColumns.length) { - columnIndex++; - const column = columns[columnIndex]; - remainingScroll -= column ? column.width : 0; - } - return max(columnIndex, 0); +export interface VerticalRangeToRenderParams { + height: number; + rowHeight: number; + scrollTop: number; + rowsCount: number; + scrollDirection: SCROLL_DIRECTION; + overscanRowCount?: number; } -export function getNonFrozenRenderedColumnCount(columnMetrics: ColumnMetrics, viewportDomWidth: number, scrollLeft: number): number { - const { columns, totalColumnWidth } = columnMetrics; - if (columns.length === 0) { - return 0; - } - const colVisibleStartIdx = getNonFrozenVisibleColStartIdx(columns, scrollLeft); - const totalFrozenColumnWidth = getTotalFrozenColumnWidth(columns); - const viewportWidth = viewportDomWidth > 0 ? viewportDomWidth : totalColumnWidth; - const firstColumn = columns[colVisibleStartIdx]; - // calculate the portion width of first column hidden behind frozen columns - const scrolledFrozenWidth = totalFrozenColumnWidth + scrollLeft; - const firstColumnHiddenWidth = scrolledFrozenWidth > firstColumn.left ? scrolledFrozenWidth - firstColumn.left : 0; - const initialWidth = viewportWidth - totalFrozenColumnWidth + firstColumnHiddenWidth; - return getColumnCountForWidth(columns, initialWidth, colVisibleStartIdx); -} - -export interface VisibleBoundaries { +export interface VerticalRangeToRender { rowVisibleStartIdx: number; rowVisibleEndIdx: number; + rowOverscanStartIdx: number; + rowOverscanEndIdx: number; } -export function getVisibleBoundaries(gridHeight: number, rowHeight: number, scrollTop: number, rowsCount: number): VisibleBoundaries { - const renderedRowsCount = ceil(gridHeight / rowHeight); - const rowVisibleStartIdx = max(0, round(scrollTop / rowHeight)); - const rowVisibleEndIdx = min(rowVisibleStartIdx + renderedRowsCount, rowsCount); - return { rowVisibleStartIdx, rowVisibleEndIdx }; +export function getVerticalRangeToRender({ + height, + rowHeight, + scrollTop, + rowsCount, + scrollDirection, + overscanRowCount = 2 +}: VerticalRangeToRenderParams): VerticalRangeToRender { + const renderedRowsCount = Math.ceil(height / rowHeight); + const rowVisibleStartIdx = Math.max(0, Math.round(scrollTop / rowHeight)); + const rowVisibleEndIdx = Math.min(rowsCount, rowVisibleStartIdx + renderedRowsCount); + const rowOverscanStartIdx = Math.max(0, rowVisibleStartIdx - (scrollDirection === SCROLL_DIRECTION.UP ? overscanRowCount : 2)); + const rowOverscanEndIdx = Math.min(rowsCount, rowVisibleEndIdx + (scrollDirection === SCROLL_DIRECTION.DOWN ? overscanRowCount : 2)); + + return { rowVisibleStartIdx, rowVisibleEndIdx, rowOverscanStartIdx, rowOverscanEndIdx }; } -interface ScrollState { - scrollTop?: number; - scrollLeft?: number; +export interface HorizontalRangeToRender { + colVisibleStartIdx: number; + colVisibleEndIdx: number; + colOverscanStartIdx: number; + colOverscanEndIdx: number; } -export function getScrollDirection(lastScroll: ScrollState, scrollTop: number, scrollLeft: number): SCROLL_DIRECTION { - if (scrollTop !== lastScroll.scrollTop && lastScroll.scrollTop !== undefined) { - return scrollTop - lastScroll.scrollTop >= 0 ? SCROLL_DIRECTION.DOWN : SCROLL_DIRECTION.UP; - } - if (scrollLeft !== lastScroll.scrollLeft && lastScroll.scrollLeft !== undefined) { - return scrollLeft - lastScroll.scrollLeft >= 0 ? SCROLL_DIRECTION.RIGHT : SCROLL_DIRECTION.LEFT; - } - return SCROLL_DIRECTION.NONE; +export interface HorizontalRangeToRenderParams { + columnMetrics: ColumnMetrics; + viewportWidth: number; + scrollLeft: number; + scrollDirection: SCROLL_DIRECTION; + overscanColumnCount?: number; } -export function getRowOverscanStartIdx(scrollDirection: SCROLL_DIRECTION, rowVisibleStartIdx: number): number { - return scrollDirection === SCROLL_DIRECTION.UP ? max(0, rowVisibleStartIdx - OVERSCAN_ROWS) : max(0, rowVisibleStartIdx); -} +export function getHorizontalRangeToRender({ + columnMetrics, + scrollLeft, + viewportWidth, + scrollDirection, + overscanColumnCount = 2 +}: HorizontalRangeToRenderParams): HorizontalRangeToRender { + const { columns, totalColumnWidth, lastFrozenColumnIndex } = columnMetrics; -export function getRowOverscanEndIdx(scrollDirection: SCROLL_DIRECTION, rowVisibleEndIdx: number, rowsCount: number): number { - if (scrollDirection === SCROLL_DIRECTION.DOWN) { - const overscanBoundaryIdx = rowVisibleEndIdx + OVERSCAN_ROWS; - return min(overscanBoundaryIdx, rowsCount); + let remainingScroll = scrollLeft; + let columnIndex = lastFrozenColumnIndex; + while (remainingScroll >= 0 && columnIndex < columns.length) { + columnIndex++; + const column = columns[columnIndex]; + remainingScroll -= column ? column.width : 0; } - return rowVisibleEndIdx; -} + const colVisibleStartIdx = Math.max(columnIndex, 0); -export function getColOverscanStartIdx(scrollDirection: SCROLL_DIRECTION, colVisibleStartIdx: number, lastFrozenColumnIdx: number): number { - if (scrollDirection === SCROLL_DIRECTION.LEFT || scrollDirection === SCROLL_DIRECTION.RIGHT) { - return lastFrozenColumnIdx > -1 ? lastFrozenColumnIdx + 1 : 0; - } - return colVisibleStartIdx; + const totalFrozenColumnWidth = getTotalFrozenColumnWidth(columns, lastFrozenColumnIndex); + viewportWidth = viewportWidth > 0 ? viewportWidth : totalColumnWidth; + const availableWidth = viewportWidth - totalFrozenColumnWidth; + const nonFrozenRenderedColumnCount = getColumnCountForWidth(columns, availableWidth, colVisibleStartIdx); + const colVisibleEndIdx = Math.min(columns.length, colVisibleStartIdx + nonFrozenRenderedColumnCount); + const colOverscanStartIdx = Math.max(0, colVisibleStartIdx - (scrollDirection === SCROLL_DIRECTION.LEFT ? overscanColumnCount : 2)); + const colOverscanEndIdx = Math.min(columns.length, colVisibleEndIdx + (scrollDirection === SCROLL_DIRECTION.RIGHT ? overscanColumnCount : 2)); + + return { colVisibleStartIdx, colVisibleEndIdx, colOverscanStartIdx, colOverscanEndIdx }; } -export function getColOverscanEndIdx(scrollDirection: SCROLL_DIRECTION, colVisibleEndIdx: number, totalNumberColumns: number): number { - if (scrollDirection === SCROLL_DIRECTION.DOWN || scrollDirection === SCROLL_DIRECTION.UP) { - return colVisibleEndIdx; - } - return totalNumberColumns; +export function getScrollDirection(prevScroll: ScrollPosition, nextScroll: ScrollPosition): SCROLL_DIRECTION { + if (nextScroll.scrollTop > prevScroll.scrollTop) return SCROLL_DIRECTION.DOWN; + if (nextScroll.scrollTop < prevScroll.scrollTop) return SCROLL_DIRECTION.UP; + if (nextScroll.scrollLeft > prevScroll.scrollLeft) return SCROLL_DIRECTION.RIGHT; + if (nextScroll.scrollLeft < prevScroll.scrollLeft) return SCROLL_DIRECTION.LEFT; + return SCROLL_DIRECTION.NONE; } diff --git a/packages/react-data-grid/style/rdg-cell.less b/packages/react-data-grid/style/rdg-cell.less index 7cf26833b9..1e4aaff261 100644 --- a/packages/react-data-grid/style/rdg-cell.less +++ b/packages/react-data-grid/style/rdg-cell.less @@ -34,6 +34,14 @@ z-index: 3 } +@supports (position: sticky) or (position: -webkit-sticky) { + .react-grid-Cell--frozen { + display: inline-block; + position: -webkit-sticky; + position: sticky; + } +} + .react-contextmenu--visible { z-index: 1000; } diff --git a/packages/react-data-grid/style/rdg-core.less b/packages/react-data-grid/style/rdg-core.less index ddcef66421..dbd0022154 100644 --- a/packages/react-data-grid/style/rdg-core.less +++ b/packages/react-data-grid/style/rdg-core.less @@ -1,3 +1,5 @@ +@import './variables.less'; + .react-grid-Container { // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context // We set a stacking context so internal components don't render above external components. @@ -6,26 +8,12 @@ } .react-grid-Grid { - position: relative; - overflow: hidden; - outline: none; - clear : both; - padding: 0; - background-color: #fff; - color: inherit; border: 1px solid #ddd; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + .user-select(); } .react-grid-Canvas { - position: absolute; - top: 0; - left: 0; + position: relative; overflow-x: auto; overflow-y: scroll; background-color: #fff; diff --git a/packages/react-data-grid/style/rdg-header.less b/packages/react-data-grid/style/rdg-header.less index 5a9689e87a..9e643f5f72 100644 --- a/packages/react-data-grid/style/rdg-header.less +++ b/packages/react-data-grid/style/rdg-header.less @@ -1,28 +1,21 @@ +@import './variables.less'; + .react-grid-Header { - position: relative; box-shadow: 0 0 4px 0 #ddd; background: #f9f9f9; } + .react-grid-Header--resizing { cursor: ew-resize; } -.react-grid-HeaderRow { - position: absolute; - left: 0; - overflow-x: hidden; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - - > div { - white-space: nowrap; - overflow: hidden; - } +.react-grid-HeaderRow { + position: relative; + overflow: hidden; + .user-select(); } + .react-grid-HeaderCell { - display: inline-block; position: absolute; padding: 8px; margin: 0; @@ -32,10 +25,7 @@ background: #f9f9f9; border-right: 1px solid #dddddd; border-bottom: 1px solid #dddddd; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + .user-select(); } .rdg-header-cell-resizable::after { @@ -51,6 +41,7 @@ .react-grid-HeaderCell--frozen:last-of-type { box-shadow: 2px 0 5px -2px rgba(136, 136, 136, .3); } + .rdg-can-drop > .react-grid-HeaderCell { background: #ececec; } diff --git a/packages/react-data-grid/style/rdg-row.less b/packages/react-data-grid/style/rdg-row.less index e7eafe6d65..f2b433bacf 100644 --- a/packages/react-data-grid/style/rdg-row.less +++ b/packages/react-data-grid/style/rdg-row.less @@ -1,5 +1,5 @@ .react-grid-Row { - overflow: hidden; + position: relative; } .react-grid-Row:hover .react-grid-Cell, diff --git a/packages/react-data-grid/style/rdg-viewport.less b/packages/react-data-grid/style/rdg-viewport.less deleted file mode 100644 index f0c3da8435..0000000000 --- a/packages/react-data-grid/style/rdg-viewport.less +++ /dev/null @@ -1,8 +0,0 @@ -.rdg-viewport { - position: absolute; - left: 0; - right: 0; - bottom: 0; - padding: 0; - overflow: hidden; -} diff --git a/packages/react-data-grid/style/react-data-grid.less b/packages/react-data-grid/style/react-data-grid.less index ac08a5e0d6..ff25dee049 100644 --- a/packages/react-data-grid/style/react-data-grid.less +++ b/packages/react-data-grid/style/react-data-grid.less @@ -4,4 +4,3 @@ @import 'rdg-header.less'; @import 'rdg-interaction-masks.less'; @import 'rdg-row.less'; -@import 'rdg-viewport.less'; diff --git a/packages/react-data-grid/style/variables.less b/packages/react-data-grid/style/variables.less new file mode 100644 index 0000000000..388b69a3e8 --- /dev/null +++ b/packages/react-data-grid/style/variables.less @@ -0,0 +1,6 @@ +.user-select(@value: none) { + -webkit-user-select: @value; + -moz-user-select: @value; + -ms-user-select: @value; + user-select: @value; +}