From 00c4def51ec1f093b5c6dabffd74dc7efcdee9a9 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 10 Feb 2017 15:24:09 -0800 Subject: [PATCH 01/31] Don't modify a re-used style object. Fiber freezes these. --- source/Grid/Grid.example.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/Grid/Grid.example.js b/source/Grid/Grid.example.js index 9fef320a9..1de137c1c 100644 --- a/source/Grid/Grid.example.js +++ b/source/Grid/Grid.example.js @@ -243,7 +243,10 @@ export default class GridExample extends Component { const classNames = cn(styles.cell, styles.letterCell) - style.backgroundColor = datum.color + style = { + ...style, + backgroundColor: datum.color + } return (
Date: Fri, 10 Feb 2017 15:25:41 -0800 Subject: [PATCH 02/31] Upgraded local (dev dep) React to 16.0.0-alpha.2 --- package.json | 8 ++++---- yarn.lock | 47 +++++++++++++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 44b6c8116..93d5cb8bf 100644 --- a/package.json +++ b/package.json @@ -128,11 +128,11 @@ "postcss": "^5.0.14", "postcss-cli": "^2.3.3", "postcss-loader": "^0.9.1", - "react": "^15.3.1", - "react-addons-shallow-compare": "^15.3.1", - "react-addons-test-utils": "^15.3.1", + "react": "16.0.0-alpha.2", + "react-addons-shallow-compare": "16.0.0-alpha.2", + "react-addons-test-utils": "16.0.0-alpha.2", "react-codemirror": "^0.2.6", - "react-dom": "^15.3.1", + "react-dom": "16.0.0-alpha.2", "react-router": "^4.0.0-alpha.5", "react-transform-catch-errors": "^1.0.2", "react-transform-hmr": "^1.0.2", diff --git a/yarn.lock b/yarn.lock index dc7857fed..8bc827d85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2245,15 +2245,16 @@ fb-watchman@^1.8.0, fb-watchman@^1.9.0: dependencies: bser "1.0.2" -fbjs@^0.8.4: - version "0.8.6" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.6.tgz#7eb67d6986b2d5007a9b6e92e0e7cb6f75cad290" +fbjs@^0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.9.tgz#180247fbd347dcc9004517b904f865400a0c8f14" dependencies: core-js "^1.0.0" isomorphic-fetch "^2.1.1" loose-envify "^1.0.0" object-assign "^4.1.0" promise "^7.1.1" + setimmediate "^1.0.5" ua-parser-js "^0.7.9" figures@^1.3.5: @@ -4571,13 +4572,19 @@ rc@~1.1.6: minimist "^1.2.0" strip-json-comments "~1.0.4" -react-addons-shallow-compare@^15.3.1: - version "15.3.2" - resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.3.2.tgz#c9edba49b9eab44d0c59024d289beb1ab97318b5" +react-addons-shallow-compare@16.0.0-alpha.2: + version "16.0.0-alpha.2" + resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-16.0.0-alpha.2.tgz#a5848da5c577e9c330bff8fcbf3ec6f9885227eb" + dependencies: + fbjs "^0.8.9" + object-assign "^4.1.0" -react-addons-test-utils@^15.3.1: - version "15.3.2" - resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.3.2.tgz#c09a44f583425a4a9c1b38444d7a6c3e6f0f41f6" +react-addons-test-utils@16.0.0-alpha.2: + version "16.0.0-alpha.2" + resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-16.0.0-alpha.2.tgz#cd1a32fea6511110617dd165f199ce4a424b868f" + dependencies: + fbjs "^0.8.9" + object-assign "^4.1.0" react-broadcast@^0.1.1: version "0.1.2" @@ -4597,9 +4604,13 @@ react-deep-force-update@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-1.0.1.tgz#f911b5be1d2a6fe387507dd6e9a767aa2924b4c7" -react-dom@^15.3.1: - version "15.3.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f" +react-dom@16.0.0-alpha.2: + version "16.0.0-alpha.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.0.0-alpha.2.tgz#b2c916b0b0d8b34567967bdb945f55827a571c29" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" react-proxy@^1.1.7: version "1.1.8" @@ -4628,11 +4639,11 @@ react-transform-hmr@^1.0.2: global "^4.3.0" react-proxy "^1.1.7" -react@^15.3.1: - version "15.3.2" - resolved "https://registry.yarnpkg.com/react/-/react-15.3.2.tgz#a7bccd2fee8af126b0317e222c28d1d54528d09e" +react@16.0.0-alpha.2: + version "16.0.0-alpha.2" + resolved "https://registry.yarnpkg.com/react/-/react-16.0.0-alpha.2.tgz#9c81ac487d2795e1fba1bcc3bdff702c6267b42f" dependencies: - fbjs "^0.8.4" + fbjs "^0.8.9" loose-envify "^1.1.0" object-assign "^4.1.0" @@ -5036,6 +5047,10 @@ set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + setprototypeof@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.1.tgz#52009b27888c4dc48f591949c0a8275834c1ca7e" From cda081b56d2105677d28c939c4ebca42ca1e8bf2 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 11 Feb 2017 08:15:11 -0800 Subject: [PATCH 03/31] Refactored CellMeasurer and CellMeasurerCache to support a more standard lifecycle measurements --- source/CellMeasurer/CellMeasurer.example.js | 169 ++----- source/CellMeasurer/CellMeasurer.jest.js | 450 +++--------------- source/CellMeasurer/CellMeasurer.js | 268 ++--------- source/CellMeasurer/CellMeasurerCache.jest.js | 76 +++ source/CellMeasurer/CellMeasurerCache.js | 156 ++++++ .../CellMeasurer/defaultCellSizeCache.jest.js | 97 ---- source/CellMeasurer/defaultCellSizeCache.js | 64 --- source/CellMeasurer/idCellSizeCache.jest.js | 68 --- source/CellMeasurer/idCellSizeCache.js | 72 --- source/CellMeasurer/index.js | 3 +- source/index.js | 4 +- 11 files changed, 369 insertions(+), 1058 deletions(-) create mode 100644 source/CellMeasurer/CellMeasurerCache.jest.js create mode 100644 source/CellMeasurer/CellMeasurerCache.js delete mode 100644 source/CellMeasurer/defaultCellSizeCache.jest.js delete mode 100644 source/CellMeasurer/defaultCellSizeCache.js delete mode 100644 source/CellMeasurer/idCellSizeCache.jest.js delete mode 100644 source/CellMeasurer/idCellSizeCache.js diff --git a/source/CellMeasurer/CellMeasurer.example.js b/source/CellMeasurer/CellMeasurer.example.js index 6ddd1dc90..b3d86a645 100644 --- a/source/CellMeasurer/CellMeasurer.example.js +++ b/source/CellMeasurer/CellMeasurer.example.js @@ -1,10 +1,9 @@ -/** @flow */ import Immutable from 'immutable' import React, { Component, PropTypes } from 'react' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' import AutoSizer from '../AutoSizer' import CellMeasurer from './CellMeasurer' -import CellSizeCache from './defaultCellSizeCache' +import CellMeasurerCache from './CellMeasurerCache' import Grid from '../Grid' import shallowCompare from 'react-addons-shallow-compare' import cn from 'classnames' @@ -14,6 +13,8 @@ const COLUMN_WIDTH = 150 const ROW_COUNT = 50 const ROW_HEIGHT = 35 +const cache = new CellMeasurerCache() + export default class CellMeasurerExample extends Component { static contextTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired @@ -22,104 +23,27 @@ export default class CellMeasurerExample extends Component { constructor (props, context) { super(props, context) - this._uniformSizeCellSizeCache = new CellSizeCache({ - uniformColumnWidth: true, - uniformRowHeight: true - }) - - this._cellRenderer = this._cellRenderer.bind(this) - this._uniformCellRenderer = this._uniformCellRenderer.bind(this) + this._cellRendererOne = this._cellRendererOne.bind(this) } render () { return ( - - - - - This component renders content for a given column or row in order to determine the widest or tallest cell. - It can be used to just-in-time measure dynamic content (eg. messages in a chat interface). - - - - {({ width }) => ( -
-

Fixed height, dynamic width

- - {({ getColumnWidth }) => ( - - )} - - -

Fixed width, dynamic height

- - {({ getRowHeight }) => ( - - )} - - -

Uniform width and height

- - {({ getColumnWidth, getRowHeight }) => ( - - )} - -
- )} -
-
+ + {({ width }) => ( +
} + rowCount={ROW_COUNT} + rowHeight={ROW_HEIGHT} + width={width} + /> + )} + ) } @@ -127,57 +51,22 @@ export default class CellMeasurerExample extends Component { return shallowCompare(this, nextProps, nextState) } - _cellRenderer ({ columnIndex, key, rowIndex, style }) { - const datum = this._getDatum(rowIndex) - const rowClass = this._getRowClassName(rowIndex) + _cellRendererOne ({ columnIndex, key, rowIndex, style }) { const classNames = cn(rowClass, styles.cell, { [styles.centeredCell]: columnIndex > 2 }) - let content - - switch (columnIndex % 3) { - case 0: - content = datum.color - break - case 1: - content = datum.name - break - case 2: - content = datum.random - break - } - - return ( -
- {content} -
- ) - } - - _getDatum (index) { - const { list } = this.context - - return list.get(index % list.size) - } - - _getRowClassName (row) { - return row % 2 === 0 ? styles.evenRow : styles.oddRow - } - - _uniformCellRenderer ({ columnIndex, key, rowIndex, style }) { return ( -
- {rowIndex}, {columnIndex} -
+
+ {rowIndex}, {columnIndex} +
+ ) } } diff --git a/source/CellMeasurer/CellMeasurer.jest.js b/source/CellMeasurer/CellMeasurer.jest.js index 6136dafd0..332219ba3 100644 --- a/source/CellMeasurer/CellMeasurer.jest.js +++ b/source/CellMeasurer/CellMeasurer.jest.js @@ -1,438 +1,140 @@ import React from 'react' +import { findDOMNode } from 'react-dom' import { render } from '../TestUtils' import CellMeasurer from './CellMeasurer' -import CellSizeCache from './defaultCellSizeCache' - -const HEIGHTS = [75, 50, 125, 100, 150] -const WIDTHS = [125, 50, 200, 175, 100] +import CellMeasurerCache from './CellMeasurerCache' // Accounts for the fact that JSDom doesn't support measurements. function mockClientWidthAndHeight ({ - cellMeasurer, height, width }) { - let clientHeightIndex = -1 - let clientWidthIndex = -1 - Object.defineProperty( - cellMeasurer._div, + Element.prototype, 'clientHeight', { configurable: true, - get: () => height || HEIGHTS[++clientHeightIndex % HEIGHTS.length] + get: jest.fn().mockReturnValue(height) } ) Object.defineProperty( - cellMeasurer._div, + Element.prototype, 'clientWidth', { configurable: true, - get: () => width || WIDTHS[++clientWidthIndex % WIDTHS.length] + get: jest.fn().mockReturnValue(width) } ) } -function createCellRenderer () { - const cellRendererParams = [] - const cellRenderer = (params) => { - cellRendererParams.push(params) - return ( -
- cell -
- ) - } - - return { - cellRenderer, - cellRendererParams - } -} - function renderHelper ({ - cellRenderer, - cellSizeCache, - columnCount = 1, - columnWidth, - rowCount = 1, - rowHeight -} = {}) { - let params + cache +}) { render( -
- - {(paramsToSave) => { - params = paramsToSave - - return
foo
- }} -
-
+ +
+ ) - - mockClientWidthAndHeight({ - cellMeasurer: params.cellMeasurer, - height: rowHeight, - width: columnWidth - }) - - return params } describe('CellMeasurer', () => { - it('should calculate the height of a single-column row', () => { - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { - getColumnWidth, - getRowHeight - } = renderHelper({ - cellRenderer, - columnWidth: 100 - }) + it('componentDidMount() should measure content that is not already in the cache', () => { + const cache = new CellMeasurerCache() - expect(cellRendererParams).toEqual([]) - expect(getRowHeight({ index: 0 })).toEqual(75) - expect(cellRendererParams).toEqual([{ columnIndex: 0, index: 0, rowIndex: 0 }]) - expect(getColumnWidth({ index: 0 })).toEqual(100) - - // For some reason this explicit unmount is necessary. - // Without it, Jasmine's :afterEach doesn't pick up and unmount the component correctly. - render.unmount() - }) - - it('should calculate the width of a single-row column', () => { - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { - getColumnWidth, - getRowHeight - } = renderHelper({ - cellRenderer, - rowHeight: 50 + mockClientWidthAndHeight({ + height: 20, + width: 100 }) - expect(cellRendererParams).toEqual([]) - expect(getColumnWidth({ index: 0 })).toEqual(125) - expect(cellRendererParams).toEqual([{ columnIndex: 0, index: 0, rowIndex: 0 }]) - expect(getRowHeight({ index: 0 })).toEqual(50) - }) + const clientHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientHeight').get + const clientWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientWidth').get - it('should calculate the height of a multi-column row based on the tallest column-cell', () => { - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { - getColumnWidth, - getRowHeight - } = renderHelper({ - cellRenderer, - columnCount: 5, - columnWidth: 100 - }) + expect(clientHeightMock.mock.calls).toHaveLength(0) + expect(clientWidthMock.mock.calls).toHaveLength(0) + expect(cache.has(0, 0)).toBe(false) - expect(cellRendererParams.length).toEqual(0) - expect(getRowHeight({ index: 0 })).toEqual(150) - expect(cellRendererParams.length).toEqual(5) - expect(getColumnWidth({ index: 0 })).toEqual(100) - }) + renderHelper({ cache }) - it('should calculate the width of a multi-row column based on the widest row-cell', () => { - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { - getColumnWidth, - getRowHeight - } = renderHelper({ - cellRenderer, - rowCount: 5, - rowHeight: 50 - }) - - expect(cellRendererParams.length).toEqual(0) - expect(getColumnWidth({ index: 0 })).toEqual(200) - expect(cellRendererParams.length).toEqual(5) - expect(getRowHeight({ index: 0 })).toEqual(50) + expect(clientHeightMock.mock.calls).toHaveLength(1) + expect(clientWidthMock.mock.calls).toHaveLength(1) + expect(cache.has(0, 0)).toBe(true) + expect(cache.getWidth(0, 0)).toBe(100) + expect(cache.getHeight(0, 0)).toBe(20) }) - it('should support :rowRenderer via :index param for easier List integration', () => { - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { getColumnWidth } = renderHelper({ - cellRenderer, - rowCount: 5, - rowHeight: 50 - }) - getColumnWidth({ index: 0 }) - expect(cellRendererParams.length).toEqual(5) - for (let i = 0; i < 5; i++) { - expect(cellRendererParams[i].index).toEqual(i) - } - }) + it('componentDidMount() should not measure content that is already in the cache', () => { + const cache = new CellMeasurerCache() + cache.set(0, 0, 100, 20) - it('should cache cell measurements once a cell has been rendered', () => { - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { - getRowHeight - } = renderHelper({ cellRenderer }) - - expect(cellRendererParams).toEqual([]) - getRowHeight({ index: 0 }) - getRowHeight({ index: 1 }) - expect(cellRendererParams).toEqual([ - { columnIndex: 0, index: 0, rowIndex: 0 }, - { columnIndex: 0, index: 1, rowIndex: 1 } - ]) - - getRowHeight({ index: 0 }) - getRowHeight({ index: 1 }) - expect(cellRendererParams).toEqual([ - { columnIndex: 0, index: 0, rowIndex: 0 }, - { columnIndex: 0, index: 1, rowIndex: 1 } - ]) - }) - - it('should reset all cached measurements when resetMeasurements() is called', () => { - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { - getRowHeight, - resetMeasurements - } = renderHelper({ cellRenderer }) - - expect(cellRendererParams).toEqual([]) - getRowHeight({ index: 0 }) - getRowHeight({ index: 1 }) - expect(cellRendererParams).toEqual([ - { columnIndex: 0, index: 0, rowIndex: 0 }, - { columnIndex: 0, index: 1, rowIndex: 1 } - ]) - - resetMeasurements() - - getRowHeight({ index: 0 }) - getRowHeight({ index: 1 }) - expect(cellRendererParams).toEqual([ - { columnIndex: 0, index: 0, rowIndex: 0 }, - { columnIndex: 0, index: 1, rowIndex: 1 }, - { columnIndex: 0, index: 0, rowIndex: 0 }, - { columnIndex: 0, index: 1, rowIndex: 1 } - ]) - }) + mockClientWidthAndHeight({ + height: 20, + width: 100 + }) - it('should reset a specific cached row measurement when resetMeasurementForColumn() is called', () => { - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { - getColumnWidth, - resetMeasurementForColumn - } = renderHelper({ cellRenderer }) + expect(cache.has(0, 0)).toBe(true) - expect(cellRendererParams).toEqual([]) - getColumnWidth({ index: 0 }) - getColumnWidth({ index: 1 }) - expect(cellRendererParams).toEqual([ - { columnIndex: 0, index: 0, rowIndex: 0 }, - { columnIndex: 1, index: 0, rowIndex: 0 } - ]) + renderHelper({ cache }) - resetMeasurementForColumn(0) + const clientHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientHeight').get + const clientWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientWidth').get - getColumnWidth({ index: 0 }) - getColumnWidth({ index: 1 }) - expect(cellRendererParams).toEqual([ - { columnIndex: 0, index: 0, rowIndex: 0 }, - { columnIndex: 1, index: 0, rowIndex: 0 }, - { columnIndex: 0, index: 0, rowIndex: 0 } - ]) + expect(clientHeightMock.mock.calls).toHaveLength(0) + expect(clientWidthMock.mock.calls).toHaveLength(0) }) - it('should reset a specific cached row measurement when resetMeasurementForRow() is called', () => { - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { - getRowHeight, - resetMeasurementForRow - } = renderHelper({ cellRenderer }) + it('componentDidUpdate() should measure content that is not already in the cache', () => { + const cache = new CellMeasurerCache() - expect(cellRendererParams).toEqual([]) - getRowHeight({ index: 0 }) - getRowHeight({ index: 1 }) - expect(cellRendererParams).toEqual([ - { columnIndex: 0, index: 0, rowIndex: 0 }, - { columnIndex: 0, index: 1, rowIndex: 1 } - ]) + renderHelper({ cache }) - resetMeasurementForRow(0) + cache.clear(0, 0) - getRowHeight({ index: 0 }) - getRowHeight({ index: 1 }) - expect(cellRendererParams).toEqual([ - { columnIndex: 0, index: 0, rowIndex: 0 }, - { columnIndex: 0, index: 1, rowIndex: 1 }, - { columnIndex: 0, index: 0, rowIndex: 0 } - ]) - }) + expect(cache.has(0, 0)).toBe(false) + expect(cache.getWidth(0, 0)).toBe(undefined) + expect(cache.getHeight(0, 0)).toBe(undefined) - it('should allow a custom caching strategy to be specified', () => { - const customCellSizeCache = new CellSizeCache() - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { - getColumnWidth, - getRowHeight - } = renderHelper({ - cellRenderer, - cellSizeCache: customCellSizeCache, - columnCount: 5, - columnWidth: 200, - rowCount: 2, - rowHeight: 50 + mockClientWidthAndHeight({ + height: 20, + width: 100 }) - expect(customCellSizeCache.getColumnWidth(0)).toEqual(undefined) - expect(cellRendererParams.length).toEqual(0) - expect(getColumnWidth({ index: 0 })).toEqual(200) - expect(customCellSizeCache.getColumnWidth(0)).toEqual(200) - expect(cellRendererParams.length).toEqual(2) - expect(getColumnWidth({ index: 0 })).toEqual(200) - expect(cellRendererParams.length).toEqual(2) + const clientHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientHeight').get + const clientWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientWidth').get - expect(customCellSizeCache.getRowHeight(0)).toEqual(undefined) - expect(cellRendererParams.length).toEqual(2) - expect(getRowHeight({ index: 0 })).toEqual(50) - expect(customCellSizeCache.getRowHeight(0)).toEqual(50) - expect(cellRendererParams.length).toEqual(7) - expect(getRowHeight({ index: 0 })).toEqual(50) - expect(cellRendererParams.length).toEqual(7) - }) + renderHelper({ cache }) - it('should support changing the custom caching strategy after initialization', () => { - const customCellSizeCacheA = new CellSizeCache() - const customCellSizeCacheB = new CellSizeCache() - const { cellRenderer } = createCellRenderer() - const { getColumnWidth: getColumnWidthA } = renderHelper({ - cellRenderer, - cellSizeCache: customCellSizeCacheA, - columnCount: 5, - columnWidth: 200 - }) - expect(customCellSizeCacheA.getColumnWidth(0)).toEqual(undefined) - expect(getColumnWidthA({ index: 0 })).toEqual(200) - expect(customCellSizeCacheA.getColumnWidth(0)).toEqual(200) - - const { getColumnWidth: getColumnWidthB } = renderHelper({ - cellRenderer, - cellSizeCache: customCellSizeCacheA, - columnCount: 5, - columnWidth: 100 - }) - expect(customCellSizeCacheA.getColumnWidth(0)).toEqual(200) - expect(getColumnWidthB({ index: 0 })).toEqual(200) - expect(customCellSizeCacheA.getColumnWidth(0)).toEqual(200) + expect(cache.has(0, 0)).toBe(true) - const { getColumnWidth: getColumnWidthC } = renderHelper({ - cellRenderer, - cellSizeCache: customCellSizeCacheB, - columnCount: 5, - columnWidth: 50 - }) - expect(customCellSizeCacheB.getColumnWidth(0)).toEqual(undefined) - expect(getColumnWidthC({ index: 0 })).toEqual(50) - expect(customCellSizeCacheB.getColumnWidth(0)).toEqual(50) + expect(clientHeightMock.mock.calls).toHaveLength(1) + expect(clientWidthMock.mock.calls).toHaveLength(1) + expect(cache.getWidth(0, 0)).toBe(100) + expect(cache.getHeight(0, 0)).toBe(20) }) - it('should calculate row height just once when using the alternative uniform-size cell size cache', () => { - const cellSizeCache = new CellSizeCache({ - uniformRowHeight: true - }) - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { - getRowHeight - } = renderHelper({ - cellRenderer, - cellSizeCache, - rowCount: 5 - }) - - expect(cellRendererParams).toEqual([]) - const height1 = getRowHeight({ index: 0 }) - const height2 = getRowHeight({ index: 1 }) - const height3 = getRowHeight({ index: 0 }) - expect(cellRendererParams).toEqual([ - { columnIndex: 0, index: 0, rowIndex: 0 } - ]) + it('componentDidUpdate() should not measure content that is already in the cache', () => { + const cache = new CellMeasurerCache() + cache.set(0, 0, 100, 20) - const expectedHeight = HEIGHTS[0] + expect(cache.has(0, 0)).toBe(true) - expect(height1).toEqual(expectedHeight) - expect(height2).toEqual(expectedHeight) - expect(height3).toEqual(expectedHeight) - }) - - it('should calculate column-width just once when using the alternative uniform-size cell size cache', () => { - const cellSizeCache = new CellSizeCache({ - uniformColumnWidth: true - }) - const { - cellRenderer, - cellRendererParams - } = createCellRenderer() - const { - getColumnWidth - } = renderHelper({ - cellRenderer, - cellSizeCache, - columnCount: 5 + mockClientWidthAndHeight({ + height: 20, + width: 100 }) - expect(cellRendererParams).toEqual([]) - const width1 = getColumnWidth({ index: 0 }) - const width2 = getColumnWidth({ index: 1 }) - const width3 = getColumnWidth({ index: 0 }) - expect(cellRendererParams).toEqual([ - { columnIndex: 0, index: 0, rowIndex: 0 } - ]) + renderHelper({ cache }) + renderHelper({ cache }) - const expectedWidth = WIDTHS[0] + const clientHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientHeight').get + const clientWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientWidth').get - expect(width1).toEqual(expectedWidth) - expect(width2).toEqual(expectedWidth) - expect(width3).toEqual(expectedWidth) + expect(clientHeightMock.mock.calls).toHaveLength(0) + expect(clientWidthMock.mock.calls).toHaveLength(0) }) }) diff --git a/source/CellMeasurer/CellMeasurer.js b/source/CellMeasurer/CellMeasurer.js index c5195a87e..b68748121 100644 --- a/source/CellMeasurer/CellMeasurer.js +++ b/source/CellMeasurer/CellMeasurer.js @@ -1,260 +1,52 @@ /** @flow */ import React, { Component, PropTypes } from 'react' import shallowCompare from 'react-addons-shallow-compare' -import ReactDOM from 'react-dom' -import CellSizeCache from './defaultCellSizeCache' +import { findDOMNode } from 'react-dom' +import type { CellMeasurerCacheType } from './CellMeasurerCache' + +type Props = { + cache: any, // TODO type CellMeasurerCacheType + children: mixed, + columnIndex: number, + rowIndex: number +}; /** - * Measures a Grid cell's contents by rendering them in a way that is not visible to the user. - * Either a fixed width or height may be provided if it is desirable to measure only in one direction. + * Wraps a cell and measures its rendered content. + * Measurements are stored in a per-cell cache. + * Cached-content is not be re-measured. */ export default class CellMeasurer extends Component { - static propTypes = { - /** - * Renders a cell given its indices. - * Should implement the following interface: ({ columnIndex: number, rowIndex: number }): PropTypes.node - */ - cellRenderer: PropTypes.func.isRequired, - - /** - * Optional, custom caching strategy for cell sizes. - */ - cellSizeCache: PropTypes.object, - - /** - * Function responsible for rendering a virtualized component. - * This function should implement the following signature: - * ({ getColumnWidth, getRowHeight, resetMeasurements }) => PropTypes.element - */ - children: PropTypes.func.isRequired, - - /** - * Number of columns in grid. - */ - columnCount: PropTypes.number.isRequired, - - /** - * A Node, Component instance, or function that returns either. - * If this property is not specified the document body will be used. - */ - container: React.PropTypes.oneOfType([ - React.PropTypes.func, - React.PropTypes.node - ]), - - /** - * Assign a fixed :height in order to measure dynamic text :width only. - */ - height: PropTypes.number, - - /** - * Number of rows in grid. - */ - rowCount: PropTypes.number.isRequired, - - /** - * Assign a fixed :width in order to measure dynamic text :height only. - */ - width: PropTypes.number - }; - - constructor (props, state) { + constructor (props : Props, state) { super(props, state) - - this._cellSizeCache = props.cellSizeCache || new CellSizeCache() - - this.getColumnWidth = this.getColumnWidth.bind(this) - this.getRowHeight = this.getRowHeight.bind(this) - this.resetMeasurements = this.resetMeasurements.bind(this) - this.resetMeasurementForColumn = this.resetMeasurementForColumn.bind(this) - this.resetMeasurementForRow = this.resetMeasurementForRow.bind(this) - } - - getColumnWidth ({ index }) { - const columnWidth = this._cellSizeCache.getColumnWidth(index) - if (columnWidth != null) { - return columnWidth - } - - const { rowCount } = this.props - - let maxWidth = 0 - - for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { - let { width } = this._measureCell({ - clientWidth: true, - columnIndex: index, - rowIndex - }) - - maxWidth = Math.max(maxWidth, width) - } - - this._cellSizeCache.setColumnWidth(index, maxWidth) - - return maxWidth - } - - getRowHeight ({ index }) { - const rowHeight = this._cellSizeCache.getRowHeight(index) - if (rowHeight != null) { - return rowHeight - } - - const { columnCount } = this.props - - let maxHeight = 0 - - for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) { - let { height } = this._measureCell({ - clientHeight: true, - columnIndex, - rowIndex: index - }) - - maxHeight = Math.max(maxHeight, height) - } - - this._cellSizeCache.setRowHeight(index, maxHeight) - - return maxHeight } - resetMeasurementForColumn (columnIndex) { - this._cellSizeCache.clearColumnWidth(columnIndex) + componentDidMount() { + this._maybeMeasureCell() } - resetMeasurementForRow (rowIndex) { - this._cellSizeCache.clearRowHeight(rowIndex) + componentDidUpdate(prevProps, prevState) { + this._maybeMeasureCell() } - resetMeasurements () { - this._cellSizeCache.clearAllColumnWidths() - this._cellSizeCache.clearAllRowHeights() + render() { + return this.props.children; } - componentDidMount () { - this._renderAndMount() - } - - componentWillReceiveProps (nextProps) { - const { cellSizeCache } = this.props + _maybeMeasureCell () { + const { cache, columnIndex, rowIndex } = this.props - if (cellSizeCache !== nextProps.cellSizeCache) { - this._cellSizeCache = nextProps.cellSizeCache - } + if (!cache.has(rowIndex, columnIndex)) { + const node = findDOMNode(this) + const height = node.clientHeight + const width = node.clientWidth - this._updateDivDimensions(nextProps) - } - - componentWillUnmount () { - this._unmountContainer() - } - - render () { - const { children } = this.props - - return children({ - cellMeasurer: this, - getColumnWidth: this.getColumnWidth, - getRowHeight: this.getRowHeight, - resetMeasurements: this.resetMeasurements, - resetMeasurementForColumn: this.resetMeasurementForColumn, - resetMeasurementForRow: this.resetMeasurementForRow - }) - } - - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - - _getContainerNode (props) { - const { container } = props - - if (container) { - return ReactDOM.findDOMNode( - typeof container === 'function' - ? container() - : container + cache.set( + rowIndex, + columnIndex, + width, + height ) - } else { - return document.body - } - } - - _measureCell ({ - clientHeight = false, - clientWidth = true, - columnIndex, - rowIndex - }) { - const { cellRenderer } = this.props - - const rendered = cellRenderer({ - columnIndex, - index: rowIndex, // Simplify List :rowRenderer use case - rowIndex - }) - - // Handle edge case where this method is called before the CellMeasurer has completed its initial render (and mounted). - this._renderAndMount() - - // @TODO Keep an eye on this for future React updates as the interface may change: - // https://twitter.com/soprano/status/737316379712331776 - ReactDOM.unstable_renderSubtreeIntoContainer(this, rendered, this._div) - - const measurements = { - height: clientHeight && this._div.clientHeight, - width: clientWidth && this._div.clientWidth - } - - ReactDOM.unmountComponentAtNode(this._div) - - return measurements - } - - _renderAndMount () { - if (!this._div) { - this._div = document.createElement('div') - this._div.style.display = 'inline-block' - this._div.style.position = 'absolute' - this._div.style.visibility = 'hidden' - this._div.style.zIndex = -1 - - this._updateDivDimensions(this.props) - - this._containerNode = this._getContainerNode(this.props) - this._containerNode.appendChild(this._div) - } - } - - _unmountContainer () { - if (this._div) { - this._containerNode.removeChild(this._div) - - this._div = null - } - - this._containerNode = null - } - - _updateDivDimensions (props) { - const { height, width } = props - - if ( - height && - height !== this._divHeight - ) { - this._divHeight = height - this._div.style.height = `${height}px` - } - - if ( - width && - width !== this._divWidth - ) { - this._divWidth = width - this._div.style.width = `${width}px` } } } diff --git a/source/CellMeasurer/CellMeasurerCache.jest.js b/source/CellMeasurer/CellMeasurerCache.jest.js new file mode 100644 index 000000000..fd3abdcc3 --- /dev/null +++ b/source/CellMeasurer/CellMeasurerCache.jest.js @@ -0,0 +1,76 @@ +import CellMeasurerCache from './CellMeasurerCache' + +describe('CellMeasurerCache', () => { + it('should correctly report cache status', () => { + const cache = new CellMeasurerCache() + expect(cache.has(0, 0)).toBe(false) + }) + + it('should cache cells', () => { + const cache = new CellMeasurerCache() + cache.set(0, 0, 100, 20) + expect(cache.has(0, 0)).toBe(true) + }) + + it('should return the correct default sizes for uncached cells if specified', () => { + const cache = new CellMeasurerCache({ + defaultHeight: 20, + defaultWidth: 100 + }) + expect(cache.getWidth(0, 0)).toBe(100) + expect(cache.getHeight(0, 0)).toBe(20) + cache.set(0, 0, 150, 30) + expect(cache.getWidth(0, 0)).toBe(150) + expect(cache.getHeight(0, 0)).toBe(30) + }) + + it('should clear a single cached cell', () => { + const cache = new CellMeasurerCache() + cache.set(0, 0, 100, 20) + cache.set(1, 0, 100, 20) + expect(cache.has(0, 0)).toBe(true) + expect(cache.has(1, 0)).toBe(true) + cache.clear(0, 0) + expect(cache.has(0, 0)).toBe(false) + expect(cache.has(1, 0)).toBe(true) + }) + + it('should clear all cached cells', () => { + const cache = new CellMeasurerCache() + cache.set(0, 0, 100, 20) + cache.set(1, 0, 100, 20) + expect(cache.has(0, 0)).toBe(true) + expect(cache.has(1, 0)).toBe(true) + cache.clearAll() + expect(cache.has(0, 0)).toBe(false) + expect(cache.has(1, 0)).toBe(false) + }) + + it('should support a custom :keyMapper', () => { + const keyMapper = jest.fn() + const cache = new CellMeasurerCache({ keyMapper }) + keyMapper.mockReturnValueOnce('a') + cache.set(0, 0, 100, 20) + keyMapper.mockReturnValueOnce('a') + expect(cache.has(0, 0)).toBe(true) + keyMapper.mockReturnValueOnce('b') + expect(cache.has(0, 0)).toBe(false) + expect(keyMapper.mock.calls).toHaveLength(3) + }) + + it('should provide a Grid-compatible :columnWidth method', () => { + const cache = new CellMeasurerCache() + expect(cache.columnWidth({ index: 0 })).toBe(undefined) + cache.set(0, 0, 100, 50) + expect(cache.columnWidth({ index: 0 })).toBe(100) + expect(cache.columnWidth({ index: 1 })).toBe(undefined) + }) + + it('should provide a Grid-compatible :rowHeight method', () => { + const cache = new CellMeasurerCache() + expect(cache.rowHeight({ index: 0 })).toBe(undefined) + cache.set(0, 0, 100, 50) + expect(cache.rowHeight({ index: 0 })).toBe(50) + expect(cache.rowHeight({ index: 1 })).toBe(undefined) + }) +}) diff --git a/source/CellMeasurer/CellMeasurerCache.js b/source/CellMeasurer/CellMeasurerCache.js new file mode 100644 index 000000000..5e224f0e9 --- /dev/null +++ b/source/CellMeasurer/CellMeasurerCache.js @@ -0,0 +1,156 @@ +/** @flow */ + +/** + * Enables more intelligent mapping of a given column and row index to an item ID. + * This prevents a cell cache from being invalidated when its parent collection is modified. + */ +type KeyMapper = ( + rowIndex: number, + columnIndex: number +) => any; + +type CellMeasurerCacheParams = { + defaultHeight ?: number, + defaultWidth ?: number, + keyMapper ?: KeyMapper +}; + +type Cache = { + [key: any]: number +}; + +type IndexParam = { + index: number +}; + +export interface CellMeasurerCacheType { + clear ( + rowIndex: number, + columnIndex: number + ) : void; + + clearAll () : void; + + columnWidth ( + index : number + ) : ?number; + + getHeight ( + rowIndex: number, + columnIndex: number + ) : ?number; + + getWidth ( + rowIndex: number, + columnIndex: number + ) : ?number; + + has ( + rowIndex: number, + columnIndex: number + ) : boolean; + + rowHeight ( + index : number + ) : ?number; + + set ( + rowIndex: number, + columnIndex: number, + width: number, + height: number + ) : void; +}; + +/** + * Caches measurements for a given cell. + */ +export default class CellMeasurerCache { + _defaultHeight: ?number; + _defaultWidth: ?number; + _keyMapper: KeyMapper; + _heightCache: Cache; + _widthCache: Cache; + + constructor (params : CellMeasurerCacheParams = {}) { + this._defaultHeight = params.defaultHeight + this._defaultWidth = params.defaultWidth + this._keyMapper = params.keyMapper || defaultKeyMapper + + this._heightCache = {} + this._widthCache = {} + } + + clear ( + rowIndex: number, + columnIndex: number + ) : void { + const key = this._keyMapper(rowIndex, columnIndex) + + delete this._heightCache[key] + delete this._widthCache[key] + } + + clearAll() : void { + this._heightCache = {} + this._widthCache = {} + } + + columnWidth = ({ index } : IndexParam) => { + return this.getWidth(0, index) + } + + getHeight ( + rowIndex: number, + columnIndex: number + ) : ?number { + const key = this._keyMapper(rowIndex, columnIndex) + + return this._heightCache.hasOwnProperty(key) + ? this._heightCache[key] + : this._defaultHeight + } + + getWidth ( + rowIndex: number, + columnIndex: number + ) : ?number { + const key = this._keyMapper(rowIndex, columnIndex) + + return this._widthCache.hasOwnProperty(key) + ? this._widthCache[key] + : this._defaultWidth + } + + has ( + rowIndex: number, + columnIndex: number + ) : boolean { + const key = this._keyMapper(rowIndex, columnIndex) + + return this._heightCache.hasOwnProperty(key) + } + + rowHeight = ({ index } : IndexParam) => { + return this.getHeight(index, 0) + } + + set ( + rowIndex: number, + columnIndex: number, + width: number, + height: number + ) : void { + const key = this._keyMapper(rowIndex, columnIndex) + + this._heightCache[key] = height + this._widthCache[key] = width + } +} + +function defaultKeyMapper ( + rowIndex: number, + columnIndex: number +): any { + return `${rowIndex}-${columnIndex}` +} diff --git a/source/CellMeasurer/defaultCellSizeCache.jest.js b/source/CellMeasurer/defaultCellSizeCache.jest.js deleted file mode 100644 index 820df074a..000000000 --- a/source/CellMeasurer/defaultCellSizeCache.jest.js +++ /dev/null @@ -1,97 +0,0 @@ -import CellSizeCache from './defaultCellSizeCache' - -describe('CellSizeCache', () => { - function verifyUniformColumnWidths (cellSizeCache) { - cellSizeCache.setColumnWidth(1, 1) - expect(cellSizeCache.getColumnWidth(1)).toEqual(1) - expect(cellSizeCache.getColumnWidth(2)).toEqual(1) - cellSizeCache.setColumnWidth(2, 2) - expect(cellSizeCache.getColumnWidth(1)).toEqual(2) - expect(cellSizeCache.getColumnWidth(2)).toEqual(2) - expect(cellSizeCache.getColumnWidth(3)).toEqual(2) - cellSizeCache.clearColumnWidth(1) - expect(cellSizeCache.getColumnWidth(1)).toEqual(undefined) - expect(cellSizeCache.getColumnWidth(2)).toEqual(undefined) - } - - function verifyUniformRowHeights (cellSizeCache) { - cellSizeCache.setRowHeight(1, 1) - expect(cellSizeCache.getRowHeight(1)).toEqual(1) - expect(cellSizeCache.getRowHeight(2)).toEqual(1) - cellSizeCache.setRowHeight(2, 2) - expect(cellSizeCache.getRowHeight(1)).toEqual(2) - expect(cellSizeCache.getRowHeight(2)).toEqual(2) - expect(cellSizeCache.getRowHeight(3)).toEqual(2) - cellSizeCache.clearRowHeight(1) - expect(cellSizeCache.getRowHeight(1)).toEqual(undefined) - expect(cellSizeCache.getRowHeight(2)).toEqual(undefined) - } - - function verifyVaryingColumnWidths (cellSizeCache) { - cellSizeCache.setColumnWidth(1, 1) - expect(cellSizeCache.getColumnWidth(1)).toEqual(1) - expect(cellSizeCache.getColumnWidth(2)).toEqual(undefined) - cellSizeCache.setColumnWidth(2, 2) - expect(cellSizeCache.getColumnWidth(1)).toEqual(1) - expect(cellSizeCache.getColumnWidth(2)).toEqual(2) - expect(cellSizeCache.getColumnWidth(3)).toEqual(undefined) - cellSizeCache.clearColumnWidth(1) - expect(cellSizeCache.getColumnWidth(1)).toEqual(undefined) - expect(cellSizeCache.getColumnWidth(2)).toEqual(2) - cellSizeCache.clearAllColumnWidths() - expect(cellSizeCache.getColumnWidth(1)).toEqual(undefined) - expect(cellSizeCache.getColumnWidth(2)).toEqual(undefined) - } - - function verifyVaryingRowHeights (cellSizeCache) { - cellSizeCache.setRowHeight(1, 1) - expect(cellSizeCache.getRowHeight(1)).toEqual(1) - expect(cellSizeCache.getRowHeight(2)).toEqual(undefined) - cellSizeCache.setRowHeight(2, 2) - expect(cellSizeCache.getRowHeight(1)).toEqual(1) - expect(cellSizeCache.getRowHeight(2)).toEqual(2) - expect(cellSizeCache.getRowHeight(3)).toEqual(undefined) - cellSizeCache.clearRowHeight(1) - expect(cellSizeCache.getRowHeight(1)).toEqual(undefined) - expect(cellSizeCache.getRowHeight(2)).toEqual(2) - cellSizeCache.clearAllRowHeights() - expect(cellSizeCache.getRowHeight(1)).toEqual(undefined) - expect(cellSizeCache.getRowHeight(2)).toEqual(undefined) - } - - it('should support uniform height and width', () => { - const cellSizeCache = new CellSizeCache({ - uniformColumnWidth: true, - uniformRowHeight: true - }) - verifyUniformColumnWidths(cellSizeCache) - verifyUniformRowHeights(cellSizeCache) - }) - - it('should support uniform height only', () => { - const cellSizeCache = new CellSizeCache({ - uniformColumnWidth: false, - uniformRowHeight: true - }) - verifyVaryingColumnWidths(cellSizeCache) - verifyUniformRowHeights(cellSizeCache) - }) - - it('should support uniform width only', () => { - const cellSizeCache = new CellSizeCache({ - uniformColumnWidth: true, - uniformRowHeight: false - }) - verifyUniformColumnWidths(cellSizeCache) - verifyVaryingRowHeights(cellSizeCache) - }) - - it('should support varying height and width', () => { - const cellSizeCache = new CellSizeCache({ - uniformColumnWidth: false, - uniformRowHeight: false - }) - verifyVaryingColumnWidths(cellSizeCache) - verifyVaryingRowHeights(cellSizeCache) - }) -}) diff --git a/source/CellMeasurer/defaultCellSizeCache.js b/source/CellMeasurer/defaultCellSizeCache.js deleted file mode 100644 index 3fc05d60a..000000000 --- a/source/CellMeasurer/defaultCellSizeCache.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Default CellMeasurer `cellSizeCache` implementation. - * Permanently caches all cell sizes (identified by column and row index) unless explicitly cleared. - * Can be configured to handle uniform cell widths and/or heights as a way of optimizing certain use cases. - */ -export default class DefaultCellSizeCache { - constructor ({ - uniformRowHeight = false, - uniformColumnWidth = false - } = {}) { - this._uniformRowHeight = uniformRowHeight - this._uniformColumnWidth = uniformColumnWidth - - this._cachedColumnWidth = undefined - this._cachedRowHeight = undefined - - this._cachedColumnWidths = {} - this._cachedRowHeights = {} - } - - clearAllColumnWidths () { - this._cachedColumnWidth = undefined - this._cachedColumnWidths = {} - } - - clearAllRowHeights () { - this._cachedRowHeight = undefined - this._cachedRowHeights = {} - } - - clearColumnWidth (index: any) { - this._cachedColumnWidth = undefined - - delete this._cachedColumnWidths[index] - } - - clearRowHeight (index: any) { - this._cachedRowHeight = undefined - - delete this._cachedRowHeights[index] - } - - getColumnWidth (index: any): ?number { - return this._uniformColumnWidth - ? this._cachedColumnWidth - : this._cachedColumnWidths[index] - } - - getRowHeight (index: any): ?number { - return this._uniformRowHeight - ? this._cachedRowHeight - : this._cachedRowHeights[index] - } - - setColumnWidth (index: any, width: number) { - this._cachedColumnWidth = width - this._cachedColumnWidths[index] = width - } - - setRowHeight (index: any, height: number) { - this._cachedRowHeight = height - this._cachedRowHeights[index] = height - } -} diff --git a/source/CellMeasurer/idCellSizeCache.jest.js b/source/CellMeasurer/idCellSizeCache.jest.js deleted file mode 100644 index 04846c1c8..000000000 --- a/source/CellMeasurer/idCellSizeCache.jest.js +++ /dev/null @@ -1,68 +0,0 @@ -import idCellSizeCache from './idCellSizeCache' - -describe('idCellSizeCache', () => { - it('should track width and height using id instead of index', () => { - const indexToIdMap = { - 0: 'foo', - 1: 'bar', - 2: 'baz' - } - const indexToIdMapCalls = [] - const cache = idCellSizeCache({ - indexToIdMap: (index) => { - indexToIdMapCalls.push(index) - return indexToIdMap[index] - } - }) - - // Set values with initial indices - cache.setColumnWidth(0, 100) - cache.setRowHeight(0, 25) - cache.setColumnWidth(1, 200) - cache.setRowHeight(1, 50) - cache.setColumnWidth(2, 300) - cache.setRowHeight(2, 75) - - // Mimic a sort operation - indexToIdMap[0] = 'baz' - indexToIdMap[1] = 'bar' - indexToIdMap[2] = 'foo' - - // Previous widths and heights should still be available - expect(cache.getColumnWidth(0)).toBe(300) - expect(cache.getRowHeight(0)).toBe(75) - expect(cache.getColumnWidth(1)).toBe(200) - expect(cache.getRowHeight(1)).toBe(50) - expect(cache.getColumnWidth(2)).toBe(100) - expect(cache.getRowHeight(2)).toBe(25) - - // Clear a specific item - cache.clearColumnWidth(0) - cache.clearRowHeight(0) - - // Mimic a sort operation - indexToIdMap[0] = 'bar' - indexToIdMap[1] = 'foo' - indexToIdMap[2] = 'baz' - - // Previous widths and heights should still be available - expect(cache.getColumnWidth(0)).toBe(200) - expect(cache.getRowHeight(0)).toBe(50) - expect(cache.getColumnWidth(1)).toBe(100) - expect(cache.getRowHeight(1)).toBe(25) - expect(cache.getColumnWidth(2)).toBeUndefined() - expect(cache.getRowHeight(2)).toBeUndefined() - - // Clear all items - cache.clearAllColumnWidths() - cache.clearAllRowHeights() - - // No sizes should be available - expect(cache.getColumnWidth(0)).toBeUndefined() - expect(cache.getRowHeight(0)).toBeUndefined() - expect(cache.getColumnWidth(1)).toBeUndefined() - expect(cache.getRowHeight(1)).toBeUndefined() - expect(cache.getColumnWidth(2)).toBeUndefined() - expect(cache.getRowHeight(2)).toBeUndefined() - }) -}) diff --git a/source/CellMeasurer/idCellSizeCache.js b/source/CellMeasurer/idCellSizeCache.js deleted file mode 100644 index d6f6e59f0..000000000 --- a/source/CellMeasurer/idCellSizeCache.js +++ /dev/null @@ -1,72 +0,0 @@ -/** @flow */ -import DefaultCellSizeCache from './defaultCellSizeCache' - -type IdCellSizeCacheConstructorParams = { - indexToIdMap : Function, - uniformRowHeight ?: boolean, - uniformColumnWidth ?: boolean -}; - -/** - * Alternate CellMeasurer `cellSizeCache` implementation. - * Similar to `defaultCellSizeCache` except that sizes are tied to data id rather than index. - * Requires an index-to-id map function (passed in externally) to operate. - */ -export default function idCellSizeCache ({ - indexToIdMap, - uniformColumnWidth = false, - uniformRowHeight = false -} : IdCellSizeCacheConstructorParams) { - const cellSizeCache = new DefaultCellSizeCache({ - uniformColumnWidth, - uniformRowHeight - }) - - return { - clearAllColumnWidths () { - cellSizeCache.clearAllColumnWidths() - }, - - clearAllRowHeights () { - cellSizeCache.clearAllRowHeights() - }, - - clearColumnWidth (index: number) { - cellSizeCache.clearColumnWidth( - indexToIdMap(index) - ) - }, - - clearRowHeight (index: number) { - cellSizeCache.clearRowHeight( - indexToIdMap(index) - ) - }, - - getColumnWidth (index: number): ?number { - return cellSizeCache.getColumnWidth( - indexToIdMap(index) - ) - }, - - getRowHeight (index: number): ?number { - return cellSizeCache.getRowHeight( - indexToIdMap(index) - ) - }, - - setColumnWidth (index: number, width: number) { - cellSizeCache.setColumnWidth( - indexToIdMap(index), - width - ) - }, - - setRowHeight (index: number, height: number) { - cellSizeCache.setRowHeight( - indexToIdMap(index), - height - ) - } - } -} diff --git a/source/CellMeasurer/index.js b/source/CellMeasurer/index.js index f2ba0dc78..33516996e 100644 --- a/source/CellMeasurer/index.js +++ b/source/CellMeasurer/index.js @@ -1,4 +1,3 @@ export default from './CellMeasurer' export CellMeasurer from './CellMeasurer' -export defaultCellSizeCache from './defaultCellSizeCache' -export idCellSizeCache from './idCellSizeCache' +export CellMeasurerCache from './CellMeasurerCache' diff --git a/source/index.js b/source/index.js index 83e6fbc59..bd7516cc2 100644 --- a/source/index.js +++ b/source/index.js @@ -3,9 +3,7 @@ export { ArrowKeyStepper } from './ArrowKeyStepper' export { AutoSizer } from './AutoSizer' export { CellMeasurer, - defaultCellSizeCache as defaultCellMeasurerCellSizeCache, - defaultCellSizeCache as uniformSizeCellMeasurerCellSizeCache, // 7.21 backwards compatible export - idCellSizeCache as idCellMeasurerCellSizeCache + CellMeasurerCache } from './CellMeasurer' export { Collection } from './Collection' export { ColumnSizer } from './ColumnSizer' From 457c2eb1f4dbc37ce605c73607334d86ca572181 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 11 Feb 2017 11:06:39 -0800 Subject: [PATCH 04/31] Tweaked rowHeight and columnWidth handling for CellMeasurer --- source/CellMeasurer/CellMeasurer.jest.js | 44 ++++++------ source/CellMeasurer/CellMeasurer.js | 4 +- source/CellMeasurer/CellMeasurerCache.jest.js | 40 ++++++++--- source/CellMeasurer/CellMeasurerCache.js | 68 +++++++++++++------ 4 files changed, 105 insertions(+), 51 deletions(-) diff --git a/source/CellMeasurer/CellMeasurer.jest.js b/source/CellMeasurer/CellMeasurer.jest.js index 332219ba3..167957524 100644 --- a/source/CellMeasurer/CellMeasurer.jest.js +++ b/source/CellMeasurer/CellMeasurer.jest.js @@ -11,7 +11,7 @@ function mockClientWidthAndHeight ({ }) { Object.defineProperty( Element.prototype, - 'clientHeight', + 'offsetHeight', { configurable: true, get: jest.fn().mockReturnValue(height) @@ -20,7 +20,7 @@ function mockClientWidthAndHeight ({ Object.defineProperty( Element.prototype, - 'clientWidth', + 'offsetWidth', { configurable: true, get: jest.fn().mockReturnValue(width) @@ -52,17 +52,17 @@ describe('CellMeasurer', () => { width: 100 }) - const clientHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientHeight').get - const clientWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientWidth').get + const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get + const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get - expect(clientHeightMock.mock.calls).toHaveLength(0) - expect(clientWidthMock.mock.calls).toHaveLength(0) + expect(offsetHeightMock.mock.calls).toHaveLength(0) + expect(offsetWidthMock.mock.calls).toHaveLength(0) expect(cache.has(0, 0)).toBe(false) renderHelper({ cache }) - expect(clientHeightMock.mock.calls).toHaveLength(1) - expect(clientWidthMock.mock.calls).toHaveLength(1) + expect(offsetHeightMock.mock.calls).toHaveLength(1) + expect(offsetWidthMock.mock.calls).toHaveLength(1) expect(cache.has(0, 0)).toBe(true) expect(cache.getWidth(0, 0)).toBe(100) expect(cache.getHeight(0, 0)).toBe(20) @@ -81,11 +81,11 @@ describe('CellMeasurer', () => { renderHelper({ cache }) - const clientHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientHeight').get - const clientWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientWidth').get + const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get + const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get - expect(clientHeightMock.mock.calls).toHaveLength(0) - expect(clientWidthMock.mock.calls).toHaveLength(0) + expect(offsetHeightMock.mock.calls).toHaveLength(0) + expect(offsetWidthMock.mock.calls).toHaveLength(0) }) it('componentDidUpdate() should measure content that is not already in the cache', () => { @@ -96,23 +96,23 @@ describe('CellMeasurer', () => { cache.clear(0, 0) expect(cache.has(0, 0)).toBe(false) - expect(cache.getWidth(0, 0)).toBe(undefined) - expect(cache.getHeight(0, 0)).toBe(undefined) + expect(cache.getWidth(0, 0)).toBe(null) + expect(cache.getHeight(0, 0)).toBe(null) mockClientWidthAndHeight({ height: 20, width: 100 }) - const clientHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientHeight').get - const clientWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientWidth').get + const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get + const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get renderHelper({ cache }) expect(cache.has(0, 0)).toBe(true) - expect(clientHeightMock.mock.calls).toHaveLength(1) - expect(clientWidthMock.mock.calls).toHaveLength(1) + expect(offsetHeightMock.mock.calls).toHaveLength(1) + expect(offsetWidthMock.mock.calls).toHaveLength(1) expect(cache.getWidth(0, 0)).toBe(100) expect(cache.getHeight(0, 0)).toBe(20) }) @@ -131,10 +131,10 @@ describe('CellMeasurer', () => { renderHelper({ cache }) renderHelper({ cache }) - const clientHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientHeight').get - const clientWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'clientWidth').get + const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get + const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get - expect(clientHeightMock.mock.calls).toHaveLength(0) - expect(clientWidthMock.mock.calls).toHaveLength(0) + expect(offsetHeightMock.mock.calls).toHaveLength(0) + expect(offsetWidthMock.mock.calls).toHaveLength(0) }) }) diff --git a/source/CellMeasurer/CellMeasurer.js b/source/CellMeasurer/CellMeasurer.js index b68748121..1fb0bde58 100644 --- a/source/CellMeasurer/CellMeasurer.js +++ b/source/CellMeasurer/CellMeasurer.js @@ -38,8 +38,8 @@ export default class CellMeasurer extends Component { if (!cache.has(rowIndex, columnIndex)) { const node = findDOMNode(this) - const height = node.clientHeight - const width = node.clientWidth + const height = node.offsetHeight + const width = node.offsetWidth cache.set( rowIndex, diff --git a/source/CellMeasurer/CellMeasurerCache.jest.js b/source/CellMeasurer/CellMeasurerCache.jest.js index fd3abdcc3..0d5aaf594 100644 --- a/source/CellMeasurer/CellMeasurerCache.jest.js +++ b/source/CellMeasurer/CellMeasurerCache.jest.js @@ -48,29 +48,53 @@ describe('CellMeasurerCache', () => { it('should support a custom :keyMapper', () => { const keyMapper = jest.fn() + keyMapper.mockReturnValue('a') + const cache = new CellMeasurerCache({ keyMapper }) - keyMapper.mockReturnValueOnce('a') cache.set(0, 0, 100, 20) - keyMapper.mockReturnValueOnce('a') expect(cache.has(0, 0)).toBe(true) - keyMapper.mockReturnValueOnce('b') + + keyMapper.mock.calls.splice(0) + keyMapper.mockReturnValue('b') expect(cache.has(0, 0)).toBe(false) - expect(keyMapper.mock.calls).toHaveLength(3) + expect(keyMapper.mock.calls).toHaveLength(1) }) it('should provide a Grid-compatible :columnWidth method', () => { const cache = new CellMeasurerCache() - expect(cache.columnWidth({ index: 0 })).toBe(undefined) + expect(cache.columnWidth({ index: 0 })).toBe(null) cache.set(0, 0, 100, 50) expect(cache.columnWidth({ index: 0 })).toBe(100) - expect(cache.columnWidth({ index: 1 })).toBe(undefined) + expect(cache.columnWidth({ index: 1 })).toBe(null) + cache.set(1, 0, 75, 50) + expect(cache.columnWidth({ index: 0 })).toBe(100) + cache.set(2, 0, 125, 50) + expect(cache.columnWidth({ index: 0 })).toBe(125) }) it('should provide a Grid-compatible :rowHeight method', () => { const cache = new CellMeasurerCache() - expect(cache.rowHeight({ index: 0 })).toBe(undefined) + expect(cache.rowHeight({ index: 0 })).toBe(null) cache.set(0, 0, 100, 50) expect(cache.rowHeight({ index: 0 })).toBe(50) - expect(cache.rowHeight({ index: 1 })).toBe(undefined) + expect(cache.rowHeight({ index: 1 })).toBe(null) + cache.set(0, 1, 100, 25) + expect(cache.rowHeight({ index: 0 })).toBe(50) + cache.set(0, 2, 100, 75) + expect(cache.rowHeight({ index: 0 })).toBe(75) + }) + + it('should return the :defaultWidth for :columnWidth if not measured', () => { + const cache = new CellMeasurerCache({ + defaultWidth: 25 + }) + expect(cache.columnWidth({ index: 0 })).toBe(25) + }) + + it('should return the :defaultHeight for :rowHeight if not measured', () => { + const cache = new CellMeasurerCache({ + defaultHeight: 25 + }) + expect(cache.rowHeight({ index: 0 })).toBe(25) }) }) diff --git a/source/CellMeasurer/CellMeasurerCache.js b/source/CellMeasurer/CellMeasurerCache.js index 5e224f0e9..5e8b821ec 100644 --- a/source/CellMeasurer/CellMeasurerCache.js +++ b/source/CellMeasurer/CellMeasurerCache.js @@ -66,19 +66,26 @@ export interface CellMeasurerCacheType { * Caches measurements for a given cell. */ export default class CellMeasurerCache { + _cellHeightCache: Cache; + _cellWidthCache: Cache; + _columnWidthCache: Cache; _defaultHeight: ?number; _defaultWidth: ?number; _keyMapper: KeyMapper; - _heightCache: Cache; - _widthCache: Cache; + _rowHeightCache: Cache; constructor (params : CellMeasurerCacheParams = {}) { - this._defaultHeight = params.defaultHeight - this._defaultWidth = params.defaultWidth + this._defaultHeight = params.defaultHeight || null + this._defaultWidth = params.defaultWidth || null this._keyMapper = params.keyMapper || defaultKeyMapper - this._heightCache = {} - this._widthCache = {} + this._columnCount = 0 + this._rowCount = 0 + + this._cellHeightCache = {} + this._cellWidthCache = {} + this._columnWidthCache = {} + this._rowHeightCache = {} } clear ( @@ -87,17 +94,19 @@ export default class CellMeasurerCache { ) : void { const key = this._keyMapper(rowIndex, columnIndex) - delete this._heightCache[key] - delete this._widthCache[key] + delete this._cellHeightCache[key] + delete this._cellWidthCache[key] } clearAll() : void { - this._heightCache = {} - this._widthCache = {} + this._cellHeightCache = {} + this._cellWidthCache = {} } columnWidth = ({ index } : IndexParam) => { - return this.getWidth(0, index) + return this._columnWidthCache.hasOwnProperty(index) + ? this._columnWidthCache[index] + : this._defaultWidth } getHeight ( @@ -106,8 +115,8 @@ export default class CellMeasurerCache { ) : ?number { const key = this._keyMapper(rowIndex, columnIndex) - return this._heightCache.hasOwnProperty(key) - ? this._heightCache[key] + return this._cellHeightCache.hasOwnProperty(key) + ? this._cellHeightCache[key] : this._defaultHeight } @@ -117,8 +126,8 @@ export default class CellMeasurerCache { ) : ?number { const key = this._keyMapper(rowIndex, columnIndex) - return this._widthCache.hasOwnProperty(key) - ? this._widthCache[key] + return this._cellWidthCache.hasOwnProperty(key) + ? this._cellWidthCache[key] : this._defaultWidth } @@ -128,11 +137,13 @@ export default class CellMeasurerCache { ) : boolean { const key = this._keyMapper(rowIndex, columnIndex) - return this._heightCache.hasOwnProperty(key) + return this._cellHeightCache.hasOwnProperty(key) } rowHeight = ({ index } : IndexParam) => { - return this.getHeight(index, 0) + return this._rowHeightCache.hasOwnProperty(index) + ? this._rowHeightCache[index] + : this._defaultHeight } set ( @@ -143,8 +154,27 @@ export default class CellMeasurerCache { ) : void { const key = this._keyMapper(rowIndex, columnIndex) - this._heightCache[key] = height - this._widthCache[key] = width + if (columnIndex >= this._columnCount) { + this._columnCount = columnIndex + 1 + } + if (rowIndex >= this._rowCount) { + this._rowCount = rowIndex + 1 + } + + this._cellHeightCache[key] = height + this._cellWidthCache[key] = width + + let columnWidth = 0 + for (let i = 0; i < this._rowCount; i++) { + columnWidth = Math.max(columnWidth, this.getWidth(i, columnIndex)) + } + let rowHeight = 0 + for (let i = 0; i < this._columnCount; i++) { + rowHeight = Math.max(rowHeight, this.getHeight(rowIndex, i)) + } + + this._columnWidthCache[columnIndex] = columnWidth + this._rowHeightCache[rowIndex] = rowHeight } } From 660a90af613a6f1bebae3e1031d0ecd2ec023d32 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 11 Feb 2017 11:49:40 -0800 Subject: [PATCH 05/31] Basic initial cell layout working (but not without bugs) --- source/CellMeasurer/CellMeasurer.jest.js | 35 +++++++---- source/CellMeasurer/CellMeasurer.js | 9 ++- source/CellMeasurer/CellMeasurerCache.jest.js | 10 ++-- source/CellMeasurer/CellMeasurerCache.js | 12 +++- source/Grid/Grid.js | 59 ++++++++++++++++++- source/Grid/defaultCellRangeRenderer.js | 22 ++++++- .../Grid/utils/CellSizeAndPositionManager.js | 59 ++++++++++++++----- 7 files changed, 167 insertions(+), 39 deletions(-) diff --git a/source/CellMeasurer/CellMeasurer.jest.js b/source/CellMeasurer/CellMeasurer.jest.js index 167957524..bdfa993b8 100644 --- a/source/CellMeasurer/CellMeasurer.jest.js +++ b/source/CellMeasurer/CellMeasurer.jest.js @@ -2,7 +2,7 @@ import React from 'react' import { findDOMNode } from 'react-dom' import { render } from '../TestUtils' import CellMeasurer from './CellMeasurer' -import CellMeasurerCache from './CellMeasurerCache' +import CellMeasurerCache, { DEFAULT_HEIGHT, DEFAULT_WIDTH } from './CellMeasurerCache' // Accounts for the fact that JSDom doesn't support measurements. function mockClientWidthAndHeight ({ @@ -29,12 +29,16 @@ function mockClientWidthAndHeight ({ } function renderHelper ({ - cache + cache, + invalidateGridSizeAfterRender }) { render( @@ -46,6 +50,7 @@ function renderHelper ({ describe('CellMeasurer', () => { it('componentDidMount() should measure content that is not already in the cache', () => { const cache = new CellMeasurerCache() + const invalidateGridSizeAfterRender = jest.fn() mockClientWidthAndHeight({ height: 20, @@ -59,8 +64,9 @@ describe('CellMeasurer', () => { expect(offsetWidthMock.mock.calls).toHaveLength(0) expect(cache.has(0, 0)).toBe(false) - renderHelper({ cache }) + renderHelper({ cache, invalidateGridSizeAfterRender }) + expect(invalidateGridSizeAfterRender).toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(1) expect(offsetWidthMock.mock.calls).toHaveLength(1) expect(cache.has(0, 0)).toBe(true) @@ -72,6 +78,8 @@ describe('CellMeasurer', () => { const cache = new CellMeasurerCache() cache.set(0, 0, 100, 20) + const invalidateGridSizeAfterRender = jest.fn() + mockClientWidthAndHeight({ height: 20, width: 100 @@ -79,25 +87,28 @@ describe('CellMeasurer', () => { expect(cache.has(0, 0)).toBe(true) - renderHelper({ cache }) + renderHelper({ cache, invalidateGridSizeAfterRender }) const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get + expect(invalidateGridSizeAfterRender).not.toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(0) expect(offsetWidthMock.mock.calls).toHaveLength(0) }) it('componentDidUpdate() should measure content that is not already in the cache', () => { const cache = new CellMeasurerCache() + const invalidateGridSizeAfterRender = jest.fn() - renderHelper({ cache }) + renderHelper({ cache, invalidateGridSizeAfterRender }) cache.clear(0, 0) + invalidateGridSizeAfterRender.mockReset() expect(cache.has(0, 0)).toBe(false) - expect(cache.getWidth(0, 0)).toBe(null) - expect(cache.getHeight(0, 0)).toBe(null) + expect(cache.getWidth(0, 0)).toBe(DEFAULT_WIDTH) + expect(cache.getHeight(0, 0)).toBe(DEFAULT_HEIGHT) mockClientWidthAndHeight({ height: 20, @@ -107,10 +118,11 @@ describe('CellMeasurer', () => { const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get - renderHelper({ cache }) + renderHelper({ cache, invalidateGridSizeAfterRender }) expect(cache.has(0, 0)).toBe(true) + expect(invalidateGridSizeAfterRender).toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(1) expect(offsetWidthMock.mock.calls).toHaveLength(1) expect(cache.getWidth(0, 0)).toBe(100) @@ -121,6 +133,8 @@ describe('CellMeasurer', () => { const cache = new CellMeasurerCache() cache.set(0, 0, 100, 20) + const invalidateGridSizeAfterRender = jest.fn() + expect(cache.has(0, 0)).toBe(true) mockClientWidthAndHeight({ @@ -128,12 +142,13 @@ describe('CellMeasurer', () => { width: 100 }) - renderHelper({ cache }) - renderHelper({ cache }) + renderHelper({ cache, invalidateGridSizeAfterRender }) + renderHelper({ cache, invalidateGridSizeAfterRender }) const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get + expect(invalidateGridSizeAfterRender).not.toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(0) expect(offsetWidthMock.mock.calls).toHaveLength(0) }) diff --git a/source/CellMeasurer/CellMeasurer.js b/source/CellMeasurer/CellMeasurer.js index 1fb0bde58..236895573 100644 --- a/source/CellMeasurer/CellMeasurer.js +++ b/source/CellMeasurer/CellMeasurer.js @@ -8,6 +8,7 @@ type Props = { cache: any, // TODO type CellMeasurerCacheType children: mixed, columnIndex: number, + parent: any, // TODO type Grid rowIndex: number }; @@ -34,7 +35,7 @@ export default class CellMeasurer extends Component { } _maybeMeasureCell () { - const { cache, columnIndex, rowIndex } = this.props + const { cache, columnIndex, parent, rowIndex } = this.props if (!cache.has(rowIndex, columnIndex)) { const node = findDOMNode(this) @@ -47,6 +48,12 @@ export default class CellMeasurer extends Component { width, height ) + + // If size has changed, let Grid know to re-render. + parent.invalidateGridSizeAfterRender({ + columnIndex, + rowIndex + }) } } } diff --git a/source/CellMeasurer/CellMeasurerCache.jest.js b/source/CellMeasurer/CellMeasurerCache.jest.js index 0d5aaf594..9f88e87ca 100644 --- a/source/CellMeasurer/CellMeasurerCache.jest.js +++ b/source/CellMeasurer/CellMeasurerCache.jest.js @@ -1,4 +1,4 @@ -import CellMeasurerCache from './CellMeasurerCache' +import CellMeasurerCache, { DEFAULT_HEIGHT, DEFAULT_WIDTH } from './CellMeasurerCache' describe('CellMeasurerCache', () => { it('should correctly report cache status', () => { @@ -62,10 +62,10 @@ describe('CellMeasurerCache', () => { it('should provide a Grid-compatible :columnWidth method', () => { const cache = new CellMeasurerCache() - expect(cache.columnWidth({ index: 0 })).toBe(null) + expect(cache.columnWidth({ index: 0 })).toBe(DEFAULT_WIDTH) cache.set(0, 0, 100, 50) expect(cache.columnWidth({ index: 0 })).toBe(100) - expect(cache.columnWidth({ index: 1 })).toBe(null) + expect(cache.columnWidth({ index: 1 })).toBe(DEFAULT_WIDTH) cache.set(1, 0, 75, 50) expect(cache.columnWidth({ index: 0 })).toBe(100) cache.set(2, 0, 125, 50) @@ -74,10 +74,10 @@ describe('CellMeasurerCache', () => { it('should provide a Grid-compatible :rowHeight method', () => { const cache = new CellMeasurerCache() - expect(cache.rowHeight({ index: 0 })).toBe(null) + expect(cache.rowHeight({ index: 0 })).toBe(DEFAULT_HEIGHT) cache.set(0, 0, 100, 50) expect(cache.rowHeight({ index: 0 })).toBe(50) - expect(cache.rowHeight({ index: 1 })).toBe(null) + expect(cache.rowHeight({ index: 1 })).toBe(DEFAULT_HEIGHT) cache.set(0, 1, 100, 25) expect(cache.rowHeight({ index: 0 })).toBe(50) cache.set(0, 2, 100, 75) diff --git a/source/CellMeasurer/CellMeasurerCache.js b/source/CellMeasurer/CellMeasurerCache.js index 5e8b821ec..270af1df0 100644 --- a/source/CellMeasurer/CellMeasurerCache.js +++ b/source/CellMeasurer/CellMeasurerCache.js @@ -1,5 +1,8 @@ /** @flow */ +export const DEFAULT_HEIGHT = 30 +export const DEFAULT_WIDTH = 100 + /** * Enables more intelligent mapping of a given column and row index to an item ID. * This prevents a cell cache from being invalidated when its parent collection is modified. @@ -75,8 +78,8 @@ export default class CellMeasurerCache { _rowHeightCache: Cache; constructor (params : CellMeasurerCacheParams = {}) { - this._defaultHeight = params.defaultHeight || null - this._defaultWidth = params.defaultWidth || null + this._defaultHeight = params.defaultHeight || DEFAULT_HEIGHT + this._defaultWidth = params.defaultWidth || DEFAULT_WIDTH this._keyMapper = params.keyMapper || defaultKeyMapper this._columnCount = 0 @@ -161,9 +164,13 @@ export default class CellMeasurerCache { this._rowCount = rowIndex + 1 } + // Size is cached per cell so we don't have to re-measure if cells are re-ordered. this._cellHeightCache[key] = height this._cellWidthCache[key] = width + // :columnWidth and :rowHeight are derived based on all cells in a column/row. + // Pre-cache these derived values for faster lookup later. + // Reads are expected to occur more frequently than writes in this case. let columnWidth = 0 for (let i = 0; i < this._rowCount; i++) { columnWidth = Math.max(columnWidth, this.getWidth(i, columnIndex)) @@ -172,7 +179,6 @@ export default class CellMeasurerCache { for (let i = 0; i < this._columnCount; i++) { rowHeight = Math.max(rowHeight, this.getHeight(rowIndex, i)) } - this._columnWidthCache[columnIndex] = columnWidth this._rowHeightCache[rowIndex] = rowHeight } diff --git a/source/Grid/Grid.js b/source/Grid/Grid.js index 364fecaac..07b8164a8 100644 --- a/source/Grid/Grid.js +++ b/source/Grid/Grid.js @@ -2,7 +2,8 @@ import React, { Component, PropTypes } from 'react' import cn from 'classnames' import calculateSizeAndPositionDataAndUpdateScrollOffset from './utils/calculateSizeAndPositionDataAndUpdateScrollOffset' -import ScalingCellSizeAndPositionManager from './utils/ScalingCellSizeAndPositionManager' +// @TODO (bvaughn) import ScalingCellSizeAndPositionManager from './utils/ScalingCellSizeAndPositionManager' +import CellSizeAndPositionManager from './utils/CellSizeAndPositionManager' import createCallbackMemoizer from '../utils/createCallbackMemoizer' import getOverscanIndices, { SCROLL_DIRECTION_BACKWARD, SCROLL_DIRECTION_FORWARD } from './utils/getOverscanIndices' import getScrollbarSize from 'dom-helpers/util/scrollbarSize' @@ -88,6 +89,9 @@ export default class Grid extends Component { /** Optional inline style applied to inner cell-container */ containerStyle: PropTypes.object, + // @TODO (bvaughn) Document + deferredMeasurementCache: PropTypes.object, + /** * Used to estimate the total width of a Grid before all of its columns have actually been measured. * The estimated total width is adjusted as columns are rendered. @@ -231,12 +235,15 @@ export default class Grid extends Component { this._columnWidthGetter = this._wrapSizeGetter(props.columnWidth) this._rowHeightGetter = this._wrapSizeGetter(props.rowHeight) - this._columnSizeAndPositionManager = new ScalingCellSizeAndPositionManager({ + this._deferredInvalidateColumnIndex = null + this._deferredInvalidateRowIndex = null + + this._columnSizeAndPositionManager = new CellSizeAndPositionManager({ // @TODO (bvaughn) Use scaling impl cellCount: props.columnCount, cellSizeGetter: (params) => this._columnWidthGetter(params), estimatedCellSize: this._getEstimatedColumnSize(props) }) - this._rowSizeAndPositionManager = new ScalingCellSizeAndPositionManager({ + this._rowSizeAndPositionManager = new CellSizeAndPositionManager({ // @TODO (bvaughn) Use scaling impl cellCount: props.rowCount, cellSizeGetter: (params) => this._rowHeightGetter(params), estimatedCellSize: this._getEstimatedRowSize(props) @@ -247,6 +254,25 @@ export default class Grid extends Component { this._styleCache = {} } + /** + * Invalidate Grid size and recompute visible cells. + * This is a deferred wrapper for recomputeGridSize(). + * It sets a flag to be evaluated on cDM/cDU to avoid unnecessary renders. + * This method is intended for advanced use-cases like CellMeasurer. + */ + // @TODO (bvaughn) Add automated test coverage for this. + invalidateGridSizeAfterRender ({ + columnIndex, + rowIndex + }) { + this._deferredInvalidateColumnIndex = typeof this._deferredInvalidateColumnIndex === 'number' + ? Math.min(this._deferredInvalidateColumnIndex, columnIndex) + : columnIndex + this._deferredInvalidateRowIndex = typeof this._deferredInvalidateRowIndex === 'number' + ? Math.min(this._deferredInvalidateRowIndex, rowIndex) + : rowIndex + } + /** * Pre-measure all columns and rows in a Grid. * Typically cells are only measured as needed and estimated sizes are used for cells that have not yet been measured. @@ -300,6 +326,10 @@ export default class Grid extends Component { componentDidMount () { const { scrollLeft, scrollToColumn, scrollTop, scrollToRow } = this.props + if (this._gridSizeInvalidated()) { + return; + } + // If this component was first rendered server-side, scrollbar size will be undefined. // In that event we need to remeasure. if (!this._scrollbarSizeMeasured) { @@ -338,6 +368,10 @@ export default class Grid extends Component { const { autoHeight, columnCount, height, rowCount, scrollToAlignment, scrollToColumn, scrollToRow, width } = this.props const { scrollLeft, scrollPositionChangeReason, scrollTop } = this.state + if (this._gridSizeInvalidated()) { + return; + } + // Handle edge case where column or row count has only just increased over 0. // In this case we may have to restore a previously-specified scroll offset. // For more info see bvaughn/react-virtualized/issues/218 @@ -627,6 +661,7 @@ export default class Grid extends Component { cellRenderer, cellRangeRenderer, columnCount, + deferredMeasurementCache, height, overscanColumnCount, overscanRowCount, @@ -698,8 +733,10 @@ export default class Grid extends Component { columnSizeAndPositionManager: this._columnSizeAndPositionManager, columnStartIndex: this._columnStartIndex, columnStopIndex: this._columnStopIndex, + deferredMeasurementCache, horizontalOffsetAdjustment, isScrolling, + parent: this, rowSizeAndPositionManager: this._rowSizeAndPositionManager, rowStartIndex: this._rowStartIndex, rowStopIndex: this._rowStopIndex, @@ -768,6 +805,22 @@ export default class Grid extends Component { : props.estimatedRowSize } + _gridSizeInvalidated () { + if (typeof this._deferredInvalidateColumnIndex === 'number') { + const columnIndex = this._deferredInvalidateColumnIndex + const rowIndex = this._deferredInvalidateRowIndex + + delete this._deferredInvalidateColumnIndex + delete this._deferredInvalidateRowIndex + + this.recomputeGridSize({ columnIndex, rowIndex }) + + return true; + } + + return false + } + _invokeOnGridRenderedHelper () { const { onSectionRendered } = this.props diff --git a/source/Grid/defaultCellRangeRenderer.js b/source/Grid/defaultCellRangeRenderer.js index a8364d2df..4fb315a90 100644 --- a/source/Grid/defaultCellRangeRenderer.js +++ b/source/Grid/defaultCellRangeRenderer.js @@ -9,8 +9,10 @@ export default function defaultCellRangeRenderer ({ columnSizeAndPositionManager, columnStartIndex, columnStopIndex, + deferredMeasurementCache, horizontalOffsetAdjustment, isScrolling, + parent, // Grid (or List or Table) rowSizeAndPositionManager, rowStartIndex, rowStopIndex, @@ -21,9 +23,11 @@ export default function defaultCellRangeRenderer ({ visibleColumnIndices, visibleRowIndices }: DefaultCellRangeRendererParams) { + const deferredMode = typeof deferredMeasurementCache !== 'undefined' + const renderedCells = [] const offsetAdjusted = verticalOffsetAdjustment || horizontalOffsetAdjustment - const canCacheStyle = !isScrolling || !offsetAdjusted + const canCacheStyle = !deferredMode && (!isScrolling || !offsetAdjusted) for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) { let rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex) @@ -43,12 +47,23 @@ export default function defaultCellRangeRenderer ({ if (canCacheStyle && styleCache[key]) { style = styleCache[key] } else { + // In deferred mode, cells will be initially rendered before we know their size. + // Don't interfere with CellMeasurer's measurements by setting an invalid size. + // @TODO (bvaughn) Add automated test coverage for this. + let deferredCell = deferredMode && !deferredMeasurementCache.has(rowIndex, columnIndex) + let height = deferredCell + ? 'auto' + : rowDatum.size + let width = deferredCell + ? 'auto' + : columnDatum.size + style = { - height: rowDatum.size, + height, left: columnDatum.offset + horizontalOffsetAdjustment, position: 'absolute', top: rowDatum.offset + verticalOffsetAdjustment, - width: columnDatum.size + width } styleCache[key] = style @@ -59,6 +74,7 @@ export default function defaultCellRangeRenderer ({ isScrolling, isVisible, key, + parent, rowIndex, style } diff --git a/source/Grid/utils/CellSizeAndPositionManager.js b/source/Grid/utils/CellSizeAndPositionManager.js index 9e869c2cf..8c896ee7d 100644 --- a/source/Grid/utils/CellSizeAndPositionManager.js +++ b/source/Grid/utils/CellSizeAndPositionManager.js @@ -19,6 +19,9 @@ export default class CellSizeAndPositionManager { // Measurements for cells up to this index can be trusted; cells afterward should be estimated. this._lastMeasuredIndex = -1 + + // Used in deferred mode to track which cells have been queued for measurement. + this._lastBatchedIndex = -1 } configure ({ @@ -41,6 +44,13 @@ export default class CellSizeAndPositionManager { return this._lastMeasuredIndex } + getOffsetAdjustment ({ + containerSize, + offset // safe + }: ContainerSizeAndOffset): number { + return 0 + } + /** * This method returns the size and position for the cell at the specified index. * It just-in-time calculates (or used cached values) for cells leading up to the index. @@ -57,19 +67,28 @@ export default class CellSizeAndPositionManager { for (var i = this._lastMeasuredIndex + 1; i <= index; i++) { let size = this._cellSizeGetter({ index: i }) - if (size == null || isNaN(size)) { + // undefined or NaN probably means a logic error in the size getter. + // null means we're using CellMeasurer and haven't yet measured a given index. + if (size === undefined || isNaN(size)) { throw Error(`Invalid size returned for cell ${i} of value ${size}`) + } else if (size === null) { + this._cellSizeAndPositionData[i] = { + offset, + size: 0 + } + + this._lastBatchedIndex = index + } else { + this._cellSizeAndPositionData[i] = { + offset, + size + } + + offset += size + + this._lastMeasuredIndex = index } - - this._cellSizeAndPositionData[i] = { - offset, - size - } - - offset += size } - - this._lastMeasuredIndex = index } return this._cellSizeAndPositionData[index] @@ -142,10 +161,12 @@ export default class CellSizeAndPositionManager { return Math.max(0, Math.min(totalSize - containerSize, idealOffset)) } - getVisibleCellRange ({ - containerSize, - offset - }: GetVisibleCellRangeParams): VisibleCellRange { + getVisibleCellRange (params: GetVisibleCellRangeParams): VisibleCellRange { + let { + containerSize, + offset + } = params + const totalSize = this.getTotalSize() if (totalSize === 0) { @@ -276,11 +297,21 @@ type ConfigureParams = { estimatedCellSize: number }; +type ContainerSizeAndOffset = { + containerSize: number, + offset: number +}; + type GetVisibleCellRangeParams = { containerSize: number, offset: number }; +type RequiresMoreMeasurementsParams = { + containerSize: number, + offset: number +} + type SizeAndPositionData = { offset: number, size: number From 4f3dc17f47663dd7bedb536e5ec0e86b63e282ac Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 11 Feb 2017 13:33:16 -0800 Subject: [PATCH 06/31] Polyfill requestAnimationFrame for React tests with fiber --- .flowconfig | 7 +++++++ package.json | 2 ++ source/jest-setup.js | 3 +++ yarn.lock | 14 ++++++++++++++ 4 files changed, 26 insertions(+) create mode 100644 .flowconfig diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 000000000..4a58bdcde --- /dev/null +++ b/.flowconfig @@ -0,0 +1,7 @@ +[ignore] + +[include] + +[libs] + +[options] diff --git a/package.json b/package.json index 93d5cb8bf..afa0182c3 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "postcss": "^5.0.14", "postcss-cli": "^2.3.3", "postcss-loader": "^0.9.1", + "raf": "^3.3.0", "react": "16.0.0-alpha.2", "react-addons-shallow-compare": "16.0.0-alpha.2", "react-addons-test-utils": "16.0.0-alpha.2", @@ -149,6 +150,7 @@ "babel-runtime": "^6.11.6", "classnames": "^2.2.3", "dom-helpers": "^2.4.0 || ^3.0.0", + "flow-bin": "^0.39.0", "loose-envify": "^1.3.0" }, "peerDependencies": { diff --git a/source/jest-setup.js b/source/jest-setup.js index 843c22985..d0d8597c0 100644 --- a/source/jest-setup.js +++ b/source/jest-setup.js @@ -3,3 +3,6 @@ jest.mock('dom-helpers/util/scrollbarSize', () => { return 20 } }) + +// Polyfill requestAnimationFrame() for ReactDOMFrameScheduling +global.requestAnimationFrame = require('raf') diff --git a/yarn.lock b/yarn.lock index 8bc827d85..8a4a23c1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2353,6 +2353,10 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" +flow-bin@^0.39.0: + version "0.39.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.39.0.tgz#b1012a14460df1aa79d3a728e10f93c6944226d0" + for-in@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" @@ -4145,6 +4149,10 @@ pbkdf2-compat@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz#b6e0c8fa99494d94e0511575802a59a5c142f288" +performance-now@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -4552,6 +4560,12 @@ querystringify@0.0.x: version "0.0.4" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c" +raf@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.3.0.tgz#93845eeffc773f8129039f677f80a36044eee2c3" + dependencies: + performance-now "~0.2.0" + randomatic@^1.1.3: version "1.1.5" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.5.tgz#5e9ef5f2d573c67bd2b8124ae90b5156e457840b" From 7fd2f2a7ef5692fa5bbd1ab57d1ffd21aae9cd12 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 12 Feb 2017 13:01:09 -0800 Subject: [PATCH 07/31] Improved examples. Basic List/Table support. Min width/height support for CellMeasurerCache. --- source/CellMeasurer/CellMeasurer.example.css | 26 +- source/CellMeasurer/CellMeasurer.example.js | 434 ++++++++++++++++-- source/CellMeasurer/CellMeasurer.jest.js | 5 +- source/CellMeasurer/CellMeasurer.js | 57 ++- source/CellMeasurer/CellMeasurerCache.jest.js | 3 + source/CellMeasurer/CellMeasurerCache.js | 69 +-- source/Grid/Grid.js | 13 +- source/Grid/defaultCellRangeRenderer.js | 40 +- .../utils/CellSizeAndPositionManager.jest.js | 3 + .../Grid/utils/CellSizeAndPositionManager.js | 24 +- source/List/List.jest.js | 2 + source/MultiGrid/MultiGrid.js | 7 - source/Table/Table.jest.js | 2 + source/Table/Table.js | 5 +- source/demo/utils.js | 71 ++- 15 files changed, 638 insertions(+), 123 deletions(-) diff --git a/source/CellMeasurer/CellMeasurer.example.css b/source/CellMeasurer/CellMeasurer.example.css index 8677f1dbb..861ee4717 100644 --- a/source/CellMeasurer/CellMeasurer.example.css +++ b/source/CellMeasurer/CellMeasurer.example.css @@ -31,7 +31,7 @@ display: flex; flex-direction: column; justify-content: center; - padding: 0 .5em; + padding: 0.5em 1em; } .cell { border-right: 1px solid #e0e0e0; @@ -44,3 +44,27 @@ text-overflow: ellipsis; overflow: hidden; } + +.tableRow { + border-bottom: 1px solid #eee; +} +.tableColumn { + padding: 5px 15px 5px 0; +} + +.Tab { + border: 1px solid #ddd; + border-radius: 4px; + padding: 4px 6px; + outline: none; + background: #eee; + margin: 4px; + cursor: pointer; +} + +.ActiveTab { + background-color: #4db6ac; + border: 1px solid #3ca59b; + color: rgba(255, 255, 255, 0.8); + cursor: default; +} \ No newline at end of file diff --git a/source/CellMeasurer/CellMeasurer.example.js b/source/CellMeasurer/CellMeasurer.example.js index b3d86a645..b316be628 100644 --- a/source/CellMeasurer/CellMeasurer.example.js +++ b/source/CellMeasurer/CellMeasurer.example.js @@ -1,3 +1,4 @@ +/** @flow */ import Immutable from 'immutable' import React, { Component, PropTypes } from 'react' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' @@ -5,16 +6,19 @@ import AutoSizer from '../AutoSizer' import CellMeasurer from './CellMeasurer' import CellMeasurerCache from './CellMeasurerCache' import Grid from '../Grid' +import List from '../List' +import { Column, Table } from '../Table' import shallowCompare from 'react-addons-shallow-compare' import cn from 'classnames' +import { findDOMNode } from 'react-dom' import styles from './CellMeasurer.example.css' +const COLUMN_COUNT = 50 const COLUMN_WIDTH = 150 +const HEIGHT = 400 const ROW_COUNT = 50 const ROW_HEIGHT = 35 -const cache = new CellMeasurerCache() - export default class CellMeasurerExample extends Component { static contextTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired @@ -23,27 +27,59 @@ export default class CellMeasurerExample extends Component { constructor (props, context) { super(props, context) - this._cellRendererOne = this._cellRendererOne.bind(this) + this.state = { + currentTab: 0 + } + + this._onClick = this._onClick.bind(this) } render () { + const { list } = this.context + const { currentTab } = this.state + + const buttonProps = { + currentTab, + onClick: this._onClick + } + + const DemoComponent = demoComponents[currentTab] + return ( - - {({ width }) => ( -
} - rowCount={ROW_COUNT} - rowHeight={ROW_HEIGHT} - width={width} - /> - )} - + + + + + This component can be used to just-in-time measure dynamic content (eg. messages in a chat interface). + + + + {({ width }) => ( +
+
+ Grid: + dynamic width text + dynamic height text + + List: + dynamic height image + + Table: + mixed fixed and dynamic height text +
+ + +
+ )} +
+
) } @@ -51,22 +87,364 @@ export default class CellMeasurerExample extends Component { return shallowCompare(this, nextProps, nextState) } - _cellRendererOne ({ columnIndex, key, rowIndex, style }) { - const classNames = cn(rowClass, styles.cell, { - [styles.centeredCell]: columnIndex > 2 + _onClick (id) { + this.setState({ + currentTab: id + }) + } +} + +function getClassName ({ columnIndex, rowIndex }) { + const rowClass = rowIndex % 2 === 0 ? styles.evenRow : styles.oddRow + + return cn(rowClass, styles.cell, { + [styles.centeredCell]: columnIndex > 2 + }) +} + +function getContent ({ index, datum, long = true }) { + switch (index % 3) { + case 0: + return datum.color + case 1: + return datum.name + case 2: + return long ? datum.randomLong : datum.random + } +} + +function Tab ({ children, currentTab, id, onClick }) { + const classNames = cn(styles.Tab, { + [styles.ActiveTab]: currentTab === id + }) + + return ( + + ) +} + +class DynamicWidthGrid extends Component { + static propTypes = { + list: PropTypes.instanceOf(Immutable.List).isRequired, + width: PropTypes.number.isRequired + } + + constructor (props, context) { + super(props, context) + + this._cache = new CellMeasurerCache({ + defaultHeight: ROW_HEIGHT, + fixedHeight: true }) + this._cellRenderer = this._cellRenderer.bind(this) + } + + render () { + const { width } = this.props + return ( - + ) + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState) + } + + _cellRenderer ({ columnIndex, key, parent, rowIndex, style }) { + const { list } = this.props + + const datum = list.get(rowIndex + columnIndex % list.size) + const classNames = getClassName({ columnIndex, rowIndex }) + const content = getContent({ index: columnIndex, datum, long: false }) + + return ( + -
- {rowIndex}, {columnIndex} +
+ {content}
) } } + +class DynamiHeightGrid extends Component { + static propTypes = { + list: PropTypes.instanceOf(Immutable.List).isRequired, + width: PropTypes.number.isRequired + } + + constructor (props, context) { + super(props, context) + + this._cache = new CellMeasurerCache({ + defaultWidth: COLUMN_WIDTH, + fixedWidth: true + }) + + this._cellRenderer = this._cellRenderer.bind(this) + } + + render () { + const { width } = this.props + + return ( + + ) + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState) + } + + _cellRenderer ({ columnIndex, key, parent, rowIndex, style }) { + const { list } = this.props + + const datum = list.get(rowIndex + columnIndex % list.size) + const classNames = getClassName({ columnIndex, rowIndex }) + const content = getContent({ index: rowIndex, datum }) + + return ( + +
+ {content} +
+
+ ) + } +} + +class DynamicHeightList extends Component { + static propTypes = { + list: PropTypes.instanceOf(Immutable.List).isRequired, + width: PropTypes.number.isRequired + } + + constructor (props, context) { + super(props, context) + + this._cache = new CellMeasurerCache({ + fixedWidth: true, + minHeight: 50 + }) + + this._rowRenderer = this._rowRenderer.bind(this) + } + + render () { + const { width } = this.props + + return ( + + ) + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState) + } + + _rowRenderer ({ index, isScrolling, key, parent, style }) { + const { list } = this.props + + const datum = list.get(index % list.size) + const classNames = getClassName({ columnIndex: 0, rowIndex: index }) + + const imageWidth = 300 + const imageHeight = datum.size + + const source = `http://lorempixel.com/${imageWidth}/${imageHeight}/` + + return ( + + {({ measure }) => ( +
+ +
+ )} +
+ ) + } +} + +class DynamicHeightTableColumn extends Component { + static propTypes = { + list: PropTypes.instanceOf(Immutable.List).isRequired, + width: PropTypes.number.isRequired + } + + constructor (props, context) { + super(props, context) + + this._cache = new CellMeasurerCache({ + fixedWidth: true, + minHeight: 25 + }) + + this._columnCellRenderer = this._columnCellRenderer.bind(this) + this._rowGetter = this._rowGetter.bind(this) + } + + render () { + const { width } = this.props + + return ( + + + + +
+ ) + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState) + } + + _columnCellRenderer ({ cellData, columnData, dataKey, parent, rowData, rowIndex }) { + const { list } = this.props + + const datum = list.get(rowIndex % list.size) + const content = rowIndex % 5 === 0 + ? '' + : datum.randomLong + + return ( + +
+ {content} +
+
+ ) + } + + _rowGetter ({ index }) { + const { list } = this.props + + return list.get(index % list.size) + } +} + +const demoComponents = [ + DynamicWidthGrid, + DynamiHeightGrid, + DynamicHeightList, + DynamicHeightTableColumn +] diff --git a/source/CellMeasurer/CellMeasurer.jest.js b/source/CellMeasurer/CellMeasurer.jest.js index bdfa993b8..406935d59 100644 --- a/source/CellMeasurer/CellMeasurer.jest.js +++ b/source/CellMeasurer/CellMeasurer.jest.js @@ -1,5 +1,6 @@ +/* global Element */ + import React from 'react' -import { findDOMNode } from 'react-dom' import { render } from '../TestUtils' import CellMeasurer from './CellMeasurer' import CellMeasurerCache, { DEFAULT_HEIGHT, DEFAULT_WIDTH } from './CellMeasurerCache' @@ -42,7 +43,7 @@ function renderHelper ({ rowIndex={0} style={{}} > -
+
) } diff --git a/source/CellMeasurer/CellMeasurer.js b/source/CellMeasurer/CellMeasurer.js index 236895573..4a54b6157 100644 --- a/source/CellMeasurer/CellMeasurer.js +++ b/source/CellMeasurer/CellMeasurer.js @@ -1,14 +1,13 @@ /** @flow */ -import React, { Component, PropTypes } from 'react' +import { Component } from 'react' import shallowCompare from 'react-addons-shallow-compare' import { findDOMNode } from 'react-dom' -import type { CellMeasurerCacheType } from './CellMeasurerCache' type Props = { - cache: any, // TODO type CellMeasurerCacheType + cache: any, children: mixed, columnIndex: number, - parent: any, // TODO type Grid + parent: any, rowIndex: number }; @@ -18,20 +17,33 @@ type Props = { * Cached-content is not be re-measured. */ export default class CellMeasurer extends Component { - constructor (props : Props, state) { - super(props, state) + props: Props; + + constructor (props, context) { + super(props, context) + + this._measure = this._measure.bind(this) } - componentDidMount() { + componentDidMount () { this._maybeMeasureCell() } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate (prevProps, prevState) { this._maybeMeasureCell() } - render() { - return this.props.children; + render () { + const { children } = this.props + // @TODO (bvaughn) __DEV__ mode check for parent.props.deferredMeasurementCache + + return typeof children === 'function' + ? children({ measure: this._measure }) + : children + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState) } _maybeMeasureCell () { @@ -56,4 +68,29 @@ export default class CellMeasurer extends Component { }) } } + + _measure (force = false) { + const { cache, columnIndex, parent, rowIndex } = this.props + + const node = findDOMNode(this) + const height = node.offsetHeight + const width = node.offsetWidth + + if ( + height !== cache.getHeight(rowIndex, columnIndex) || + width !== cache.getWidth(rowIndex, columnIndex) + ) { + cache.set( + rowIndex, + columnIndex, + width, + height + ) + + parent.recomputeGridSize({ + columnIndex, + rowIndex + }) + } + } } diff --git a/source/CellMeasurer/CellMeasurerCache.jest.js b/source/CellMeasurer/CellMeasurerCache.jest.js index 9f88e87ca..5a3ad1be5 100644 --- a/source/CellMeasurer/CellMeasurerCache.jest.js +++ b/source/CellMeasurer/CellMeasurerCache.jest.js @@ -1,6 +1,9 @@ import CellMeasurerCache, { DEFAULT_HEIGHT, DEFAULT_WIDTH } from './CellMeasurerCache' describe('CellMeasurerCache', () => { + // @TODO (bvaughn) Test fixed sizes and overrides for default + // @TODO (bvaughn) Test min width/height + it('should correctly report cache status', () => { const cache = new CellMeasurerCache() expect(cache.has(0, 0)).toBe(false) diff --git a/source/CellMeasurer/CellMeasurerCache.js b/source/CellMeasurer/CellMeasurerCache.js index 270af1df0..1207891b9 100644 --- a/source/CellMeasurer/CellMeasurerCache.js +++ b/source/CellMeasurer/CellMeasurerCache.js @@ -3,10 +3,8 @@ export const DEFAULT_HEIGHT = 30 export const DEFAULT_WIDTH = 100 -/** - * Enables more intelligent mapping of a given column and row index to an item ID. - * This prevents a cell cache from being invalidated when its parent collection is modified. - */ +// Enables more intelligent mapping of a given column and row index to an item ID. +// This prevents a cell cache from being invalidated when its parent collection is modified. type KeyMapper = ( rowIndex: number, columnIndex: number @@ -15,6 +13,10 @@ type KeyMapper = ( type CellMeasurerCacheParams = { defaultHeight ?: number, defaultWidth ?: number, + fixedHeight ?: boolean, + fixedWidth ?: boolean, + minHeight: ?number, + minWidth: ?number, keyMapper ?: KeyMapper }; @@ -26,45 +28,6 @@ type IndexParam = { index: number }; -export interface CellMeasurerCacheType { - clear ( - rowIndex: number, - columnIndex: number - ) : void; - - clearAll () : void; - - columnWidth ( - index : number - ) : ?number; - - getHeight ( - rowIndex: number, - columnIndex: number - ) : ?number; - - getWidth ( - rowIndex: number, - columnIndex: number - ) : ?number; - - has ( - rowIndex: number, - columnIndex: number - ) : boolean; - - rowHeight ( - index : number - ) : ?number; - - set ( - rowIndex: number, - columnIndex: number, - width: number, - height: number - ) : void; -}; - /** * Caches measurements for a given cell. */ @@ -74,12 +37,18 @@ export default class CellMeasurerCache { _columnWidthCache: Cache; _defaultHeight: ?number; _defaultWidth: ?number; + _minHeight: ?number; + _minWidth: ?number; _keyMapper: KeyMapper; _rowHeightCache: Cache; constructor (params : CellMeasurerCacheParams = {}) { this._defaultHeight = params.defaultHeight || DEFAULT_HEIGHT this._defaultWidth = params.defaultWidth || DEFAULT_WIDTH + this._hasFixedHeight = params.fixedHeight === true + this._hasFixedWidth = params.fixedWidth === true + this._minHeight = params.minHeight || 0 + this._minWidth = params.minWidth || 0 this._keyMapper = params.keyMapper || defaultKeyMapper this._columnCount = 0 @@ -101,7 +70,7 @@ export default class CellMeasurerCache { delete this._cellWidthCache[key] } - clearAll() : void { + clearAll () : void { this._cellHeightCache = {} this._cellWidthCache = {} } @@ -112,6 +81,14 @@ export default class CellMeasurerCache { : this._defaultWidth } + hasFixedHeight () : boolean { + return this._hasFixedHeight + } + + hasFixedWidth () : boolean { + return this._hasFixedWidth + } + getHeight ( rowIndex: number, columnIndex: number @@ -119,7 +96,7 @@ export default class CellMeasurerCache { const key = this._keyMapper(rowIndex, columnIndex) return this._cellHeightCache.hasOwnProperty(key) - ? this._cellHeightCache[key] + ? Math.max(this._minHeight, this._cellHeightCache[key]) : this._defaultHeight } @@ -130,7 +107,7 @@ export default class CellMeasurerCache { const key = this._keyMapper(rowIndex, columnIndex) return this._cellWidthCache.hasOwnProperty(key) - ? this._cellWidthCache[key] + ? Math.max(this._minWidth, this._cellWidthCache[key]) : this._defaultWidth } diff --git a/source/Grid/Grid.js b/source/Grid/Grid.js index 07b8164a8..b87451b68 100644 --- a/source/Grid/Grid.js +++ b/source/Grid/Grid.js @@ -89,7 +89,7 @@ export default class Grid extends Component { /** Optional inline style applied to inner cell-container */ containerStyle: PropTypes.object, - // @TODO (bvaughn) Document + // @TODO (bvaughn) Document this; and is this the best name? deferredMeasurementCache: PropTypes.object, /** @@ -238,12 +238,17 @@ export default class Grid extends Component { this._deferredInvalidateColumnIndex = null this._deferredInvalidateRowIndex = null + const deferredMeasurementCache = props.deferredMeasurementCache + const deferredMode = typeof deferredMeasurementCache !== 'undefined' + this._columnSizeAndPositionManager = new CellSizeAndPositionManager({ // @TODO (bvaughn) Use scaling impl + batchAllCells: deferredMode && !deferredMeasurementCache.hasFixedHeight(), cellCount: props.columnCount, cellSizeGetter: (params) => this._columnWidthGetter(params), estimatedCellSize: this._getEstimatedColumnSize(props) }) this._rowSizeAndPositionManager = new CellSizeAndPositionManager({ // @TODO (bvaughn) Use scaling impl + batchAllCells: deferredMode && !deferredMeasurementCache.hasFixedWidth(), cellCount: props.rowCount, cellSizeGetter: (params) => this._rowHeightGetter(params), estimatedCellSize: this._getEstimatedRowSize(props) @@ -327,7 +332,7 @@ export default class Grid extends Component { const { scrollLeft, scrollToColumn, scrollTop, scrollToRow } = this.props if (this._gridSizeInvalidated()) { - return; + return } // If this component was first rendered server-side, scrollbar size will be undefined. @@ -369,7 +374,7 @@ export default class Grid extends Component { const { scrollLeft, scrollPositionChangeReason, scrollTop } = this.state if (this._gridSizeInvalidated()) { - return; + return } // Handle edge case where column or row count has only just increased over 0. @@ -815,7 +820,7 @@ export default class Grid extends Component { this.recomputeGridSize({ columnIndex, rowIndex }) - return true; + return true } return false diff --git a/source/Grid/defaultCellRangeRenderer.js b/source/Grid/defaultCellRangeRenderer.js index 4fb315a90..c615c183c 100644 --- a/source/Grid/defaultCellRangeRenderer.js +++ b/source/Grid/defaultCellRangeRenderer.js @@ -27,7 +27,7 @@ export default function defaultCellRangeRenderer ({ const renderedCells = [] const offsetAdjusted = verticalOffsetAdjustment || horizontalOffsetAdjustment - const canCacheStyle = !deferredMode && (!isScrolling || !offsetAdjusted) + const canCacheStyle = !isScrolling || !offsetAdjusted for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) { let rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex) @@ -50,23 +50,31 @@ export default function defaultCellRangeRenderer ({ // In deferred mode, cells will be initially rendered before we know their size. // Don't interfere with CellMeasurer's measurements by setting an invalid size. // @TODO (bvaughn) Add automated test coverage for this. - let deferredCell = deferredMode && !deferredMeasurementCache.has(rowIndex, columnIndex) - let height = deferredCell - ? 'auto' - : rowDatum.size - let width = deferredCell - ? 'auto' - : columnDatum.size + if ( + deferredMode && + !deferredMeasurementCache.has(rowIndex, columnIndex) + ) { + // Position not-yet-measured cells at top/left 0,0, + // And give them width/height of 'auto' so they can grow larger than the parent Grid if necessary. + // Positioning them further to the right/bottom influences their measured size. + style = { + height: 'auto', + left: 0, + position: 'absolute', + top: 0, + width: 'auto' + } + } else { + style = { + height: rowDatum.size, + left: columnDatum.offset + horizontalOffsetAdjustment, + position: 'absolute', + top: rowDatum.offset + verticalOffsetAdjustment, + width: columnDatum.size + } - style = { - height, - left: columnDatum.offset + horizontalOffsetAdjustment, - position: 'absolute', - top: rowDatum.offset + verticalOffsetAdjustment, - width + styleCache[key] = style } - - styleCache[key] = style } let cellRendererParams = { diff --git a/source/Grid/utils/CellSizeAndPositionManager.jest.js b/source/Grid/utils/CellSizeAndPositionManager.jest.js index 3aa7d281a..c10c980ae 100644 --- a/source/Grid/utils/CellSizeAndPositionManager.jest.js +++ b/source/Grid/utils/CellSizeAndPositionManager.jest.js @@ -22,6 +22,9 @@ describe('CellSizeAndPositionManager', () => { } } + // @TODO (bvaughn) Test batchAllCells mode measures all initially; doesn't measure once already measured. + // Test this resets properly when clear is called too + describe('configure', () => { it('should update inner :cellCount and :estimatedCellSize', () => { const { cellSizeAndPositionManager } = getCellSizeAndPositionManager() diff --git a/source/Grid/utils/CellSizeAndPositionManager.js b/source/Grid/utils/CellSizeAndPositionManager.js index 8c896ee7d..7d6ebaf75 100644 --- a/source/Grid/utils/CellSizeAndPositionManager.js +++ b/source/Grid/utils/CellSizeAndPositionManager.js @@ -5,10 +5,12 @@ */ export default class CellSizeAndPositionManager { constructor ({ + batchAllCells = false, cellCount, cellSizeGetter, estimatedCellSize }: CellSizeAndPositionManagerConstructorParams) { + this._batchAllCells = batchAllCells this._cellSizeGetter = cellSizeGetter this._cellCount = cellCount this._estimatedCellSize = estimatedCellSize @@ -162,6 +164,17 @@ export default class CellSizeAndPositionManager { } getVisibleCellRange (params: GetVisibleCellRangeParams): VisibleCellRange { + // Advanced use-cases (eg CellMeasurer) require batched measurements to determine accurate sizes. + // eg we can't know a row's height without measuring the height of all columns within that row. + // @TODO (bvaughn) We could use a CellMeasurerCache method to determine if a given cell had been measured, + // by querying all perpendicular cells. But is it worth it? Maybe just a hard-limitation. + if (this._batchAllCells) { + return { + start: 0, + stop: this._cellCount - 1 + } + } + let { containerSize, offset @@ -181,7 +194,10 @@ export default class CellSizeAndPositionManager { let stop = start - while (offset < maxOffset && stop < this._cellCount - 1) { + while ( + offset < maxOffset && + stop < this._cellCount - 1 + ) { stop++ offset += this.getSizeAndPositionOfCell(stop).size @@ -287,6 +303,7 @@ export default class CellSizeAndPositionManager { } type CellSizeAndPositionManagerConstructorParams = { + batchAllCells ?: boolean, cellCount: number, cellSizeGetter: Function, estimatedCellSize: number @@ -307,11 +324,6 @@ type GetVisibleCellRangeParams = { offset: number }; -type RequiresMoreMeasurementsParams = { - containerSize: number, - offset: number -} - type SizeAndPositionData = { offset: number, size: number diff --git a/source/List/List.jest.js b/source/List/List.jest.js index a89ef5545..ae7ab6136 100644 --- a/source/List/List.jest.js +++ b/source/List/List.jest.js @@ -38,6 +38,8 @@ describe('List', () => { ) } + // @TODO (bvaughn) Test new :parent param + describe('number of rendered children', () => { it('should render enough children to fill the view', () => { const rendered = findDOMNode(render(getMarkup())) diff --git a/source/MultiGrid/MultiGrid.js b/source/MultiGrid/MultiGrid.js index 98ca192a8..851cb68e5 100644 --- a/source/MultiGrid/MultiGrid.js +++ b/source/MultiGrid/MultiGrid.js @@ -59,13 +59,6 @@ export default class MultiGrid extends Component { this._topRightGrid && this._topRightGrid.measureAllCells() } - /** See issue #546 */ - measureAllRows () { - console.warn('MultiGrid measureAllRows() is deprecated; use measureAllCells() instead.') - - this.measureAllCells() - } - /** See Grid#recomputeGridSize */ recomputeGridSize ({ columnIndex = 0, diff --git a/source/Table/Table.jest.js b/source/Table/Table.jest.js index d9969a756..aadcac4f3 100644 --- a/source/Table/Table.jest.js +++ b/source/Table/Table.jest.js @@ -76,6 +76,8 @@ describe('Table', () => { ) } + // @TODO (bvaughn) Test new :parent param + describe('children', () => { it('should accept Column children', () => { const children = [ diff --git a/source/Table/Table.js b/source/Table/Table.js index b2df13668..ebaa4be31 100644 --- a/source/Table/Table.js +++ b/source/Table/Table.js @@ -342,6 +342,7 @@ export default class Table extends Component { column, columnIndex, isScrolling, + parent, rowData, rowIndex }) { @@ -354,7 +355,7 @@ export default class Table extends Component { } = column.props const cellData = cellDataGetter({ columnData, dataKey, rowData }) - const renderedCell = cellRenderer({ cellData, columnData, dataKey, isScrolling, rowData, rowIndex }) + const renderedCell = cellRenderer({ cellData, columnData, dataKey, isScrolling, parent, rowData, rowIndex }) const style = this._cachedColumnStyles[columnIndex] @@ -443,6 +444,7 @@ export default class Table extends Component { rowIndex: index, isScrolling, key, + parent, style }) { const { @@ -468,6 +470,7 @@ export default class Table extends Component { column, columnIndex, isScrolling, + parent, rowData, rowIndex: index, scrollbarWidth diff --git a/source/demo/utils.js b/source/demo/utils.js index 1cd5552b1..42e6f4d55 100644 --- a/source/demo/utils.js +++ b/source/demo/utils.js @@ -5,11 +5,21 @@ export function generateRandomList () { const list = [] for (var i = 0; i < 1000; i++) { + const random = loremIpsum[i % loremIpsum.length] + const randoms = [random] + + for (let j = Math.round(Math.random() * 10); j--;) { + randoms.push( + loremIpsum[i * j % loremIpsum.length] + ) + } + list.push({ color: BADGE_COLORS[i % BADGE_COLORS.length], index: i, name: NAMES[i % NAMES.length], - random: loremIpsum[i % loremIpsum.length], + random, + randomLong: randoms.join(' '), size: ROW_HEIGHTS[Math.floor(Math.random() * ROW_HEIGHTS.length)] }) } @@ -24,21 +34,78 @@ const ROW_HEIGHTS = [50, 75, 100] const loremIpsum = [ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 'Phasellus vulputate odio commodo tortor sodales, et vehicula ipsum viverra.', + 'In et mollis velit, accumsan volutpat libero.', + 'Nulla rutrum tellus ipsum, eget fermentum sem dictum quis.', + 'Suspendisse eget vehicula elit.', + 'Proin ut lacus lacus.', + 'Aliquam erat volutpat.', + 'Vivamus ac suscipit est, et elementum lectus.', 'Cras tincidunt nisi in urna molestie varius.', + 'Integer in magna eu nibh imperdiet tristique.', + 'Curabitur eu pellentesque nisl.', + 'Etiam non consequat est.', + 'Duis mi massa, feugiat nec molestie sit amet, suscipit et metus.', 'Curabitur ac enim dictum arcu varius fermentum vel sodales dui.', 'Ut tristique augue at congue molestie.', + 'Integer semper sem lorem, scelerisque suscipit lacus consequat nec.', + 'Etiam euismod efficitur magna nec dignissim.', + 'Morbi vel neque lectus.', + 'Etiam ac accumsan elit, et pharetra ex.', + 'Suspendisse vitae gravida mauris.', + 'Pellentesque sed laoreet erat.', + 'Nam aliquet purus quis massa eleifend, et efficitur felis aliquam.', + 'Fusce faucibus diam erat, sed consectetur urna auctor at.', + 'Praesent et nulla velit.', 'Cras eget enim nec odio feugiat tristique eu quis ante.', + 'Morbi blandit diam vitae odio sollicitudin finibus.', + 'Integer ac ante fermentum, placerat orci vel, fermentum lacus.', + 'Maecenas est elit, semper ut posuere et, congue ut orci.', 'Phasellus eget enim vitae nunc luctus sodales a eu erat.', + 'Curabitur dapibus nisi sed nisi dictum, in imperdiet urna posuere.', + 'Vivamus commodo odio metus, tincidunt facilisis augue dictum quis.', + 'Curabitur sagittis a lectus ac sodales.', + 'Nam eget eros purus.', + 'Nam scelerisque et ante in porta.', + 'Proin vitae augue tristique, malesuada nisl ut, fermentum nisl.', 'Nulla bibendum quam id velit blandit dictum.', + 'Cras tempus ac dolor ut convallis.', + 'Sed vel ipsum est.', + 'Nulla ut leo vestibulum, ultricies sapien ac, pellentesque dolor.', + 'Etiam ultricies maximus tempus.', 'Donec dignissim mi ac libero feugiat, vitae lacinia odio viverra.', + 'Curabitur condimentum tellus sit amet neque posuere, condimentum tempus purus eleifend.', + 'Donec tempus, augue id hendrerit pretium, mauris leo congue nulla, ac iaculis erat nunc in dolor.', 'Praesent vel lectus venenatis, elementum mauris vitae, ullamcorper nulla.', + 'Maecenas non diam cursus, imperdiet massa eget, pellentesque ex.', + 'Vestibulum luctus risus vel augue auctor blandit.', + 'Nullam augue diam, pulvinar sed sapien et, hendrerit venenatis risus.', 'Quisque sollicitudin nulla nec tellus feugiat hendrerit.', 'Vestibulum a eros accumsan, lacinia eros non, pretium diam.', + 'Aenean iaculis augue sit amet scelerisque aliquam.', 'Donec ornare felis et dui hendrerit, eget bibendum nibh interdum.', + 'Maecenas tellus magna, tristique vitae orci vel, auctor tincidunt nisi.', + 'Fusce non libero quis velit porttitor maximus at eget enim.', + 'Sed in aliquet tellus.', + 'Etiam a tortor erat.', 'Donec nec diam vel tellus egestas lobortis.', + 'Vivamus dictum erat nulla, sit amet accumsan dolor scelerisque eu.', + 'In nec eleifend ex, pellentesque dapibus sapien.', + 'Duis a mollis nisi.', 'Sed ornare nisl sit amet dolor pellentesque, eu fermentum leo interdum.', 'Sed eget mauris condimentum, molestie justo eu, feugiat felis.', + 'Nunc suscipit leo non dui blandit, ac malesuada ex consequat.', + 'Morbi varius placerat congue.', + 'Praesent id velit in nunc elementum aliquet.', 'Sed luctus justo vitae nibh bibendum blandit.', + 'Sed et sapien turpis.', 'Nulla ac eros vestibulum, mollis ante eu, rutrum nulla.', - 'Sed cursus magna ut vehicula rutrum.' + 'Sed cursus magna ut vehicula rutrum.', + 'Ut consectetur feugiat consectetur.', + 'Nulla nec ligula posuere neque sollicitudin rutrum a a dui.', + 'Nulla ut quam odio.', + 'Integer dignissim sapien et orci sodales volutpat.', + 'Nullam a sapien leo.', + 'Praesent cursus semper purus, vitae gravida risus dapibus mattis.', + 'Sed pellentesque nulla lorem, in commodo arcu feugiat sed.', + 'Phasellus blandit arcu non diam varius ornare.' ] From 77414b64a1a7abbdac4f8420a743a74b2a7760bd Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 13 Feb 2017 13:40:09 -0800 Subject: [PATCH 08/31] Bumped React deps from 0.14-15.0 to 15.3-16.0 and replaced shallowCompare() with PureComponent --- README.md | 1 - package.json | 6 +-- .../ArrowKeyStepper.example.js | 9 +--- source/ArrowKeyStepper/ArrowKeyStepper.js | 9 +--- source/AutoSizer/AutoSizer.example.js | 9 +--- source/AutoSizer/AutoSizer.js | 9 +--- source/CellMeasurer/CellMeasurer.example.js | 36 +++---------- source/CellMeasurer/CellMeasurer.jest.js | 50 ++++++++++++------- source/CellMeasurer/CellMeasurer.js | 12 ++--- source/Collection/Collection.example.js | 9 +--- source/Collection/Collection.js | 10 ++-- source/Collection/CollectionView.js | 9 +--- source/ColumnSizer/ColumnSizer.example.js | 9 +--- source/ColumnSizer/ColumnSizer.js | 9 +--- source/Grid/Grid.example.js | 12 ++--- source/Grid/Grid.jest.js | 9 +--- source/Grid/Grid.js | 18 +++---- .../Grid/utils/CellSizeAndPositionManager.js | 2 - .../InfiniteLoader/InfiniteLoader.example.js | 9 +--- source/InfiniteLoader/InfiniteLoader.js | 9 +--- source/List/List.example.js | 9 +--- source/List/List.js | 9 +--- source/MultiGrid/MultiGrid.example.js | 9 +--- source/MultiGrid/MultiGrid.js | 9 +--- source/ScrollSync/ScrollSync.example.js | 9 +--- source/ScrollSync/ScrollSync.js | 9 +--- source/Table/Table.example.js | 9 +--- source/Table/Table.js | 9 +--- .../WindowScroller/WindowScroller.example.js | 9 +--- source/WindowScroller/WindowScroller.js | 9 +--- source/demo/Application.js | 9 +--- webpack.config.umd.js | 3 +- yarn.lock | 7 --- 33 files changed, 104 insertions(+), 251 deletions(-) diff --git a/README.md b/README.md index 687d63b7f..6851eae9d 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,6 @@ Dependencies React Virtualized has very few dependencies and most are managed by NPM automatically. However the following peer dependencies must be specified by your project in order to avoid version conflicts: [`react`](https://www.npmjs.com/package/react), -[`react-addons-shallow-compare`](https://www.npmjs.com/package/react-addons-shallow-compare), and [`react-dom`](https://www.npmjs.com/package/react-dom). NPM will not automatically install these for you but it will show you a warning message with instructions on how to install them. diff --git a/package.json b/package.json index afa0182c3..3bfa54d3d 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,6 @@ "postcss-loader": "^0.9.1", "raf": "^3.3.0", "react": "16.0.0-alpha.2", - "react-addons-shallow-compare": "16.0.0-alpha.2", "react-addons-test-utils": "16.0.0-alpha.2", "react-codemirror": "^0.2.6", "react-dom": "16.0.0-alpha.2", @@ -154,9 +153,8 @@ "loose-envify": "^1.3.0" }, "peerDependencies": { - "react": "^0.14.0 || ^15.0.0", - "react-addons-shallow-compare": "^0.14.0 || ^15.0.0", - "react-dom": "^0.14.0 || ^15.0.0" + "react": "^15.3.0 || ^16.0.0-alpha", + "react-dom": "^15.3.0 || ^16.0.0-alpha" }, "browserify": { "transform": [ diff --git a/source/ArrowKeyStepper/ArrowKeyStepper.example.js b/source/ArrowKeyStepper/ArrowKeyStepper.example.js index 53fe84623..c82dc8653 100644 --- a/source/ArrowKeyStepper/ArrowKeyStepper.example.js +++ b/source/ArrowKeyStepper/ArrowKeyStepper.example.js @@ -1,14 +1,13 @@ /** @flow */ -import React, { Component } from 'react' +import React, { PureComponent } from 'react' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' import ArrowKeyStepper from './ArrowKeyStepper' import AutoSizer from '../AutoSizer' import Grid from '../Grid' -import shallowCompare from 'react-addons-shallow-compare' import cn from 'classnames' import styles from './ArrowKeyStepper.example.css' -export default class ArrowKeyStepperExample extends Component { +export default class ArrowKeyStepperExample extends PureComponent { constructor (props) { super(props) @@ -102,10 +101,6 @@ export default class ArrowKeyStepperExample extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _getColumnWidth ({ index }) { return (1 + (index % 3)) * 60 } diff --git a/source/ArrowKeyStepper/ArrowKeyStepper.js b/source/ArrowKeyStepper/ArrowKeyStepper.js index 3a5d54729..8d36e20ab 100644 --- a/source/ArrowKeyStepper/ArrowKeyStepper.js +++ b/source/ArrowKeyStepper/ArrowKeyStepper.js @@ -1,11 +1,10 @@ /** @flow */ -import React, { Component, PropTypes } from 'react' -import shallowCompare from 'react-addons-shallow-compare' +import React, { PropTypes, PureComponent } from 'react' /** * This HOC decorates a virtualized component and responds to arrow-key events by scrolling one row or column at a time. */ -export default class ArrowKeyStepper extends Component { +export default class ArrowKeyStepper extends PureComponent { static defaultProps = { disabled: false, mode: 'edges', @@ -71,10 +70,6 @@ export default class ArrowKeyStepper extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _onKeyDown (event) { const { columnCount, disabled, mode, rowCount } = this.props diff --git a/source/AutoSizer/AutoSizer.example.js b/source/AutoSizer/AutoSizer.example.js index 31ea8a938..8138017c6 100644 --- a/source/AutoSizer/AutoSizer.example.js +++ b/source/AutoSizer/AutoSizer.example.js @@ -1,13 +1,12 @@ /** @flow */ import Immutable from 'immutable' -import React, { Component, PropTypes } from 'react' +import React, { PropTypes, PureComponent } from 'react' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' import AutoSizer from './AutoSizer' import List from '../List' -import shallowCompare from 'react-addons-shallow-compare' import styles from './AutoSizer.example.css' -export default class AutoSizerExample extends Component { +export default class AutoSizerExample extends PureComponent { static contextTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired } @@ -78,10 +77,6 @@ export default class AutoSizerExample extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _rowRenderer ({ index, key, style }) { const { list } = this.context const row = list.get(index) diff --git a/source/AutoSizer/AutoSizer.js b/source/AutoSizer/AutoSizer.js index e454db122..f1e6ae967 100644 --- a/source/AutoSizer/AutoSizer.js +++ b/source/AutoSizer/AutoSizer.js @@ -1,6 +1,5 @@ /** @flow */ -import React, { Component, PropTypes } from 'react' -import shallowCompare from 'react-addons-shallow-compare' +import React, { PropTypes, PureComponent } from 'react' import createDetectElementResize from '../vendor/detectElementResize' /** @@ -8,7 +7,7 @@ import createDetectElementResize from '../vendor/detectElementResize' * Child component should not be declared as a child but should rather be specified by a `ChildComponent` property. * All other properties will be passed through to the child component. */ -export default class AutoSizer extends Component { +export default class AutoSizer extends PureComponent { static propTypes = { /** * Function responsible for rendering children. @@ -90,10 +89,6 @@ export default class AutoSizer extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _onResize () { const { onResize } = this.props diff --git a/source/CellMeasurer/CellMeasurer.example.js b/source/CellMeasurer/CellMeasurer.example.js index b316be628..4ce00408d 100644 --- a/source/CellMeasurer/CellMeasurer.example.js +++ b/source/CellMeasurer/CellMeasurer.example.js @@ -1,6 +1,6 @@ /** @flow */ import Immutable from 'immutable' -import React, { Component, PropTypes } from 'react' +import React, { PropTypes, PureComponent } from 'react' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' import AutoSizer from '../AutoSizer' import CellMeasurer from './CellMeasurer' @@ -8,9 +8,7 @@ import CellMeasurerCache from './CellMeasurerCache' import Grid from '../Grid' import List from '../List' import { Column, Table } from '../Table' -import shallowCompare from 'react-addons-shallow-compare' import cn from 'classnames' -import { findDOMNode } from 'react-dom' import styles from './CellMeasurer.example.css' const COLUMN_COUNT = 50 @@ -19,7 +17,7 @@ const HEIGHT = 400 const ROW_COUNT = 50 const ROW_HEIGHT = 35 -export default class CellMeasurerExample extends Component { +export default class CellMeasurerExample extends PureComponent { static contextTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired } @@ -83,10 +81,6 @@ export default class CellMeasurerExample extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _onClick (id) { this.setState({ currentTab: id @@ -128,7 +122,7 @@ function Tab ({ children, currentTab, id, onClick }) { ) } -class DynamicWidthGrid extends Component { +class DynamicWidthGrid extends PureComponent { static propTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired, width: PropTypes.number.isRequired @@ -165,10 +159,6 @@ class DynamicWidthGrid extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _cellRenderer ({ columnIndex, key, parent, rowIndex, style }) { const { list } = this.props @@ -199,7 +189,7 @@ class DynamicWidthGrid extends Component { } } -class DynamiHeightGrid extends Component { +class DynamiHeightGrid extends PureComponent { static propTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired, width: PropTypes.number.isRequired @@ -236,10 +226,6 @@ class DynamiHeightGrid extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _cellRenderer ({ columnIndex, key, parent, rowIndex, style }) { const { list } = this.props @@ -269,7 +255,7 @@ class DynamiHeightGrid extends Component { } } -class DynamicHeightList extends Component { +class DynamicHeightList extends PureComponent { static propTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired, width: PropTypes.number.isRequired @@ -303,10 +289,6 @@ class DynamicHeightList extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _rowRenderer ({ index, isScrolling, key, parent, style }) { const { list } = this.props @@ -318,7 +300,7 @@ class DynamicHeightList extends Component { const source = `http://lorempixel.com/${imageWidth}/${imageHeight}/` - return ( + return ( , + invalidateCellSizeAfterRender = jest.fn() }) { render( -
+ {children} ) } @@ -51,7 +52,7 @@ function renderHelper ({ describe('CellMeasurer', () => { it('componentDidMount() should measure content that is not already in the cache', () => { const cache = new CellMeasurerCache() - const invalidateGridSizeAfterRender = jest.fn() + const invalidateCellSizeAfterRender = jest.fn() mockClientWidthAndHeight({ height: 20, @@ -65,9 +66,9 @@ describe('CellMeasurer', () => { expect(offsetWidthMock.mock.calls).toHaveLength(0) expect(cache.has(0, 0)).toBe(false) - renderHelper({ cache, invalidateGridSizeAfterRender }) + renderHelper({ cache, invalidateCellSizeAfterRender }) - expect(invalidateGridSizeAfterRender).toHaveBeenCalled() + expect(invalidateCellSizeAfterRender).toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(1) expect(offsetWidthMock.mock.calls).toHaveLength(1) expect(cache.has(0, 0)).toBe(true) @@ -79,7 +80,7 @@ describe('CellMeasurer', () => { const cache = new CellMeasurerCache() cache.set(0, 0, 100, 20) - const invalidateGridSizeAfterRender = jest.fn() + const invalidateCellSizeAfterRender = jest.fn() mockClientWidthAndHeight({ height: 20, @@ -88,24 +89,24 @@ describe('CellMeasurer', () => { expect(cache.has(0, 0)).toBe(true) - renderHelper({ cache, invalidateGridSizeAfterRender }) + renderHelper({ cache, invalidateCellSizeAfterRender }) const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get - expect(invalidateGridSizeAfterRender).not.toHaveBeenCalled() + expect(invalidateCellSizeAfterRender).not.toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(0) expect(offsetWidthMock.mock.calls).toHaveLength(0) }) it('componentDidUpdate() should measure content that is not already in the cache', () => { const cache = new CellMeasurerCache() - const invalidateGridSizeAfterRender = jest.fn() + const invalidateCellSizeAfterRender = jest.fn() - renderHelper({ cache, invalidateGridSizeAfterRender }) + renderHelper({ cache, invalidateCellSizeAfterRender }) cache.clear(0, 0) - invalidateGridSizeAfterRender.mockReset() + invalidateCellSizeAfterRender.mockReset() expect(cache.has(0, 0)).toBe(false) expect(cache.getWidth(0, 0)).toBe(DEFAULT_WIDTH) @@ -119,11 +120,11 @@ describe('CellMeasurer', () => { const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get - renderHelper({ cache, invalidateGridSizeAfterRender }) + renderHelper({ cache, invalidateCellSizeAfterRender }) expect(cache.has(0, 0)).toBe(true) - expect(invalidateGridSizeAfterRender).toHaveBeenCalled() + expect(invalidateCellSizeAfterRender).toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(1) expect(offsetWidthMock.mock.calls).toHaveLength(1) expect(cache.getWidth(0, 0)).toBe(100) @@ -134,7 +135,7 @@ describe('CellMeasurer', () => { const cache = new CellMeasurerCache() cache.set(0, 0, 100, 20) - const invalidateGridSizeAfterRender = jest.fn() + const invalidateCellSizeAfterRender = jest.fn() expect(cache.has(0, 0)).toBe(true) @@ -143,14 +144,27 @@ describe('CellMeasurer', () => { width: 100 }) - renderHelper({ cache, invalidateGridSizeAfterRender }) - renderHelper({ cache, invalidateGridSizeAfterRender }) + renderHelper({ cache, invalidateCellSizeAfterRender }) + renderHelper({ cache, invalidateCellSizeAfterRender }) const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get - expect(invalidateGridSizeAfterRender).not.toHaveBeenCalled() + expect(invalidateCellSizeAfterRender).not.toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(0) expect(offsetWidthMock.mock.calls).toHaveLength(0) }) + + it('componentDidUpdate() should pass a :measure param to a function child', () => { + const cache = new CellMeasurerCache() + + const children = jest.fn() + children.mockReturnValue(
) + + renderHelper({ cache, children }) + + expect(children).toHaveBeenCalled() + + console.log(children.mock.calls) + }) }) diff --git a/source/CellMeasurer/CellMeasurer.js b/source/CellMeasurer/CellMeasurer.js index 4a54b6157..9433071e8 100644 --- a/source/CellMeasurer/CellMeasurer.js +++ b/source/CellMeasurer/CellMeasurer.js @@ -1,6 +1,5 @@ /** @flow */ -import { Component } from 'react' -import shallowCompare from 'react-addons-shallow-compare' +import { PureComponent } from 'react' import { findDOMNode } from 'react-dom' type Props = { @@ -16,7 +15,7 @@ type Props = { * Measurements are stored in a per-cell cache. * Cached-content is not be re-measured. */ -export default class CellMeasurer extends Component { +export default class CellMeasurer extends PureComponent { props: Props; constructor (props, context) { @@ -35,6 +34,7 @@ export default class CellMeasurer extends Component { render () { const { children } = this.props + // @TODO (bvaughn) __DEV__ mode check for parent.props.deferredMeasurementCache return typeof children === 'function' @@ -42,10 +42,6 @@ export default class CellMeasurer extends Component { : children } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _maybeMeasureCell () { const { cache, columnIndex, parent, rowIndex } = this.props @@ -62,7 +58,7 @@ export default class CellMeasurer extends Component { ) // If size has changed, let Grid know to re-render. - parent.invalidateGridSizeAfterRender({ + parent.invalidateCellSizeAfterRender({ columnIndex, rowIndex }) diff --git a/source/Collection/Collection.example.js b/source/Collection/Collection.example.js index ed3e8f0d9..d0bcc989b 100644 --- a/source/Collection/Collection.example.js +++ b/source/Collection/Collection.example.js @@ -1,11 +1,10 @@ /** @flow */ -import React, { Component, PropTypes } from 'react' +import React, { PropTypes, PureComponent } from 'react' import Immutable from 'immutable' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' import { LabeledInput, InputRow } from '../demo/LabeledInput' import AutoSizer from '../AutoSizer' import Collection from './Collection' -import shallowCompare from 'react-addons-shallow-compare' import styles from './Collection.example.css' // Defines a pattern of sizes and positions for a range of 10 rotating cells @@ -13,7 +12,7 @@ import styles from './Collection.example.css' const GUTTER_SIZE = 3 const CELL_WIDTH = 75 -export default class CollectionExample extends Component { +export default class CollectionExample extends PureComponent { static contextTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired }; @@ -126,10 +125,6 @@ export default class CollectionExample extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _cellRenderer ({ index, isScrolling, key, style }) { const { list } = this.context const { showScrollingPlaceholder } = this.state diff --git a/source/Collection/Collection.js b/source/Collection/Collection.js index 423500314..a246ac6d8 100644 --- a/source/Collection/Collection.js +++ b/source/Collection/Collection.js @@ -1,15 +1,15 @@ -import React, { Component, PropTypes } from 'react' +/** @flow */ +import React, { PropTypes, PureComponent } from 'react' import CollectionView from './CollectionView' import calculateSizeAndPositionData from './utils/calculateSizeAndPositionData' import getUpdatedOffsetForIndex from '../utils/getUpdatedOffsetForIndex' -import shallowCompare from 'react-addons-shallow-compare' import type { ScrollPosition, SizeInfo } from './types' /** * Renders scattered or non-linear data. * Unlike Grid, which renders checkerboard data, Collection can render arbitrarily positioned- even overlapping- data. */ -export default class Collection extends Component { +export default class Collection extends PureComponent { static propTypes = { 'aria-label': PropTypes.string, @@ -85,10 +85,6 @@ export default class Collection extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - /** CellLayoutManager interface */ calculateSizeAndPositionData () { diff --git a/source/Collection/CollectionView.js b/source/Collection/CollectionView.js index f60418e2a..598c2b0d4 100644 --- a/source/Collection/CollectionView.js +++ b/source/Collection/CollectionView.js @@ -1,9 +1,8 @@ /** @flow */ -import React, { Component, PropTypes } from 'react' +import React, { PropTypes, PureComponent } from 'react' import cn from 'classnames' import createCallbackMemoizer from '../utils/createCallbackMemoizer' import getScrollbarSize from 'dom-helpers/util/scrollbarSize' -import shallowCompare from 'react-addons-shallow-compare' // @TODO It would be nice to refactor Grid to use this code as well. @@ -26,7 +25,7 @@ const SCROLL_POSITION_CHANGE_REASONS = { * Monitors changes in properties (eg. cellCount) and state (eg. scroll offsets) to determine when rendering needs to occur. * This component does not render any visible content itself; it defers to the specified :cellLayoutManager. */ -export default class CollectionView extends Component { +export default class CollectionView extends PureComponent { static propTypes = { 'aria-label': PropTypes.string, @@ -416,10 +415,6 @@ export default class CollectionView extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - /* ---------------------------- Helper methods ---------------------------- */ /** diff --git a/source/ColumnSizer/ColumnSizer.example.js b/source/ColumnSizer/ColumnSizer.example.js index 7b2df742b..e2363428d 100644 --- a/source/ColumnSizer/ColumnSizer.example.js +++ b/source/ColumnSizer/ColumnSizer.example.js @@ -1,16 +1,15 @@ /** * @flow */ -import React, { Component } from 'react' +import React, { PureComponent } from 'react' import styles from './ColumnSizer.example.css' import AutoSizer from '../AutoSizer' import ColumnSizer from './ColumnSizer' import Grid from '../Grid' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' import { LabeledInput, InputRow } from '../demo/LabeledInput' -import shallowCompare from 'react-addons-shallow-compare' -export default class ColumnSizerExample extends Component { +export default class ColumnSizerExample extends PureComponent { constructor (props) { super(props) @@ -106,10 +105,6 @@ export default class ColumnSizerExample extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _noColumnMaxWidthChange (event) { let columnMaxWidth = parseInt(event.target.value, 10) diff --git a/source/ColumnSizer/ColumnSizer.js b/source/ColumnSizer/ColumnSizer.js index 1538ee3eb..fb22b99b3 100644 --- a/source/ColumnSizer/ColumnSizer.js +++ b/source/ColumnSizer/ColumnSizer.js @@ -1,11 +1,10 @@ /** @flow */ -import { Component, PropTypes } from 'react' -import shallowCompare from 'react-addons-shallow-compare' +import { PropTypes, PureComponent } from 'react' /** * High-order component that auto-calculates column-widths for `Grid` cells. */ -export default class ColumnSizer extends Component { +export default class ColumnSizer extends PureComponent { static propTypes = { /** * Function responsible for rendering a virtualized Grid. @@ -86,10 +85,6 @@ export default class ColumnSizer extends Component { }) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _registerChild (child) { if ( child && diff --git a/source/Grid/Grid.example.js b/source/Grid/Grid.example.js index 1de137c1c..95da29550 100644 --- a/source/Grid/Grid.example.js +++ b/source/Grid/Grid.example.js @@ -1,15 +1,14 @@ /** @flow */ import Immutable from 'immutable' -import React, { Component, PropTypes } from 'react' +import React, { PropTypes, PureComponent } from 'react' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' import { LabeledInput, InputRow } from '../demo/LabeledInput' import AutoSizer from '../AutoSizer' import Grid from './Grid' -import shallowCompare from 'react-addons-shallow-compare' import cn from 'classnames' import styles from './Grid.example.css' -export default class GridExample extends Component { +export default class GridExample extends PureComponent { static contextTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired }; @@ -158,10 +157,6 @@ export default class GridExample extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _cellRenderer ({ columnIndex, key, rowIndex, style }) { if (columnIndex === 0) { return this._renderLeftSideCell({ columnIndex, key, rowIndex, style }) @@ -243,6 +238,9 @@ export default class GridExample extends Component { const classNames = cn(styles.cell, styles.letterCell) + // Don't modify styles. + // These are frozen by React now. + // Since Grid caches and re-uses them, they aren't safe to modify. style = { ...style, backgroundColor: datum.color diff --git a/source/Grid/Grid.jest.js b/source/Grid/Grid.jest.js index 5ee3085ce..7374e4556 100644 --- a/source/Grid/Grid.jest.js +++ b/source/Grid/Grid.jest.js @@ -3,7 +3,6 @@ import React from 'react' import { findDOMNode } from 'react-dom' import { Simulate } from 'react-addons-test-utils' import { render } from '../TestUtils' -import shallowCompare from 'react-addons-shallow-compare' import Grid, { DEFAULT_SCROLLING_RESET_TIME_INTERVAL } from './Grid' import { SCROLL_DIRECTION_BACKWARD, SCROLL_DIRECTION_FORWARD } from './utils/getOverscanIndices' import { DEFAULT_MAX_SCROLL_SIZE } from './utils/ScalingCellSizeAndPositionManager' @@ -1394,14 +1393,10 @@ describe('Grid', () => { expect(cellRendererCalled).toEqual(false) }) - it('should not re-render grid components if they shallowCompare style', () => { + it('should not re-render grid components if they extend PureComponent', () => { let componentUpdates = 0 - class GridComponent extends React.Component { - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - + class GridComponent extends React.PureComponent { componentDidUpdate () { componentUpdates++ } diff --git a/source/Grid/Grid.js b/source/Grid/Grid.js index b87451b68..6a00cc996 100644 --- a/source/Grid/Grid.js +++ b/source/Grid/Grid.js @@ -1,13 +1,11 @@ /** @flow */ -import React, { Component, PropTypes } from 'react' +import React, { PropTypes, PureComponent } from 'react' import cn from 'classnames' import calculateSizeAndPositionDataAndUpdateScrollOffset from './utils/calculateSizeAndPositionDataAndUpdateScrollOffset' -// @TODO (bvaughn) import ScalingCellSizeAndPositionManager from './utils/ScalingCellSizeAndPositionManager' -import CellSizeAndPositionManager from './utils/CellSizeAndPositionManager' +import ScalingCellSizeAndPositionManager from './utils/ScalingCellSizeAndPositionManager' import createCallbackMemoizer from '../utils/createCallbackMemoizer' import getOverscanIndices, { SCROLL_DIRECTION_BACKWARD, SCROLL_DIRECTION_FORWARD } from './utils/getOverscanIndices' import getScrollbarSize from 'dom-helpers/util/scrollbarSize' -import shallowCompare from 'react-addons-shallow-compare' import updateScrollIndexHelper from './utils/updateScrollIndexHelper' import defaultCellRangeRenderer from './defaultCellRangeRenderer' @@ -30,7 +28,7 @@ const SCROLL_POSITION_CHANGE_REASONS = { * Renders tabular data with virtualization along the vertical and horizontal axes. * Row heights and column widths must be known ahead of time and specified as properties. */ -export default class Grid extends Component { +export default class Grid extends PureComponent { static propTypes = { 'aria-label': PropTypes.string, @@ -241,13 +239,13 @@ export default class Grid extends Component { const deferredMeasurementCache = props.deferredMeasurementCache const deferredMode = typeof deferredMeasurementCache !== 'undefined' - this._columnSizeAndPositionManager = new CellSizeAndPositionManager({ // @TODO (bvaughn) Use scaling impl + this._columnSizeAndPositionManager = new ScalingCellSizeAndPositionManager({ batchAllCells: deferredMode && !deferredMeasurementCache.hasFixedHeight(), cellCount: props.columnCount, cellSizeGetter: (params) => this._columnWidthGetter(params), estimatedCellSize: this._getEstimatedColumnSize(props) }) - this._rowSizeAndPositionManager = new CellSizeAndPositionManager({ // @TODO (bvaughn) Use scaling impl + this._rowSizeAndPositionManager = new ScalingCellSizeAndPositionManager({ batchAllCells: deferredMode && !deferredMeasurementCache.hasFixedWidth(), cellCount: props.rowCount, cellSizeGetter: (params) => this._rowHeightGetter(params), @@ -266,7 +264,7 @@ export default class Grid extends Component { * This method is intended for advanced use-cases like CellMeasurer. */ // @TODO (bvaughn) Add automated test coverage for this. - invalidateGridSizeAfterRender ({ + invalidateCellSizeAfterRender ({ columnIndex, rowIndex }) { @@ -655,10 +653,6 @@ export default class Grid extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - /* ---------------------------- Helper methods ---------------------------- */ _calculateChildrenToRender (props = this.props, state = this.state) { diff --git a/source/Grid/utils/CellSizeAndPositionManager.js b/source/Grid/utils/CellSizeAndPositionManager.js index 7d6ebaf75..cc86abc85 100644 --- a/source/Grid/utils/CellSizeAndPositionManager.js +++ b/source/Grid/utils/CellSizeAndPositionManager.js @@ -166,8 +166,6 @@ export default class CellSizeAndPositionManager { getVisibleCellRange (params: GetVisibleCellRangeParams): VisibleCellRange { // Advanced use-cases (eg CellMeasurer) require batched measurements to determine accurate sizes. // eg we can't know a row's height without measuring the height of all columns within that row. - // @TODO (bvaughn) We could use a CellMeasurerCache method to determine if a given cell had been measured, - // by querying all perpendicular cells. But is it worth it? Maybe just a hard-limitation. if (this._batchAllCells) { return { start: 0, diff --git a/source/InfiniteLoader/InfiniteLoader.example.js b/source/InfiniteLoader/InfiniteLoader.example.js index 1f96246f5..ef4c7e1ea 100644 --- a/source/InfiniteLoader/InfiniteLoader.example.js +++ b/source/InfiniteLoader/InfiniteLoader.example.js @@ -1,17 +1,16 @@ /** @flow */ -import React, { Component, PropTypes } from 'react' +import React, { PropTypes, PureComponent } from 'react' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' import Immutable from 'immutable' import AutoSizer from '../AutoSizer' import InfiniteLoader from './InfiniteLoader' import List from '../List' -import shallowCompare from 'react-addons-shallow-compare' import styles from './InfiniteLoader.example.css' const STATUS_LOADING = 1 const STATUS_LOADED = 2 -export default class InfiniteLoaderExample extends Component { +export default class InfiniteLoaderExample extends PureComponent { static contextTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired } @@ -99,10 +98,6 @@ export default class InfiniteLoaderExample extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _clearData () { this.setState({ loadedRowCount: 0, diff --git a/source/InfiniteLoader/InfiniteLoader.js b/source/InfiniteLoader/InfiniteLoader.js index 69abc61fe..acb14481d 100644 --- a/source/InfiniteLoader/InfiniteLoader.js +++ b/source/InfiniteLoader/InfiniteLoader.js @@ -1,6 +1,5 @@ /** @flow */ -import { Component, PropTypes } from 'react' -import shallowCompare from 'react-addons-shallow-compare' +import { PropTypes, PureComponent } from 'react' import createCallbackMemoizer from '../utils/createCallbackMemoizer' /** @@ -8,7 +7,7 @@ import createCallbackMemoizer from '../utils/createCallbackMemoizer' * This component decorates a virtual component and just-in-time prefetches rows as a user scrolls. * It is intended as a convenience component; fork it if you'd like finer-grained control over data-loading. */ -export default class InfiniteLoader extends Component { +export default class InfiniteLoader extends PureComponent { static propTypes = { /** * Function responsible for rendering a virtualized component. @@ -78,10 +77,6 @@ export default class InfiniteLoader extends Component { }) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _loadUnloadedRanges (unloadedRanges) { const { loadMoreRows } = this.props diff --git a/source/List/List.example.js b/source/List/List.example.js index 81dd9cd6a..3e22ea559 100644 --- a/source/List/List.example.js +++ b/source/List/List.example.js @@ -3,15 +3,14 @@ */ import cn from 'classnames' import Immutable from 'immutable' -import React, { Component, PropTypes } from 'react' +import React, { PropTypes, PureComponent } from 'react' import styles from './List.example.css' import AutoSizer from '../AutoSizer' import List from './List' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' import { LabeledInput, InputRow } from '../demo/LabeledInput' -import shallowCompare from 'react-addons-shallow-compare' -export default class ListExample extends Component { +export default class ListExample extends PureComponent { static contextTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired }; @@ -141,10 +140,6 @@ export default class ListExample extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _getDatum (index) { const { list } = this.context diff --git a/source/List/List.js b/source/List/List.js index 945c2b4fd..743b2ca3f 100644 --- a/source/List/List.js +++ b/source/List/List.js @@ -1,8 +1,7 @@ /** @flow */ import Grid from '../Grid' -import React, { Component, PropTypes } from 'react' +import React, { PropTypes, PureComponent } from 'react' import cn from 'classnames' -import shallowCompare from 'react-addons-shallow-compare' /** * It is inefficient to create and manage a large list of DOM elements within a scrolling container @@ -12,7 +11,7 @@ import shallowCompare from 'react-addons-shallow-compare' * * This component renders a virtualized list of elements with either fixed or dynamic heights. */ -export default class List extends Component { +export default class List extends PureComponent { static propTypes = { 'aria-label': PropTypes.string, @@ -157,10 +156,6 @@ export default class List extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _cellRenderer ({ rowIndex, style, ...rest }) { const { rowRenderer } = this.props diff --git a/source/MultiGrid/MultiGrid.example.js b/source/MultiGrid/MultiGrid.example.js index b815a8792..c796a4c12 100644 --- a/source/MultiGrid/MultiGrid.example.js +++ b/source/MultiGrid/MultiGrid.example.js @@ -1,11 +1,10 @@ /** @flow */ import Immutable from 'immutable' -import React, { Component, PropTypes } from 'react' +import React, { PropTypes, PureComponent } from 'react' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' import { LabeledInput, InputRow } from '../demo/LabeledInput' import AutoSizer from '../AutoSizer' import MultiGrid from './MultiGrid' -import shallowCompare from 'react-addons-shallow-compare' import styles from './MultiGrid.example.css' const STYLE = { @@ -26,7 +25,7 @@ const STYLE_TOP_RIGHT_GRID = { fontWeight: 'bold' } -export default class MultiGridExample extends Component { +export default class MultiGridExample extends PureComponent { static contextTypes = { list: PropTypes.instanceOf(Immutable.List).isRequired }; @@ -90,10 +89,6 @@ export default class MultiGridExample extends Component { ) } - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } - _cellRenderer ({ columnIndex, key, rowIndex, style }) { return (
) } - - shouldComponentUpdate (nextProps, nextState) { - return shallowCompare(this, nextProps, nextState) - } } diff --git a/webpack.config.umd.js b/webpack.config.umd.js index 6603d7bcc..fb1439ef7 100644 --- a/webpack.config.umd.js +++ b/webpack.config.umd.js @@ -15,8 +15,7 @@ module.exports = { }, externals: { 'react': 'React', - 'react-dom': 'ReactDOM', - 'react-addons-shallow-compare': 'var React.addons.shallowCompare' + 'react-dom': 'ReactDOM' }, plugins: [ new ExtractTextPlugin('../styles.css', { diff --git a/yarn.lock b/yarn.lock index 8a4a23c1f..566fb02e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4586,13 +4586,6 @@ rc@~1.1.6: minimist "^1.2.0" strip-json-comments "~1.0.4" -react-addons-shallow-compare@16.0.0-alpha.2: - version "16.0.0-alpha.2" - resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-16.0.0-alpha.2.tgz#a5848da5c577e9c330bff8fcbf3ec6f9885227eb" - dependencies: - fbjs "^0.8.9" - object-assign "^4.1.0" - react-addons-test-utils@16.0.0-alpha.2: version "16.0.0-alpha.2" resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-16.0.0-alpha.2.tgz#cd1a32fea6511110617dd165f199ce4a424b868f" From 4fc699918ce58c47e21c05be3ce03ca0b763463a Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 13 Feb 2017 16:33:06 -0800 Subject: [PATCH 09/31] Collection.forceUpdate() calls inner CollectionView.forceUpdate() Resolves #568 --- source/Collection/Collection.jest.js | 15 +++++++++++++++ source/Collection/Collection.js | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/source/Collection/Collection.jest.js b/source/Collection/Collection.jest.js index 379c4c196..38fa9405b 100644 --- a/source/Collection/Collection.jest.js +++ b/source/Collection/Collection.jest.js @@ -719,4 +719,19 @@ describe('Collection', () => { done() }) }) + + // See issue #568 for more + it('forceUpdate will also forceUpdate the inner CollectionView', () => { + const cellRenderer = jest.fn() + cellRenderer.mockImplementation(({ key }) =>
) + + const rendered = render(getMarkup({ cellRenderer })) + + expect(cellRenderer).toHaveBeenCalled() + + cellRenderer.mockReset() + rendered.forceUpdate() + + expect(cellRenderer).toHaveBeenCalled() + }) }) diff --git a/source/Collection/Collection.js b/source/Collection/Collection.js index a246ac6d8..df293d024 100644 --- a/source/Collection/Collection.js +++ b/source/Collection/Collection.js @@ -64,6 +64,12 @@ export default class Collection extends PureComponent { this._setCollectionViewRef = this._setCollectionViewRef.bind(this) } + forceUpdate () { + if (this._collectionView !== undefined) { + this._collectionView.forceUpdate() + } + } + /** See Collection#recomputeCellSizesAndPositions */ recomputeCellSizesAndPositions () { this._cellCache = [] From 8482ad87d556f306125673c9bd847b2e8d5cb179 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 13 Feb 2017 16:48:29 -0800 Subject: [PATCH 10/31] Added forceUpdateGrids() to MultiGrid --- docs/MultiGrid.md | 14 ++++++++++++++ source/MultiGrid/MultiGrid.jest.js | 22 ++++++++++++++++++++++ source/MultiGrid/MultiGrid.js | 7 +++++++ 3 files changed, 43 insertions(+) diff --git a/docs/MultiGrid.md b/docs/MultiGrid.md index b4faef6ae..38db0e0e8 100644 --- a/docs/MultiGrid.md +++ b/docs/MultiGrid.md @@ -18,6 +18,20 @@ Some properties (eg `columnCount`, `rowCount`) are adjusted slightly to supporte | styleTopLeftGrid | object | | Optional custom inline style to attach to top-left `Grid` element. | | styleTopRightGrid | object | | Optional custom inline style to attach to top-right `Grid` element. | +### Public Methods + +##### forceUpdateGrids + +Pass-thru that calls `forceUpdate` on all child `Grid`s. + +##### measureAllCells + +Pass-thru that calls `measureAllCells` on all child `Grid`s. + +##### recomputeGridSize + +Pass-thru that calls `recomputeGridSize` on all child `Grid`s. + ### Examples ```jsx diff --git a/source/MultiGrid/MultiGrid.jest.js b/source/MultiGrid/MultiGrid.jest.js index 0ade6728b..1abf5e280 100644 --- a/source/MultiGrid/MultiGrid.jest.js +++ b/source/MultiGrid/MultiGrid.jest.js @@ -206,6 +206,28 @@ describe('MultiGrid', () => { }) }) + describe('#forceUpdateGrids', () => { + it('should call forceUpdate() on inner Grids', () => { + const cellRenderer = jest.fn() + cellRenderer.mockImplementation(({ key }) =>
) + + const rendered = render(getMarkup({ + cellRenderer, + columnCount: 2, + fixedColumnCount: 1, + fixedRowCount: 1, + rowCount: 2 + })) + + expect(cellRenderer.mock.calls).toHaveLength(4) + + cellRenderer.mockReset() + rendered.forceUpdateGrids() + + expect(cellRenderer.mock.calls).toHaveLength(4) + }) + }) + describe('styles', () => { it('should support custom style for the outer MultiGrid wrapper element', () => { const rendered = findDOMNode(render(getMarkup({ diff --git a/source/MultiGrid/MultiGrid.js b/source/MultiGrid/MultiGrid.js index fca98d557..0ea241d98 100644 --- a/source/MultiGrid/MultiGrid.js +++ b/source/MultiGrid/MultiGrid.js @@ -50,6 +50,13 @@ export default class MultiGrid extends PureComponent { this._topRightGridRef = this._topRightGridRef.bind(this) } + forceUpdateGrids () { + this._bottomLeftGrid && this._bottomLeftGrid.forceUpdate() + this._bottomRightGrid && this._bottomRightGrid.forceUpdate() + this._topLeftGrid && this._topLeftGrid.forceUpdate() + this._topRightGrid && this._topRightGrid.forceUpdate() + } + /** See Grid#measureAllCells */ measureAllCells () { this._bottomLeftGrid && this._bottomLeftGrid.measureAllCells() From e78036ab3b7a90a376c710b59d07b456d46422b8 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 13 Feb 2017 18:15:54 -0800 Subject: [PATCH 11/31] Fix compressed Grid offset adjustment bug Resolves #576 --- source/Grid/defaultCellRangeRenderer.js | 14 ++++++++++++-- source/Grid/utils/CellSizeAndPositionManager.js | 4 ++++ .../utils/ScalingCellSizeAndPositionManager.js | 4 ++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/source/Grid/defaultCellRangeRenderer.js b/source/Grid/defaultCellRangeRenderer.js index c615c183c..5f04650e1 100644 --- a/source/Grid/defaultCellRangeRenderer.js +++ b/source/Grid/defaultCellRangeRenderer.js @@ -26,8 +26,18 @@ export default function defaultCellRangeRenderer ({ const deferredMode = typeof deferredMeasurementCache !== 'undefined' const renderedCells = [] - const offsetAdjusted = verticalOffsetAdjustment || horizontalOffsetAdjustment - const canCacheStyle = !isScrolling || !offsetAdjusted + + // Browsers have native size limits for elements (eg Chrome 33M pixels, IE 1.5M pixes). + // User cannot scroll beyond these size limitations. + // In order to work around this, ScalingCellSizeAndPositionManager compresses offsets. + // We should never cache styles for compressed offsets though as this can lead to bugs. + // See issue #576 for more. + const areOffsetsAdjusted = ( + columnSizeAndPositionManager.areOffsetsAdjusted() || + rowSizeAndPositionManager.areOffsetsAdjusted() + ) + + const canCacheStyle = !isScrolling || !areOffsetsAdjusted for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) { let rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex) diff --git a/source/Grid/utils/CellSizeAndPositionManager.js b/source/Grid/utils/CellSizeAndPositionManager.js index cc86abc85..648b22d33 100644 --- a/source/Grid/utils/CellSizeAndPositionManager.js +++ b/source/Grid/utils/CellSizeAndPositionManager.js @@ -26,6 +26,10 @@ export default class CellSizeAndPositionManager { this._lastBatchedIndex = -1 } + areOffsetsAdjusted (): bool { + return false + } + configure ({ cellCount, estimatedCellSize diff --git a/source/Grid/utils/ScalingCellSizeAndPositionManager.js b/source/Grid/utils/ScalingCellSizeAndPositionManager.js index b5dc4ee87..769f60353 100644 --- a/source/Grid/utils/ScalingCellSizeAndPositionManager.js +++ b/source/Grid/utils/ScalingCellSizeAndPositionManager.js @@ -21,6 +21,10 @@ export default class ScalingCellSizeAndPositionManager { this._maxScrollSize = maxScrollSize } + areOffsetsAdjusted (): bool { + return this._cellSizeAndPositionManager.getTotalSize() > this._maxScrollSize + } + configure (params): void { this._cellSizeAndPositionManager.configure(params) } From 4be96bc4cfe09385a919a7b449f16af3bf5f0269 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 13 Feb 2017 21:50:32 -0800 Subject: [PATCH 12/31] Added warning for Grid/CellMeasurer improper usage --- package.json | 3 +- source/CellMeasurer/CellMeasurer.jest.js | 85 ++++++++++++++++++------ source/CellMeasurer/CellMeasurer.js | 27 ++++++-- source/Collection/CollectionView.js | 2 +- source/Grid/Grid.example.js | 2 +- 5 files changed, 89 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 3bfa54d3d..29a214268 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,8 @@ "fit", "getComputedStyle", "it", - "jest" + "jest", + "spyOn" ] }, "jest": { diff --git a/source/CellMeasurer/CellMeasurer.jest.js b/source/CellMeasurer/CellMeasurer.jest.js index f70852631..66369a6e5 100644 --- a/source/CellMeasurer/CellMeasurer.jest.js +++ b/source/CellMeasurer/CellMeasurer.jest.js @@ -29,18 +29,28 @@ function mockClientWidthAndHeight ({ ) } -function renderHelper ({ +function createParent ({ cache, - children =
, invalidateCellSizeAfterRender = jest.fn() -}) { +} = {}) { + return { + invalidateCellSizeAfterRender, + props: { + deferredMeasurementCache: cache + } + } +} + +function renderHelper ({ + cache = new CellMeasurerCache(), + children =
, + parent +} = {}) { render( @@ -52,7 +62,7 @@ function renderHelper ({ describe('CellMeasurer', () => { it('componentDidMount() should measure content that is not already in the cache', () => { const cache = new CellMeasurerCache() - const invalidateCellSizeAfterRender = jest.fn() + const parent = createParent({ cache }) mockClientWidthAndHeight({ height: 20, @@ -66,9 +76,9 @@ describe('CellMeasurer', () => { expect(offsetWidthMock.mock.calls).toHaveLength(0) expect(cache.has(0, 0)).toBe(false) - renderHelper({ cache, invalidateCellSizeAfterRender }) + renderHelper({ cache, parent }) - expect(invalidateCellSizeAfterRender).toHaveBeenCalled() + expect(parent.invalidateCellSizeAfterRender).toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(1) expect(offsetWidthMock.mock.calls).toHaveLength(1) expect(cache.has(0, 0)).toBe(true) @@ -80,7 +90,7 @@ describe('CellMeasurer', () => { const cache = new CellMeasurerCache() cache.set(0, 0, 100, 20) - const invalidateCellSizeAfterRender = jest.fn() + const parent = createParent({ cache }) mockClientWidthAndHeight({ height: 20, @@ -89,24 +99,24 @@ describe('CellMeasurer', () => { expect(cache.has(0, 0)).toBe(true) - renderHelper({ cache, invalidateCellSizeAfterRender }) + renderHelper({ cache, parent }) const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get - expect(invalidateCellSizeAfterRender).not.toHaveBeenCalled() + expect(parent.invalidateCellSizeAfterRender).not.toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(0) expect(offsetWidthMock.mock.calls).toHaveLength(0) }) it('componentDidUpdate() should measure content that is not already in the cache', () => { const cache = new CellMeasurerCache() - const invalidateCellSizeAfterRender = jest.fn() + const parent = createParent({ cache }) - renderHelper({ cache, invalidateCellSizeAfterRender }) + renderHelper({ cache, parent }) cache.clear(0, 0) - invalidateCellSizeAfterRender.mockReset() + parent.invalidateCellSizeAfterRender.mockReset() expect(cache.has(0, 0)).toBe(false) expect(cache.getWidth(0, 0)).toBe(DEFAULT_WIDTH) @@ -120,11 +130,11 @@ describe('CellMeasurer', () => { const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get - renderHelper({ cache, invalidateCellSizeAfterRender }) + renderHelper({ cache, parent }) expect(cache.has(0, 0)).toBe(true) - expect(invalidateCellSizeAfterRender).toHaveBeenCalled() + expect(parent.invalidateCellSizeAfterRender).toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(1) expect(offsetWidthMock.mock.calls).toHaveLength(1) expect(cache.getWidth(0, 0)).toBe(100) @@ -135,7 +145,7 @@ describe('CellMeasurer', () => { const cache = new CellMeasurerCache() cache.set(0, 0, 100, 20) - const invalidateCellSizeAfterRender = jest.fn() + const parent = createParent({ cache }) expect(cache.has(0, 0)).toBe(true) @@ -144,13 +154,13 @@ describe('CellMeasurer', () => { width: 100 }) - renderHelper({ cache, invalidateCellSizeAfterRender }) - renderHelper({ cache, invalidateCellSizeAfterRender }) + renderHelper({ cache, parent }) + renderHelper({ cache, parent }) const offsetHeightMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetHeight').get const offsetWidthMock = Object.getOwnPropertyDescriptor(Element.prototype, 'offsetWidth').get - expect(invalidateCellSizeAfterRender).not.toHaveBeenCalled() + expect(parent.invalidateCellSizeAfterRender).not.toHaveBeenCalled() expect(offsetHeightMock.mock.calls).toHaveLength(0) expect(offsetWidthMock.mock.calls).toHaveLength(0) }) @@ -165,6 +175,37 @@ describe('CellMeasurer', () => { expect(children).toHaveBeenCalled() - console.log(children.mock.calls) + const params = children.mock.calls[0][0] + + expect(typeof params.measure === 'function').toBe(true) + }) + + it('should still update cache without a parent Grid', () => { + spyOn(console, 'warn') + + mockClientWidthAndHeight({ + height: 20, + width: 100 + }) + + const cache = new CellMeasurerCache() + + renderHelper({ cache }) // No parent Grid + + expect(cache.has(0, 0)).toBe(true) + + expect(console.warn).not.toHaveBeenCalled() + }) + + it('should error if parent Grid does not specify a :deferredMeasurementCache prop', () => { + spyOn(console, 'warn') + + const parent = createParent() // Parent Grid with no deferredMeasurementCache prop + + renderHelper({ parent }) + + expect(console.warn).toHaveBeenCalledWith( + 'CellMeasurer should be rendered within a Grid that has a deferredMeasurementCache prop' + ) }) }) diff --git a/source/CellMeasurer/CellMeasurer.js b/source/CellMeasurer/CellMeasurer.js index 9433071e8..746ae6b45 100644 --- a/source/CellMeasurer/CellMeasurer.js +++ b/source/CellMeasurer/CellMeasurer.js @@ -10,6 +10,17 @@ type Props = { rowIndex: number }; +function warnAboutImproperUse (parent) { + if (process.env.NODE_ENV !== 'production') { + if ( + parent && + parent.props.deferredMeasurementCache === undefined + ) { + console.warn('CellMeasurer should be rendered within a Grid that has a deferredMeasurementCache prop') + } + } +} + /** * Wraps a cell and measures its rendered content. * Measurements are stored in a per-cell cache. @@ -35,7 +46,11 @@ export default class CellMeasurer extends PureComponent { render () { const { children } = this.props - // @TODO (bvaughn) __DEV__ mode check for parent.props.deferredMeasurementCache + if (process.env.NODE_ENV !== 'production') { + const { parent } = this.props + + warnAboutImproperUse(parent) + } return typeof children === 'function' ? children({ measure: this._measure }) @@ -58,10 +73,12 @@ export default class CellMeasurer extends PureComponent { ) // If size has changed, let Grid know to re-render. - parent.invalidateCellSizeAfterRender({ - columnIndex, - rowIndex - }) + if (parent !== undefined) { + parent.invalidateCellSizeAfterRender({ + columnIndex, + rowIndex + }) + } } } diff --git a/source/Collection/CollectionView.js b/source/Collection/CollectionView.js index 598c2b0d4..7cd22a666 100644 --- a/source/Collection/CollectionView.js +++ b/source/Collection/CollectionView.js @@ -4,7 +4,7 @@ import cn from 'classnames' import createCallbackMemoizer from '../utils/createCallbackMemoizer' import getScrollbarSize from 'dom-helpers/util/scrollbarSize' -// @TODO It would be nice to refactor Grid to use this code as well. +// @TODO Merge Collection and CollectionView /** * Specifies the number of miliseconds during which to disable pointer events while a scroll is in progress. diff --git a/source/Grid/Grid.example.js b/source/Grid/Grid.example.js index 95da29550..6ae88f13f 100644 --- a/source/Grid/Grid.example.js +++ b/source/Grid/Grid.example.js @@ -239,7 +239,7 @@ export default class GridExample extends PureComponent { const classNames = cn(styles.cell, styles.letterCell) // Don't modify styles. - // These are frozen by React now. + // These are frozen by React now (as of 16.0.0). // Since Grid caches and re-uses them, they aren't safe to modify. style = { ...style, From 13a1bbe922e55babf4dc141e82dd43b81af82fba Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 13 Feb 2017 22:06:03 -0800 Subject: [PATCH 13/31] Added warning for Grid cells rendered without a style prop --- source/CellMeasurer/CellMeasurer.jest.js | 2 +- source/CellMeasurer/CellMeasurer.js | 17 +++++++++++++---- source/Grid/Grid.jest.js | 18 ++++++++++++++++-- source/Grid/defaultCellRangeRenderer.js | 18 ++++++++++++++++++ source/MultiGrid/MultiGrid.jest.js | 2 +- 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/source/CellMeasurer/CellMeasurer.jest.js b/source/CellMeasurer/CellMeasurer.jest.js index 66369a6e5..73bb27996 100644 --- a/source/CellMeasurer/CellMeasurer.jest.js +++ b/source/CellMeasurer/CellMeasurer.jest.js @@ -205,7 +205,7 @@ describe('CellMeasurer', () => { renderHelper({ parent }) expect(console.warn).toHaveBeenCalledWith( - 'CellMeasurer should be rendered within a Grid that has a deferredMeasurementCache prop' + 'CellMeasurer should be rendered within a Grid that has a deferredMeasurementCache prop.' ) }) }) diff --git a/source/CellMeasurer/CellMeasurer.js b/source/CellMeasurer/CellMeasurer.js index 746ae6b45..e474adf44 100644 --- a/source/CellMeasurer/CellMeasurer.js +++ b/source/CellMeasurer/CellMeasurer.js @@ -3,11 +3,12 @@ import { PureComponent } from 'react' import { findDOMNode } from 'react-dom' type Props = { - cache: any, + cache: mixed, children: mixed, columnIndex: number, - parent: any, - rowIndex: number + parent: mixed, + rowIndex: number, + style: mixed }; function warnAboutImproperUse (parent) { @@ -16,11 +17,15 @@ function warnAboutImproperUse (parent) { parent && parent.props.deferredMeasurementCache === undefined ) { - console.warn('CellMeasurer should be rendered within a Grid that has a deferredMeasurementCache prop') + console.warn('CellMeasurer should be rendered within a Grid that has a deferredMeasurementCache prop.') } } } +// Prevent Grid from warning about missing :style prop on CellMeasurer. +// It's understood that style will often be passed to the child instead. +const EMPTY_OBJECT = {} + /** * Wraps a cell and measures its rendered content. * Measurements are stored in a per-cell cache. @@ -29,6 +34,10 @@ function warnAboutImproperUse (parent) { export default class CellMeasurer extends PureComponent { props: Props; + static defaultProps = { + style: EMPTY_OBJECT + } + constructor (props, context) { super(props, context) diff --git a/source/Grid/Grid.jest.js b/source/Grid/Grid.jest.js index 7374e4556..873cdb7b6 100644 --- a/source/Grid/Grid.jest.js +++ b/source/Grid/Grid.jest.js @@ -1381,9 +1381,9 @@ describe('Grid', () => { describe('pure', () => { it('should not re-render unless props have changed', () => { let cellRendererCalled = false - function cellRenderer () { + function cellRenderer ({ key, style }) { cellRendererCalled = true - return 'foo' + return
} const markup = getMarkup({ cellRenderer }) render(markup) @@ -1550,4 +1550,18 @@ describe('Grid', () => { expect(cellRendererCalls.length).toEqual(3) expect(firstProps.style).not.toBe(secondProps.style) }) + + it('should warn about cells that forget to include the style property', () => { + spyOn(console, 'warn') + + function cellRenderer (props) { + return
+ } + + render(getMarkup({ + cellRenderer + })) + + expect(console.warn).toHaveBeenCalledWith('Rendered cell should include style property for positioning.') + }) }) diff --git a/source/Grid/defaultCellRangeRenderer.js b/source/Grid/defaultCellRangeRenderer.js index 5f04650e1..f8a3d8c41 100644 --- a/source/Grid/defaultCellRangeRenderer.js +++ b/source/Grid/defaultCellRangeRenderer.js @@ -127,6 +127,10 @@ export default function defaultCellRangeRenderer ({ continue } + if (process.env.NODE_ENV !== 'production') { + warnAboutMissingStyle(parent, renderedCell) + } + renderedCells.push(renderedCell) } } @@ -134,6 +138,20 @@ export default function defaultCellRangeRenderer ({ return renderedCells } +function warnAboutMissingStyle (parent, renderedCell) { + if (process.env.NODE_ENV !== 'production') { + if ( + renderedCell && + renderedCell.props.style === undefined && + parent.__missingStyleWarning === undefined + ) { + parent.__missingStyleWarning = true + + console.warn('Rendered cell should include style property for positioning.') + } + } +} + type DefaultCellRangeRendererParams = { cellCache: Object, cellRenderer: Function, diff --git a/source/MultiGrid/MultiGrid.jest.js b/source/MultiGrid/MultiGrid.jest.js index 1abf5e280..2aa4289be 100644 --- a/source/MultiGrid/MultiGrid.jest.js +++ b/source/MultiGrid/MultiGrid.jest.js @@ -209,7 +209,7 @@ describe('MultiGrid', () => { describe('#forceUpdateGrids', () => { it('should call forceUpdate() on inner Grids', () => { const cellRenderer = jest.fn() - cellRenderer.mockImplementation(({ key }) =>
) + cellRenderer.mockImplementation(({ key }) =>
) const rendered = render(getMarkup({ cellRenderer, From bb806fc2c25c6cbe686e373e2b8af8d5256beced Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 13 Feb 2017 22:12:59 -0800 Subject: [PATCH 14/31] Prepping for @next --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 29a214268..b86883f13 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,7 @@ "description": "React components for efficiently rendering large, scrollable lists and tabular data", "author": "Brian Vaughn ", "user": "bvaughn", - "version": "8.11.4", - "next": "8.9.0", + "version": "9.0.0-rc.0", "homepage": "https://github.com/bvaughn/react-virtualized", "main": "dist/commonjs/index.js", "module": "dist/es/index.js", From b5b0eb5d8fd7e81db1a43e910d0cfa5c28713fbf Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 14 Feb 2017 14:37:45 -0800 Subject: [PATCH 15/31] Split up CellMeasurer example files for better organization and readability --- .../CellMeasurer.DynamiHeightGrid.example.js | 75 ++++ .../CellMeasurer.DynamicHeightList.example.js | 83 +++++ ...asurer.DynamicHeightTableColumn.example.js | 97 ++++++ .../CellMeasurer.DynamicWidthGrid.example.js | 76 ++++ source/CellMeasurer/CellMeasurer.example.js | 327 +----------------- 5 files changed, 343 insertions(+), 315 deletions(-) create mode 100644 source/CellMeasurer/CellMeasurer.DynamiHeightGrid.example.js create mode 100644 source/CellMeasurer/CellMeasurer.DynamicHeightList.example.js create mode 100644 source/CellMeasurer/CellMeasurer.DynamicHeightTableColumn.example.js create mode 100644 source/CellMeasurer/CellMeasurer.DynamicWidthGrid.example.js diff --git a/source/CellMeasurer/CellMeasurer.DynamiHeightGrid.example.js b/source/CellMeasurer/CellMeasurer.DynamiHeightGrid.example.js new file mode 100644 index 000000000..50f20ca68 --- /dev/null +++ b/source/CellMeasurer/CellMeasurer.DynamiHeightGrid.example.js @@ -0,0 +1,75 @@ +/** @flow */ +import Immutable from 'immutable' +import React, { PropTypes, PureComponent } from 'react' +import CellMeasurer from './CellMeasurer' +import CellMeasurerCache from './CellMeasurerCache' +import Grid from '../Grid' +import styles from './CellMeasurer.example.css' + +export default class DynamiHeightGrid extends PureComponent { + static propTypes = { + getClassName: PropTypes.func.isRequired, + getContent: PropTypes.func.isRequired, + list: PropTypes.instanceOf(Immutable.List).isRequired, + width: PropTypes.number.isRequired + } + + constructor (props, context) { + super(props, context) + + this._cache = new CellMeasurerCache({ + defaultWidth: 150, + fixedWidth: true + }) + + this._cellRenderer = this._cellRenderer.bind(this) + } + + render () { + const { width } = this.props + + return ( + + ) + } + + _cellRenderer ({ columnIndex, key, parent, rowIndex, style }) { + const { getClassName, getContent, list } = this.props + + const datum = list.get(rowIndex + columnIndex % list.size) + const classNames = getClassName({ columnIndex, rowIndex }) + const content = getContent({ index: rowIndex, datum }) + + return ( + +
+ {content} +
+
+ ) + } +} diff --git a/source/CellMeasurer/CellMeasurer.DynamicHeightList.example.js b/source/CellMeasurer/CellMeasurer.DynamicHeightList.example.js new file mode 100644 index 000000000..78d1875c2 --- /dev/null +++ b/source/CellMeasurer/CellMeasurer.DynamicHeightList.example.js @@ -0,0 +1,83 @@ +/** @flow */ +import Immutable from 'immutable' +import React, { PropTypes, PureComponent } from 'react' +import CellMeasurer from './CellMeasurer' +import CellMeasurerCache from './CellMeasurerCache' +import List from '../List' +import styles from './CellMeasurer.example.css' + +export default class DynamicHeightList extends PureComponent { + static propTypes = { + getClassName: PropTypes.func.isRequired, + list: PropTypes.instanceOf(Immutable.List).isRequired, + width: PropTypes.number.isRequired + } + + constructor (props, context) { + super(props, context) + + this._cache = new CellMeasurerCache({ + fixedWidth: true, + minHeight: 50 + }) + + this._rowRenderer = this._rowRenderer.bind(this) + } + + render () { + const { width } = this.props + + return ( + + ) + } + + _rowRenderer ({ index, isScrolling, key, parent, style }) { + const { getClassName, list } = this.props + + const datum = list.get(index % list.size) + const classNames = getClassName({ columnIndex: 0, rowIndex: index }) + + const imageWidth = 300 + const imageHeight = datum.size + + const source = `http://lorempixel.com/${imageWidth}/${imageHeight}/` + + return ( + + {({ measure }) => ( +
+ +
+ )} +
+ ) + } +} diff --git a/source/CellMeasurer/CellMeasurer.DynamicHeightTableColumn.example.js b/source/CellMeasurer/CellMeasurer.DynamicHeightTableColumn.example.js new file mode 100644 index 000000000..386880269 --- /dev/null +++ b/source/CellMeasurer/CellMeasurer.DynamicHeightTableColumn.example.js @@ -0,0 +1,97 @@ +/** @flow */ +import Immutable from 'immutable' +import React, { PropTypes, PureComponent } from 'react' +import CellMeasurer from './CellMeasurer' +import CellMeasurerCache from './CellMeasurerCache' +import { Column, Table } from '../Table' +import styles from './CellMeasurer.example.css' + +export default class DynamicHeightTableColumn extends PureComponent { + static propTypes = { + list: PropTypes.instanceOf(Immutable.List).isRequired, + width: PropTypes.number.isRequired + } + + constructor (props, context) { + super(props, context) + + this._cache = new CellMeasurerCache({ + fixedWidth: true, + minHeight: 25 + }) + + this._columnCellRenderer = this._columnCellRenderer.bind(this) + this._rowGetter = this._rowGetter.bind(this) + } + + render () { + const { width } = this.props + + return ( + + + + +
+ ) + } + + _columnCellRenderer ({ cellData, columnData, dataKey, parent, rowData, rowIndex }) { + const { list } = this.props + + const datum = list.get(rowIndex % list.size) + const content = rowIndex % 5 === 0 + ? '' + : datum.randomLong + + return ( + +
+ {content} +
+
+ ) + } + + _rowGetter ({ index }) { + const { list } = this.props + + return list.get(index % list.size) + } +} diff --git a/source/CellMeasurer/CellMeasurer.DynamicWidthGrid.example.js b/source/CellMeasurer/CellMeasurer.DynamicWidthGrid.example.js new file mode 100644 index 000000000..0bbdd3ec8 --- /dev/null +++ b/source/CellMeasurer/CellMeasurer.DynamicWidthGrid.example.js @@ -0,0 +1,76 @@ +/** @flow */ +import Immutable from 'immutable' +import React, { PropTypes, PureComponent } from 'react' +import CellMeasurer from './CellMeasurer' +import CellMeasurerCache from './CellMeasurerCache' +import Grid from '../Grid' +import styles from './CellMeasurer.example.css' + +export default class DynamicWidthGrid extends PureComponent { + static propTypes = { + getClassName: PropTypes.func.isRequired, + getContent: PropTypes.func.isRequired, + list: PropTypes.instanceOf(Immutable.List).isRequired, + width: PropTypes.number.isRequired + } + + constructor (props, context) { + super(props, context) + + this._cache = new CellMeasurerCache({ + defaultHeight: 35, + fixedHeight: true + }) + + this._cellRenderer = this._cellRenderer.bind(this) + } + + render () { + const { width } = this.props + + return ( + + ) + } + + _cellRenderer ({ columnIndex, key, parent, rowIndex, style }) { + const { getClassName, getContent, list } = this.props + + const datum = list.get(rowIndex + columnIndex % list.size) + const classNames = getClassName({ columnIndex, rowIndex }) + const content = getContent({ index: columnIndex, datum, long: false }) + + return ( + +
+ {content} +
+
+ ) + } +} diff --git a/source/CellMeasurer/CellMeasurer.example.js b/source/CellMeasurer/CellMeasurer.example.js index 4ce00408d..b6f5201d2 100644 --- a/source/CellMeasurer/CellMeasurer.example.js +++ b/source/CellMeasurer/CellMeasurer.example.js @@ -3,19 +3,19 @@ import Immutable from 'immutable' import React, { PropTypes, PureComponent } from 'react' import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox' import AutoSizer from '../AutoSizer' -import CellMeasurer from './CellMeasurer' -import CellMeasurerCache from './CellMeasurerCache' -import Grid from '../Grid' -import List from '../List' -import { Column, Table } from '../Table' import cn from 'classnames' import styles from './CellMeasurer.example.css' +import DynamicWidthGrid from './CellMeasurer.DynamicWidthGrid.example.js' +import DynamiHeightGrid from './CellMeasurer.DynamiHeightGrid.example.js' +import DynamicHeightList from './CellMeasurer.DynamicHeightList.example.js' +import DynamicHeightTableColumn from './CellMeasurer.DynamicHeightTableColumn.example.js' -const COLUMN_COUNT = 50 -const COLUMN_WIDTH = 150 -const HEIGHT = 400 -const ROW_COUNT = 50 -const ROW_HEIGHT = 35 +const demoComponents = [ + DynamicWidthGrid, + DynamiHeightGrid, + DynamicHeightList, + DynamicHeightTableColumn +] export default class CellMeasurerExample extends PureComponent { static contextTypes = { @@ -71,6 +71,8 @@ export default class CellMeasurerExample extends PureComponent {
@@ -121,308 +123,3 @@ function Tab ({ children, currentTab, id, onClick }) { ) } - -class DynamicWidthGrid extends PureComponent { - static propTypes = { - list: PropTypes.instanceOf(Immutable.List).isRequired, - width: PropTypes.number.isRequired - } - - constructor (props, context) { - super(props, context) - - this._cache = new CellMeasurerCache({ - defaultHeight: ROW_HEIGHT, - fixedHeight: true - }) - - this._cellRenderer = this._cellRenderer.bind(this) - } - - render () { - const { width } = this.props - - return ( - - ) - } - - _cellRenderer ({ columnIndex, key, parent, rowIndex, style }) { - const { list } = this.props - - const datum = list.get(rowIndex + columnIndex % list.size) - const classNames = getClassName({ columnIndex, rowIndex }) - const content = getContent({ index: columnIndex, datum, long: false }) - - return ( - -
- {content} -
-
- ) - } -} - -class DynamiHeightGrid extends PureComponent { - static propTypes = { - list: PropTypes.instanceOf(Immutable.List).isRequired, - width: PropTypes.number.isRequired - } - - constructor (props, context) { - super(props, context) - - this._cache = new CellMeasurerCache({ - defaultWidth: COLUMN_WIDTH, - fixedWidth: true - }) - - this._cellRenderer = this._cellRenderer.bind(this) - } - - render () { - const { width } = this.props - - return ( - - ) - } - - _cellRenderer ({ columnIndex, key, parent, rowIndex, style }) { - const { list } = this.props - - const datum = list.get(rowIndex + columnIndex % list.size) - const classNames = getClassName({ columnIndex, rowIndex }) - const content = getContent({ index: rowIndex, datum }) - - return ( - -
- {content} -
-
- ) - } -} - -class DynamicHeightList extends PureComponent { - static propTypes = { - list: PropTypes.instanceOf(Immutable.List).isRequired, - width: PropTypes.number.isRequired - } - - constructor (props, context) { - super(props, context) - - this._cache = new CellMeasurerCache({ - fixedWidth: true, - minHeight: 50 - }) - - this._rowRenderer = this._rowRenderer.bind(this) - } - - render () { - const { width } = this.props - - return ( - - ) - } - - _rowRenderer ({ index, isScrolling, key, parent, style }) { - const { list } = this.props - - const datum = list.get(index % list.size) - const classNames = getClassName({ columnIndex: 0, rowIndex: index }) - - const imageWidth = 300 - const imageHeight = datum.size - - const source = `http://lorempixel.com/${imageWidth}/${imageHeight}/` - - return ( - - {({ measure }) => ( -
- -
- )} -
- ) - } -} - -class DynamicHeightTableColumn extends PureComponent { - static propTypes = { - list: PropTypes.instanceOf(Immutable.List).isRequired, - width: PropTypes.number.isRequired - } - - constructor (props, context) { - super(props, context) - - this._cache = new CellMeasurerCache({ - fixedWidth: true, - minHeight: 25 - }) - - this._columnCellRenderer = this._columnCellRenderer.bind(this) - this._rowGetter = this._rowGetter.bind(this) - } - - render () { - const { width } = this.props - - return ( - - - - -
- ) - } - - _columnCellRenderer ({ cellData, columnData, dataKey, parent, rowData, rowIndex }) { - const { list } = this.props - - const datum = list.get(rowIndex % list.size) - const content = rowIndex % 5 === 0 - ? '' - : datum.randomLong - - return ( - -
- {content} -
-
- ) - } - - _rowGetter ({ index }) { - const { list } = this.props - - return list.get(index % list.size) - } -} - -const demoComponents = [ - DynamicWidthGrid, - DynamiHeightGrid, - DynamicHeightList, - DynamicHeightTableColumn -] From 209f5a92e777587972e3826789184244ab01aa1a Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 14 Feb 2017 14:39:48 -0800 Subject: [PATCH 16/31] Updated Grid so that invalidated CellMeasurer cell sizes did not break updated scrollToIndex offsets --- source/Grid/Grid.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/source/Grid/Grid.js b/source/Grid/Grid.js index 6a00cc996..0e12827c1 100644 --- a/source/Grid/Grid.js +++ b/source/Grid/Grid.js @@ -329,9 +329,9 @@ export default class Grid extends PureComponent { componentDidMount () { const { scrollLeft, scrollToColumn, scrollTop, scrollToRow } = this.props - if (this._gridSizeInvalidated()) { - return - } + // If cell sizes have been invalidated (eg we are using CellMeasurer) then reset cached positions. + // We must do this at the start of the method as we may calculate and update scroll position below. + this._handleInvalidatedGridSize() // If this component was first rendered server-side, scrollbar size will be undefined. // In that event we need to remeasure. @@ -371,9 +371,9 @@ export default class Grid extends PureComponent { const { autoHeight, columnCount, height, rowCount, scrollToAlignment, scrollToColumn, scrollToRow, width } = this.props const { scrollLeft, scrollPositionChangeReason, scrollTop } = this.state - if (this._gridSizeInvalidated()) { - return - } + // If cell sizes have been invalidated (eg we are using CellMeasurer) then reset cached positions. + // We must do this at the start of the method as we may calculate and update scroll position below. + this._handleInvalidatedGridSize() // Handle edge case where column or row count has only just increased over 0. // In this case we may have to restore a previously-specified scroll offset. @@ -804,7 +804,11 @@ export default class Grid extends PureComponent { : props.estimatedRowSize } - _gridSizeInvalidated () { + /** + * Check for batched CellMeasurer size invalidations. + * This will occur the first time one or more previously unmeasured cells are rendered. + */ + _handleInvalidatedGridSize () { if (typeof this._deferredInvalidateColumnIndex === 'number') { const columnIndex = this._deferredInvalidateColumnIndex const rowIndex = this._deferredInvalidateRowIndex @@ -813,11 +817,7 @@ export default class Grid extends PureComponent { delete this._deferredInvalidateRowIndex this.recomputeGridSize({ columnIndex, rowIndex }) - - return true } - - return false } _invokeOnGridRenderedHelper () { From 29fcc94db397b412d7fe1a06e98920640053117c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 14 Feb 2017 15:04:49 -0800 Subject: [PATCH 17/31] Added bettern min height/width handilng (and tests) --- source/CellMeasurer/CellMeasurerCache.jest.js | 15 +++++++++-- source/CellMeasurer/CellMeasurerCache.js | 25 +++++++++++++------ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/source/CellMeasurer/CellMeasurerCache.jest.js b/source/CellMeasurer/CellMeasurerCache.jest.js index 5a3ad1be5..42527b437 100644 --- a/source/CellMeasurer/CellMeasurerCache.jest.js +++ b/source/CellMeasurer/CellMeasurerCache.jest.js @@ -1,8 +1,19 @@ import CellMeasurerCache, { DEFAULT_HEIGHT, DEFAULT_WIDTH } from './CellMeasurerCache' describe('CellMeasurerCache', () => { - // @TODO (bvaughn) Test fixed sizes and overrides for default - // @TODO (bvaughn) Test min width/height + it('should override defaultHeight/defaultWidth if minHeight/minWidth are greater', () => { + const cache = new CellMeasurerCache({ + defaultHeight: 20, + defaultWidth: 100, + minHeight: 30, + minWidth: 150 + }) + cache.set(0, 0, 50, 10) + expect(cache.getHeight(0, 0)).toBe(30) + expect(cache.getWidth(0, 0)).toBe(150) + expect(cache.rowHeight({ index: 0 })).toBe(30) + expect(cache.columnWidth({ index: 0 })).toBe(150) + }) it('should correctly report cache status', () => { const cache = new CellMeasurerCache() diff --git a/source/CellMeasurer/CellMeasurerCache.js b/source/CellMeasurer/CellMeasurerCache.js index 1207891b9..c2ddafc93 100644 --- a/source/CellMeasurer/CellMeasurerCache.js +++ b/source/CellMeasurer/CellMeasurerCache.js @@ -43,13 +43,24 @@ export default class CellMeasurerCache { _rowHeightCache: Cache; constructor (params : CellMeasurerCacheParams = {}) { - this._defaultHeight = params.defaultHeight || DEFAULT_HEIGHT - this._defaultWidth = params.defaultWidth || DEFAULT_WIDTH - this._hasFixedHeight = params.fixedHeight === true - this._hasFixedWidth = params.fixedWidth === true - this._minHeight = params.minHeight || 0 - this._minWidth = params.minWidth || 0 - this._keyMapper = params.keyMapper || defaultKeyMapper + const { + defaultHeight, + defaultWidth, + fixedHeight, + fixedWidth, + keyMapper, + minHeight, + minWidth + } = params + + this._hasFixedHeight = fixedHeight === true + this._hasFixedWidth = fixedWidth === true + this._minHeight = minHeight || 0 + this._minWidth = minWidth || 0 + this._keyMapper = keyMapper || defaultKeyMapper + + this._defaultHeight = Math.max(this._minHeight, defaultHeight || DEFAULT_HEIGHT) + this._defaultWidth = Math.max(this._minWidth, defaultWidth || DEFAULT_WIDTH) this._columnCount = 0 this._rowCount = 0 From c9c584d242cee485b698753720ec15e352afdac3 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 14 Feb 2017 15:05:06 -0800 Subject: [PATCH 18/31] 9.0.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b86883f13..f1d3928ce 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "React components for efficiently rendering large, scrollable lists and tabular data", "author": "Brian Vaughn ", "user": "bvaughn", - "version": "9.0.0-rc.0", + "version": "9.0.0-rc.1", "homepage": "https://github.com/bvaughn/react-virtualized", "main": "dist/commonjs/index.js", "module": "dist/es/index.js", From 30b2112c225654a624927932ec2552c94804f2b7 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 14 Feb 2017 18:43:05 -0800 Subject: [PATCH 19/31] Improved docs and a few tests --- docs/CellMeasurer.md | 266 ++++++------------ docs/Grid.md | 1 + .../CellMeasurer.DynamicWidthGrid.example.js | 2 +- source/CellMeasurer/CellMeasurer.jest.js | 6 +- source/CellMeasurer/CellMeasurer.js | 4 +- source/CellMeasurer/CellMeasurerCache.js | 4 +- source/Grid/Grid.jest.js | 3 +- source/Grid/Grid.js | 5 +- source/Grid/defaultCellRangeRenderer.js | 4 +- 9 files changed, 112 insertions(+), 183 deletions(-) diff --git a/docs/CellMeasurer.md b/docs/CellMeasurer.md index 86b7f606f..697b1c6fd 100644 --- a/docs/CellMeasurer.md +++ b/docs/CellMeasurer.md @@ -7,218 +7,136 @@ Specify a fixed width to measure dynamic height (or vice versa). This is an advanced component and has some limitations and performance considerations. [See below for more information](#limitations-and-performance-considerations). -`CellMeasurer` is intended for use with `Grid` components but [can be adapted to work with `List` as well](#using-cellmeasurer-with-list). +`CellMeasurer` can be used with `Grid`, `List`, and `Table` components. It is not intended to be used with the `Collection` component. ### Prop Types | Property | Type | Required? | Description | |:---|:---|:---:|:---| -| cellRenderer | Function | ✓ | Renders a cell given its indices. `({ columnIndex: number, rowIndex: number, index: number }): PropTypes.node`.
**NOTE**: `index` is just an alias to `rowIndex` | -| cellSizeCache | Object | | Optional, custom caching strategy for cell sizes. Learn more [here](#cellsizecache). | -| children | Function | ✓ | Function responsible for rendering a virtualized component; `({ getColumnWidth: Function, getRowHeight: Function, resetMeasurements: Function }) => PropTypes.element` | -| columnCount | number | ✓ | Number of columns in the `Grid`; in order to measure a row's height, all of that row's columns must be rendered. | -| container | | | A Node, Component instance, or function that returns either. If this property is not specified the document body will be used. | -| height | number | | Fixed height; specify this property to measure cell-width only. | -| rowCount | number | ✓ | Number of rows in the `Grid`; in order to measure a column's width, all of that column's rows must be rendered. | -| width | number | | Fixed width; specify this property to measure cell-height only. | - -### Children function - -The child function is passed the following named parameters: - -| Parameter | Type | Description | -|:---|:---|:---| -| getColumnWidth | Function | Callback to set as the `columnWidth` property of a `Grid` | -| getRowHeight | Function | Callback to set as the `rowHeight` property of a `Grid` | -| resetMeasurementForColumn(index) | Function | Use this function to clear cached measurements for specific column in `CellRenderer`; its size will be remeasured the next time it is requested. | -| resetMeasurementForRow(index) | Function | Use this function to clear cached measurements for specific row in `CellRenderer`; its size will be remeasured the next time it is requested. | -| resetMeasurements | Function | Use this function to clear cached measurements in `CellRenderer`; each cell will be remeasured the next time its size is requested. | - -### CellSizeCache - -If you choose to override the `cellSizeCache` property your cache should support the following operations: - -```js -class CellSizeCache { - clearAllColumnWidths (): void; - clearAllRowHeights (): void; - clearColumnWidth (index: number): void; - clearRowHeight (index: number): void; - getColumnWidth (index: number): number | undefined | null; - getRowHeight (index: number): number | undefined | null; - setColumnWidth (index: number, width: number): void; - setRowHeight (index: number, height: number): void; -} -``` +| cache | `CellMeasurerCache` | ✓ | Cache to be shared between `CellMeasurer` and its parent `Grid`. Learn more [here](#cellmeasurercache). | +| children | Element or Function | ✓ | Either a React element as a child (eg `
`) or a function (eg. `({ measure }) =>
`). See below for more detailed examples.` | +| columnIndex | number | ✓ | Index of column being measured (within the parent `Grid`). | +| parent | `Grid` | ✓ | Reference to the parent `Grid`; this value is passed by `Grid` to the `cellRenderer` and should be passed along as-is. | +| rowIndex | number | ✓ | Index of row being measured (within the parent `Grid`). | -The [default caching strategy](https://github.com/bvaughn/react-virtualized/blob/master/source/CellMeasurer/defaultCellSizeCache.js) is exported as `defaultCellMeasurerCellSizeCache` should you wish to decorate it. -You can also pass `uniformRowHeight` and/or `uniformColumnWidth` named parameters to the constructor for lists with a uniform (yet unknown) cell sizes. +### CellMeasurerCache -An [id-based caching strategy](#id-based-cell-size-cache) is also available for data that may be sorted. -This strategy maps data ids to cell sizes rathe than index so that the sorting order of the data does not invalidate sizes. +The `CellMeasurerCache` stores `CellMeasurer` measurements and shares them with a parent `Grid`. +It should be configured based on the type of measurements you need. It accepts the following parameters: + +### Prop Types +| Property | Type | Required? | Description | +|:---|:---|:---:|:---| +| defaultHeight | number | Umeasured cells will initially report this height | +| defaultWidth | number | Umeasured cells will initially report this width | +| fixedHeight | boolean | Rendered cells will have a fixed height, dynamic width | +| fixedWidth | boolean | Rendered cells will have a fixed width, dynamic height | +| minHeight | number | Derived row height (of multiple cells) should not be less than this value | +| minWidth | number | Derived column width (of multiple cells) should not be less than this value | +| keyMapper | KeyMapper | Enables more intelligent mapping of a given column and row index to an item ID. This prevents a cell cache from being invalidated when its parent collection is modified. `(rowIndex: number, columnIndex: number) => any` | ### Examples -###### Default `cellSizeCache` +###### Grid This example shows a `Grid` with fixed row heights and dynamic column widths. For more examples check out the component [demo page](https://bvaughn.github.io/react-virtualized/#/components/CellMeasurer). ```jsx import React from 'react'; -import ReactDOM from 'react-dom'; -import { CellMeasurer, Grid } from 'react-virtualized'; -import 'react-virtualized/styles.css'; // only needs to be imported once - -ReactDOM.render( - - {({ getColumnWidth }) => ( - - )} - , - document.getElementById('example') -); +import { CellMeasurer, CellMeasurerCache, Grid } from 'react-virtualized'; + +// In this example, average cell width is assumed to be about 100px. +// This value will be used for the initial `Grid` layout. +// Cell measurements smaller than 75px should also be rounded up. +// Height is not dynamic. +const cache = new CellMeasurerCache({ + defaultWidth: 100, + minWidth: 75, + fixedHeight: true +}); + +function cellRenderer ({ columnIndex, key, parent, rowIndex, style }) { + const content // Derive this from your data somehow + + return ( + + {content} + + ); +} + +function renderGrid (props) { + return ( + + ); +} ``` -#### ID-based cell size cache +###### Using `CellMeasurer` with images -`CellMeasurer` measures each cell once and then caches the measurements so it doesn't have to measure it again. -By default this caching is done using the cell's row and column indices. -Certain things (eg insertions, sorting) can invalidate this type of cache though. -If your list is dynamic- you may consider using an id-based caching strategy instead. -The `idCellMeasurerCellSizeCache` exists for this purpose: +This example shows how you might use the `CellMeasurer` component along with the `List` component in order to display dynamic-height rows. +The difference between this example and the above example is that the height of the row is not determined until image data has loaded. +To support this, a function-child is passed to `CellMeasurer` which then receives a `measure` parameter. +`measure` should be called when cell content is ready to be measured (in this case, when the image has loaded). ```jsx import React from 'react'; -import ReactDOM from 'react-dom'; -import { CellMeasurer, Grid, idCellMeasurerCellSizeCache } from 'react-virtualized'; -import 'react-virtualized/styles.css'; // only needs to be imported once - -ReactDOM.render( - - {({ getColumnWidth, getRowHeight }) => ( - - )} - , - document.getElementById('example') -); -``` +import { CellMeasurer, CellMeasurerCache, Grid } from 'react-virtualized'; -###### Customizing `cellSizeCache` +// In this example, average cell height is assumed to be about 50px. +// This value will be used for the initial `Grid` layout. +// Width is not dynamic. +const cache = new CellMeasurerCache({ + defaultHeight: 50, + fixedWidth: true +}); -The cell size cache can be optimized when width and/or height is uniform across cells. -In this case the cache will allow only a single cell width/height measurement and then return that value for all other cells. -You can use it like so: +function rowRenderer ({ index, isScrolling, key, parent, style }) { + const source // This comes from your list data -```jsx -import { - CellMeasurer, - defaultCellMeasurerCellSizeCache as CellSizeCache, - Grid -} from 'react-virtualized'; - -// Column widths vary but row heights are uniform -const cellSizeCache = new CellSizeCache({ - uniformRowHeight: true, - uniformColumnWidth: false -}) - -function render () { return ( - {({ getColumnWidth, getRowHeight }) => ( - ( + )} - ) + ); } -``` - -###### Using `CellMeasurer` with `List` - -`CellMeasurer` is intended for use with `Grid` components but can be adapted to work with `List` as well. -Doing this is just a matter of renaming the `rowIndex` property specified by `CellMeasurer` to an `index` property expected by `rowRenderer`. -```jsx - listProps.rowRenderer({ index: rowIndex, ...rest }) - } - columnCount={1} - rowCount={listProps.rowCount} - width={listProps.width} -> - {({ getRowHeight }) => ( +function renderList (props) { + return ( - )} - + ); +} ``` ### Limitations and Performance Considerations -###### Stateful Components - -The current implementation of `CellMeasurer` creates cells on demand, measures them, and then throws them away. -Future versions of this component may try to clone or in some other way share cells with their parent `Grid` in order to improve performance. -However until that happens, be wary of using `CellMeasurer` to measure stateful components. -Since cells are just-in-time created for measuring purposes they will only be measured with their default state. -To avoid this issue for now, use controlled props (instead of state) for cell rendering behavior. - -###### Styling - -Cells may be measured outside of the context of their intended `Grid` (or `List`). -This means that they will not inherit the parent styles while being measured. -Take care not rely on inherited styles for things that will affect measurement (eg `font-size`). -(See [issue 352](https://github.com/bvaughn/react-virtualized/issues/352) for more background information.) - -Certain box-sizing settings (eg `box-sizing: border-box`) may cause slight discrepancies if borders are applied to a `Grid` whose cells are being measured. -For this reason, it is recommended that you avoid placing borders on a `Grid` that uses a `CellMeasurer` and instead style its parent container. -(See [issue 338](https://github.com/bvaughn/react-virtualized/issues/338) for more background information.) - ###### Performance Measuring a column's width requires measuring all rows in order to determine the widest occurrence of that column. diff --git a/docs/Grid.md b/docs/Grid.md index f681d6937..ac6ba2c6c 100644 --- a/docs/Grid.md +++ b/docs/Grid.md @@ -14,6 +14,7 @@ A windowed grid of elements. `Grid` only renders cells necessary to fill itself | columnCount | Number | ✓ | Number of columns in grid. | | columnWidth | Number or Function | ✓ | Either a fixed column width (number) or a function that returns the width of a column given its index: `({ index: number }): number` | | containerStyle | Object | | Optional custom inline style to attach to inner cell-container element. | +| deferredMeasurementCache | `CellMeasurer` | | If CellMeasurer is used to measure this Grid's children, this should be a pointer to its CellMeasurerCache. A shared CellMeasurerCache reference enables Grid and CellMeasurer to share measurement data. | | estimatedColumnSize | Number | | Used to estimate the total width of a `Grid` before all of its columns have actually been measured. The estimated total width is adjusted as columns are rendered. | | estimatedRowSize | Number | | Used to estimate the total height of a `Grid` before all of its rows have actually been measured. The estimated total height is adjusted as rows are rendered. | | height | Number | ✓ | Height of Grid; this property determines the number of visible (vs virtualized) rows. | diff --git a/source/CellMeasurer/CellMeasurer.DynamicWidthGrid.example.js b/source/CellMeasurer/CellMeasurer.DynamicWidthGrid.example.js index 0bbdd3ec8..53f794d98 100644 --- a/source/CellMeasurer/CellMeasurer.DynamicWidthGrid.example.js +++ b/source/CellMeasurer/CellMeasurer.DynamicWidthGrid.example.js @@ -18,7 +18,7 @@ export default class DynamicWidthGrid extends PureComponent { super(props, context) this._cache = new CellMeasurerCache({ - defaultHeight: 35, + defaultWidth: 100, fixedHeight: true }) diff --git a/source/CellMeasurer/CellMeasurer.jest.js b/source/CellMeasurer/CellMeasurer.jest.js index 73bb27996..49371e5cd 100644 --- a/source/CellMeasurer/CellMeasurer.jest.js +++ b/source/CellMeasurer/CellMeasurer.jest.js @@ -197,7 +197,7 @@ describe('CellMeasurer', () => { expect(console.warn).not.toHaveBeenCalled() }) - it('should error if parent Grid does not specify a :deferredMeasurementCache prop', () => { + it('should warn if parent Grid does not specify a :deferredMeasurementCache prop', () => { spyOn(console, 'warn') const parent = createParent() // Parent Grid with no deferredMeasurementCache prop @@ -207,5 +207,9 @@ describe('CellMeasurer', () => { expect(console.warn).toHaveBeenCalledWith( 'CellMeasurer should be rendered within a Grid that has a deferredMeasurementCache prop.' ) + + renderHelper({ parent }) + + expect(console.warn).toHaveBeenCalledTimes(1) }) }) diff --git a/source/CellMeasurer/CellMeasurer.js b/source/CellMeasurer/CellMeasurer.js index e474adf44..11b27e506 100644 --- a/source/CellMeasurer/CellMeasurer.js +++ b/source/CellMeasurer/CellMeasurer.js @@ -15,8 +15,10 @@ function warnAboutImproperUse (parent) { if (process.env.NODE_ENV !== 'production') { if ( parent && - parent.props.deferredMeasurementCache === undefined + parent.props.deferredMeasurementCache === undefined && + parent.__warnedAboutImproperUse !== true ) { + parent.__warnedAboutImproperUse = true console.warn('CellMeasurer should be rendered within a Grid that has a deferredMeasurementCache prop.') } } diff --git a/source/CellMeasurer/CellMeasurerCache.js b/source/CellMeasurer/CellMeasurerCache.js index c2ddafc93..f8289e6eb 100644 --- a/source/CellMeasurer/CellMeasurerCache.js +++ b/source/CellMeasurer/CellMeasurerCache.js @@ -15,8 +15,8 @@ type CellMeasurerCacheParams = { defaultWidth ?: number, fixedHeight ?: boolean, fixedWidth ?: boolean, - minHeight: ?number, - minWidth: ?number, + minHeight?: number, + minWidth?: number, keyMapper ?: KeyMapper }; diff --git a/source/Grid/Grid.jest.js b/source/Grid/Grid.jest.js index 873cdb7b6..ce06b3933 100644 --- a/source/Grid/Grid.jest.js +++ b/source/Grid/Grid.jest.js @@ -1551,7 +1551,7 @@ describe('Grid', () => { expect(firstProps.style).not.toBe(secondProps.style) }) - it('should warn about cells that forget to include the style property', () => { + it('should warn about cells that forget to include the :style property', () => { spyOn(console, 'warn') function cellRenderer (props) { @@ -1563,5 +1563,6 @@ describe('Grid', () => { })) expect(console.warn).toHaveBeenCalledWith('Rendered cell should include style property for positioning.') + expect(console.warn).toHaveBeenCalledTimes(1) }) }) diff --git a/source/Grid/Grid.js b/source/Grid/Grid.js index 0e12827c1..be0903cf6 100644 --- a/source/Grid/Grid.js +++ b/source/Grid/Grid.js @@ -87,7 +87,10 @@ export default class Grid extends PureComponent { /** Optional inline style applied to inner cell-container */ containerStyle: PropTypes.object, - // @TODO (bvaughn) Document this; and is this the best name? + /** + * If CellMeasurer is used to measure this Grid's children, this should be a pointer to its CellMeasurerCache. + * A shared CellMeasurerCache reference enables Grid and CellMeasurer to share measurement data. + */ deferredMeasurementCache: PropTypes.object, /** diff --git a/source/Grid/defaultCellRangeRenderer.js b/source/Grid/defaultCellRangeRenderer.js index f8a3d8c41..33ef42ebe 100644 --- a/source/Grid/defaultCellRangeRenderer.js +++ b/source/Grid/defaultCellRangeRenderer.js @@ -143,9 +143,9 @@ function warnAboutMissingStyle (parent, renderedCell) { if ( renderedCell && renderedCell.props.style === undefined && - parent.__missingStyleWarning === undefined + parent.__warnedAboutMissingStyle !== true ) { - parent.__missingStyleWarning = true + parent.__warnedAboutMissingStyle = true console.warn('Rendered cell should include style property for positioning.') } From 38cb0a40aa749cf464d3366df25829a6fc8c0e7b Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 14 Feb 2017 19:05:31 -0800 Subject: [PATCH 20/31] Added test for style caching when in deferred measurement mode --- source/Grid/Grid.jest.js | 17 +++++++++++++++++ source/Grid/defaultCellRangeRenderer.js | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/source/Grid/Grid.jest.js b/source/Grid/Grid.jest.js index ce06b3933..d6a7d0de4 100644 --- a/source/Grid/Grid.jest.js +++ b/source/Grid/Grid.jest.js @@ -4,6 +4,7 @@ import { findDOMNode } from 'react-dom' import { Simulate } from 'react-addons-test-utils' import { render } from '../TestUtils' import Grid, { DEFAULT_SCROLLING_RESET_TIME_INTERVAL } from './Grid' +import CellMeasurerCache from '../CellMeasurer/CellMeasurerCache' import { SCROLL_DIRECTION_BACKWARD, SCROLL_DIRECTION_FORWARD } from './utils/getOverscanIndices' import { DEFAULT_MAX_SCROLL_SIZE } from './utils/ScalingCellSizeAndPositionManager' @@ -1551,6 +1552,22 @@ describe('Grid', () => { expect(firstProps.style).not.toBe(secondProps.style) }) + it('should only cache styles when a :deferredMeasurementCache is provided if the cell has already been measured', () => { + const cache = new CellMeasurerCache() + cache.set(0, 0, 100, 100) + cache.set(1, 1, 100, 100) + + const grid = render(getMarkup({ + columnCount: 2, + deferredMeasurementCache: cache, + rowCount: 2 + })) + + const keys = Object.keys(grid._styleCache) + + expect(keys).toEqual(['0-0', '1-1']) + }) + it('should warn about cells that forget to include the :style property', () => { spyOn(console, 'warn') diff --git a/source/Grid/defaultCellRangeRenderer.js b/source/Grid/defaultCellRangeRenderer.js index 33ef42ebe..ab18263f5 100644 --- a/source/Grid/defaultCellRangeRenderer.js +++ b/source/Grid/defaultCellRangeRenderer.js @@ -59,7 +59,6 @@ export default function defaultCellRangeRenderer ({ } else { // In deferred mode, cells will be initially rendered before we know their size. // Don't interfere with CellMeasurer's measurements by setting an invalid size. - // @TODO (bvaughn) Add automated test coverage for this. if ( deferredMode && !deferredMeasurementCache.has(rowIndex, columnIndex) From 735e1e6368a69b08a9cf20fc8a19c2d7ff72b8ac Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 15 Feb 2017 22:38:51 -0800 Subject: [PATCH 21/31] Fixed typo in InfiniteLoader --- docs/InfiniteLoader.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/InfiniteLoader.md b/docs/InfiniteLoader.md index b86d0cd1a..544517012 100644 --- a/docs/InfiniteLoader.md +++ b/docs/InfiniteLoader.md @@ -41,6 +41,9 @@ import ReactDOM from 'react-dom'; import { InfiniteLoader, List } from 'react-virtualized'; import 'react-virtualized/styles.css'; // only needs to be imported once +// This example assumes you have a way to know/load this information +const remoteRowCount + const list = []; function isRowLoaded ({ index }) { @@ -77,7 +80,7 @@ ReactDOM.render( height={200} onRowsRendered={onRowsRendered} ref={registerChild} - rowCount={list.length} + rowCount={remoteRowCount} rowHeight={20} rowRenderer={rowRenderer} width={300} From e82d5fa121b2f47e14980b0fe4fb45149f75f77c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 15 Feb 2017 22:39:15 -0800 Subject: [PATCH 22/31] Added invalidation test for Grid --- source/ColumnSizer/ColumnSizer.jest.js | 2 + source/Grid/Grid.jest.js | 91 ++++++++++++++++++-- source/InfiniteLoader/InfiniteLoader.jest.js | 2 +- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/source/ColumnSizer/ColumnSizer.jest.js b/source/ColumnSizer/ColumnSizer.jest.js index d958e1449..c9364e170 100644 --- a/source/ColumnSizer/ColumnSizer.jest.js +++ b/source/ColumnSizer/ColumnSizer.jest.js @@ -111,6 +111,8 @@ describe('ColumnSizer', () => { }) it('should error if the registered child is not a Grid or a MultiGrid', () => { + spyOn(console, 'error') + expect(() => { render( { it('should clear style cache if cell sizes change', () => { const cellRendererCalls = [] - function cellRenderer (props) { - cellRendererCalls.push(props) + function cellRenderer (params) { + cellRendererCalls.push(params) + return
} const props = { @@ -1525,8 +1526,9 @@ describe('Grid', () => { it('should not pull from the style cache while scrolling if there is an offset adjustment', () => { let cellRendererCalls = [] - function cellRenderer (props) { - cellRendererCalls.push(props) + function cellRenderer (params) { + cellRendererCalls.push(params) + return
} const grid = render(getMarkup({ @@ -1571,8 +1573,8 @@ describe('Grid', () => { it('should warn about cells that forget to include the :style property', () => { spyOn(console, 'warn') - function cellRenderer (props) { - return
+ function cellRenderer (params) { + return
} render(getMarkup({ @@ -1582,4 +1584,81 @@ describe('Grid', () => { expect(console.warn).toHaveBeenCalledWith('Rendered cell should include style property for positioning.') expect(console.warn).toHaveBeenCalledTimes(1) }) + + describe('invalidateCellSizeAfterRender', () => { + it('should invalidate cache and refresh displayed cells after mount', () => { + const cache = new CellMeasurerCache() + + let invalidateCellSizeAfterRender = true + + const cellRenderer = jest.fn() + cellRenderer.mockImplementation( + (params) => { + // Don't get stuck in a loop + if (invalidateCellSizeAfterRender) { + invalidateCellSizeAfterRender = false + + params.parent.invalidateCellSizeAfterRender({ + columnIndex: 1, + rowIndex: 0 + }) + } + return
+ }) + + const props = { + cellRenderer, + columnCount: 2, + deferredMeasurementCache: cache, + rowCount: 2 + } + + render(getMarkup(props)) + + // 4 times for initial render + 4 once cellCache was cleared + expect(cellRenderer).toHaveBeenCalledTimes(8) + }) + + it('should invalidate cache and refresh displayed cells after update', () => { + const cache = new CellMeasurerCache() + + const cellRenderer = jest.fn() + cellRenderer.mockImplementation( + (params) =>
+ ) + + const props = { + cellRenderer, + columnCount: 2, + deferredMeasurementCache: cache, + rowCount: 2 + } + + const grid = render(getMarkup(props)) + + expect(cellRenderer).toHaveBeenCalledTimes(4) + + let invalidateCellSizeAfterRender = false + + cellRenderer.mockReset() + cellRenderer.mockImplementation( + (params) => { + // Don't get stuck in a loop + if (invalidateCellSizeAfterRender) { + invalidateCellSizeAfterRender = false + params.parent.invalidateCellSizeAfterRender({ + columnIndex: 1, + rowIndex: 0 + }) + } + return
+ }) + + invalidateCellSizeAfterRender = true + grid.recomputeGridSize() + + // 4 times for initial render + 4 once cellCache was cleared + expect(cellRenderer).toHaveBeenCalledTimes(8) + }) + }) }) diff --git a/source/InfiniteLoader/InfiniteLoader.jest.js b/source/InfiniteLoader/InfiniteLoader.jest.js index 939adf044..c5ea4c2b5 100644 --- a/source/InfiniteLoader/InfiniteLoader.jest.js +++ b/source/InfiniteLoader/InfiniteLoader.jest.js @@ -29,7 +29,7 @@ describe('InfiniteLoader', () => { function rowRenderer ({ index, key, style }) { rowRendererCalls.push(index) return ( -
+
) } From fb5ffb09ce66b41c7817e337e0e386042a87d1fd Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 15 Feb 2017 22:47:28 -0800 Subject: [PATCH 23/31] Added deferred mode cache test --- source/Grid/Grid.jest.js | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/source/Grid/Grid.jest.js b/source/Grid/Grid.jest.js index 64dd6af85..9a3c0742b 100644 --- a/source/Grid/Grid.jest.js +++ b/source/Grid/Grid.jest.js @@ -1585,8 +1585,8 @@ describe('Grid', () => { expect(console.warn).toHaveBeenCalledTimes(1) }) - describe('invalidateCellSizeAfterRender', () => { - it('should invalidate cache and refresh displayed cells after mount', () => { + describe('deferredMeasurementCache', () => { + it('invalidateCellSizeAfterRender should invalidate cache and refresh displayed cells after mount', () => { const cache = new CellMeasurerCache() let invalidateCellSizeAfterRender = true @@ -1660,5 +1660,39 @@ describe('Grid', () => { // 4 times for initial render + 4 once cellCache was cleared expect(cellRenderer).toHaveBeenCalledTimes(8) }) + + it('should not cache cells until they have been measured by CellMeasurer', () => { + const cache = new CellMeasurerCache() + + // Fake measure cell 0,0 but not cell 0,1 + cache.set(0, 0, 100, 30) + + const cellRenderer = jest.fn() + cellRenderer.mockImplementation( + (params) =>
+ ) + + const props = { + cellRenderer, + columnCount: 2, + deferredMeasurementCache: cache, + rowCount: 1 + } + + // Trigger 2 renders + // The second render should re-use the style for cell 0,0 + // But should not re-use the style for cell 0,1 since it was not measured + const grid = render(getMarkup(props)) + grid.forceUpdate() + + // 0,0 - 0,1 - 0,0 - 0,1 + expect(cellRenderer).toHaveBeenCalledTimes(4) + const style00A = cellRenderer.mock.calls[0][0].style + const style01A = cellRenderer.mock.calls[1][0].style + const style00B = cellRenderer.mock.calls[2][0].style + const style01B = cellRenderer.mock.calls[3][0].style + expect(style00A).toBe(style00B) + expect(style01A).not.toBe(style01B) + }) }) }) From 1f97eb6e783b9b267b72f6765da1682c10a6e815 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 16 Feb 2017 09:06:10 -0800 Subject: [PATCH 24/31] Fixed formatting on CellMeasurer docs --- docs/CellMeasurer.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/CellMeasurer.md b/docs/CellMeasurer.md index 697b1c6fd..a203f6dea 100644 --- a/docs/CellMeasurer.md +++ b/docs/CellMeasurer.md @@ -26,13 +26,13 @@ It should be configured based on the type of measurements you need. It accepts t ### Prop Types | Property | Type | Required? | Description | |:---|:---|:---:|:---| -| defaultHeight | number | Umeasured cells will initially report this height | -| defaultWidth | number | Umeasured cells will initially report this width | -| fixedHeight | boolean | Rendered cells will have a fixed height, dynamic width | -| fixedWidth | boolean | Rendered cells will have a fixed width, dynamic height | -| minHeight | number | Derived row height (of multiple cells) should not be less than this value | -| minWidth | number | Derived column width (of multiple cells) should not be less than this value | -| keyMapper | KeyMapper | Enables more intelligent mapping of a given column and row index to an item ID. This prevents a cell cache from being invalidated when its parent collection is modified. `(rowIndex: number, columnIndex: number) => any` | +| defaultHeight | number | | Umeasured cells will initially report this height | +| defaultWidth | number | | Umeasured cells will initially report this width | +| fixedHeight | boolean | | Rendered cells will have a fixed height, dynamic width | +| fixedWidth | boolean | | Rendered cells will have a fixed width, dynamic height | +| minHeight | number | | Derived row height (of multiple cells) should not be less than this value | +| minWidth | number | | Derived column width (of multiple cells) should not be less than this value | +| keyMapper | KeyMapper | | Enables more intelligent mapping of a given column and row index to an item ID. This prevents a cell cache from being invalidated when its parent collection is modified. `(rowIndex: number, columnIndex: number) => any` | ### Examples From 7dd1e79ab6708173d717b537cab8ee0720e9325e Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 16 Feb 2017 14:24:48 -0800 Subject: [PATCH 25/31] Removed unused parameter --- source/CellMeasurer/CellMeasurer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/CellMeasurer/CellMeasurer.js b/source/CellMeasurer/CellMeasurer.js index 11b27e506..a98e5bc17 100644 --- a/source/CellMeasurer/CellMeasurer.js +++ b/source/CellMeasurer/CellMeasurer.js @@ -93,7 +93,7 @@ export default class CellMeasurer extends PureComponent { } } - _measure (force = false) { + _measure () { const { cache, columnIndex, parent, rowIndex } = this.props const node = findDOMNode(this) From 09ae04d0a319a8ab4d78d1cab15adf34559a89b4 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 16 Feb 2017 14:44:09 -0800 Subject: [PATCH 26/31] Bumped RC in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3742cc6e7..90d2eae8e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "React components for efficiently rendering large, scrollable lists and tabular data", "author": "Brian Vaughn ", "user": "bvaughn", - "version": "9.0.0-rc.1", + "version": "9.0.0-rc.2", "homepage": "https://github.com/bvaughn/react-virtualized", "main": "dist/commonjs/index.js", "module": "dist/es/index.js", From 8720a7660f408724a4f671b3fb549fb1ace96e2e Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 16 Feb 2017 14:54:39 -0800 Subject: [PATCH 27/31] Added :parent pass-thru param test for List and Table --- source/List/List.jest.js | 8 ++++++-- source/Table/Table.jest.js | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/source/List/List.jest.js b/source/List/List.jest.js index ae7ab6136..efb20ae98 100644 --- a/source/List/List.jest.js +++ b/source/List/List.jest.js @@ -38,8 +38,6 @@ describe('List', () => { ) } - // @TODO (bvaughn) Test new :parent param - describe('number of rendered children', () => { it('should render enough children to fill the view', () => { const rendered = findDOMNode(render(getMarkup())) @@ -456,6 +454,12 @@ describe('List', () => { expect(rowRendererCalls[1].isVisible).toEqual(false) }) + it('should relay the Grid :parent param to the :rowRenderer', () => { + const rowRenderer = jest.fn().mockReturnValue(null) + findDOMNode(render(getMarkup({ rowRenderer }))) + expect(rowRenderer.mock.calls[0][0].parent).not.toBeUndefined() + }) + describe('pure', () => { it('should not re-render unless props have changed', () => { let rowRendererCalled = false diff --git a/source/Table/Table.jest.js b/source/Table/Table.jest.js index aadcac4f3..bdc35ceae 100644 --- a/source/Table/Table.jest.js +++ b/source/Table/Table.jest.js @@ -76,8 +76,6 @@ describe('Table', () => { ) } - // @TODO (bvaughn) Test new :parent param - describe('children', () => { it('should accept Column children', () => { const children = [ @@ -984,4 +982,10 @@ describe('Table', () => { const rendered = findDOMNode(render(getMarkup())) expect(rendered.querySelector('.ReactVirtualized__Grid__innerScrollContainer').style.width).toEqual('auto') }) + + it('should relay the Grid :parent param to the Column :cellRenderer', () => { + const cellRenderer = jest.fn().mockReturnValue(null) + findDOMNode(render(getMarkup({ cellRenderer }))) + expect(cellRenderer.mock.calls[0][0].parent).not.toBeUndefined() + }) }) From 847dc6d1fcd86c7228631ccfbccdd92ce4ce107d Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 16 Feb 2017 14:58:41 -0800 Subject: [PATCH 28/31] Tweaked CellMeasurerCache test to also cover min sizes --- source/CellMeasurer/CellMeasurerCache.jest.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/source/CellMeasurer/CellMeasurerCache.jest.js b/source/CellMeasurer/CellMeasurerCache.jest.js index 42527b437..7ea12d3a1 100644 --- a/source/CellMeasurer/CellMeasurerCache.jest.js +++ b/source/CellMeasurer/CellMeasurerCache.jest.js @@ -29,13 +29,15 @@ describe('CellMeasurerCache', () => { it('should return the correct default sizes for uncached cells if specified', () => { const cache = new CellMeasurerCache({ defaultHeight: 20, - defaultWidth: 100 + defaultWidth: 100, + minHeight: 15, + minWidth: 80 }) expect(cache.getWidth(0, 0)).toBe(100) expect(cache.getHeight(0, 0)).toBe(20) - cache.set(0, 0, 150, 30) - expect(cache.getWidth(0, 0)).toBe(150) - expect(cache.getHeight(0, 0)).toBe(30) + cache.set(0, 0, 70, 10) + expect(cache.getWidth(0, 0)).toBe(80) + expect(cache.getHeight(0, 0)).toBe(15) }) it('should clear a single cached cell', () => { From 59718a01ce84828348f01e190b6ddf1803fc281b Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 16 Feb 2017 15:01:50 -0800 Subject: [PATCH 29/31] Added CellSizeAndPositionManager test for :batchAllCells param --- .../utils/CellSizeAndPositionManager.jest.js | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/source/Grid/utils/CellSizeAndPositionManager.jest.js b/source/Grid/utils/CellSizeAndPositionManager.jest.js index c10c980ae..8a3f82ee9 100644 --- a/source/Grid/utils/CellSizeAndPositionManager.jest.js +++ b/source/Grid/utils/CellSizeAndPositionManager.jest.js @@ -3,11 +3,13 @@ import CellSizeAndPositionManager from './CellSizeAndPositionManager' describe('CellSizeAndPositionManager', () => { function getCellSizeAndPositionManager ({ + batchAllCells, cellCount = 100, estimatedCellSize = 15 } = {}) { const cellSizeGetterCalls = [] const cellSizeAndPositionManager = new CellSizeAndPositionManager({ + batchAllCells, cellCount, cellSizeGetter: ({ index }) => { cellSizeGetterCalls.push(index) @@ -22,9 +24,6 @@ describe('CellSizeAndPositionManager', () => { } } - // @TODO (bvaughn) Test batchAllCells mode measures all initially; doesn't measure once already measured. - // Test this resets properly when clear is called too - describe('configure', () => { it('should update inner :cellCount and :estimatedCellSize', () => { const { cellSizeAndPositionManager } = getCellSizeAndPositionManager() @@ -336,6 +335,22 @@ describe('CellSizeAndPositionManager', () => { expect(start).toEqual(95) expect(stop).toEqual(99) }) + + it('should return all cells if :batchAllCells param was used (for CellMeasurer support)', () => { + const { cellSizeAndPositionManager } = getCellSizeAndPositionManager({ + batchAllCells: true, + cellCount: 100 + }) + const { + start, + stop + } = cellSizeAndPositionManager.getVisibleCellRange({ + containerSize: 50, + offset: 950 + }) + expect(start).toEqual(0) + expect(stop).toEqual(99) + }) }) describe('resetCell', () => { From 665cc698badd52053588c7f0d05434ef74e0786b Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 16 Feb 2017 21:13:32 -0800 Subject: [PATCH 30/31] Tidying up docs --- README.md | 19 ++++++++++++------- docs/CellMeasurer.md | 10 +++++++--- docs/Grid.md | 9 +++++---- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6851eae9d..5ccc23958 100644 --- a/README.md +++ b/README.md @@ -95,17 +95,21 @@ ES6, CommonJS, and UMD builds are available with each distribution. For example: ```js -// If you're using the Table component you'll need to include the default styles. +// Most of react-virtualized's styles are functional (eg position, size). +// Functional styles are applied directly to DOM elements. +// The Table component ships with a few presentational styles as well. +// They are optional, but if you want them you will need to also import the CSS file. // This only needs to be done once; probably during your application's bootstrapping process. -// Grid and List base styles are purely functional and so they're all inline. import 'react-virtualized/styles.css' -// Then you can import any react-virtualized components you need. -// Tree-shaking is supported with ES6 modules (`jsnext:main` package target). -import { Table } from 'react-virtualized' +// You can import any component you want as a named export from 'react-virtualized', eg +import { Column, Table } from 'react-virtualized' + +// Or for better tree-shaking support you can use deep imports, eg +import { List } from 'react-virtualized/dist/commonjs/List' ``` -Alternately you can load a global-friendly UMD build: +You can also use a global-friendly UMD build: ```html @@ -163,7 +167,8 @@ There are also a couple of how-to guides: * [Displaying items in reverse order](docs/reverseList.md) * [Using AutoSizer](docs/usingAutoSizer.md) * [Creating an infinite-loading list](docs/creatingAnInfiniteLoadingList.md) -* [Displaying a reverse list](docs/reverseList.md) +* [Natural sort Table](docs/tableWithNaturalSort.md) + Examples --------------- diff --git a/docs/CellMeasurer.md b/docs/CellMeasurer.md index a203f6dea..1c3bc7abd 100644 --- a/docs/CellMeasurer.md +++ b/docs/CellMeasurer.md @@ -3,7 +3,6 @@ CellMeasurer High-order component that automatically measures a cell's contents by temporarily rendering it in a way that is not visible to the user. Specify a fixed width to measure dynamic height (or vice versa). - This is an advanced component and has some limitations and performance considerations. [See below for more information](#limitations-and-performance-considerations). @@ -13,8 +12,8 @@ This is an advanced component and has some limitations and performance considera | Property | Type | Required? | Description | |:---|:---|:---:|:---| | cache | `CellMeasurerCache` | ✓ | Cache to be shared between `CellMeasurer` and its parent `Grid`. Learn more [here](#cellmeasurercache). | -| children | Element or Function | ✓ | Either a React element as a child (eg `
`) or a function (eg. `({ measure }) =>
`). See below for more detailed examples.` | -| columnIndex | number | ✓ | Index of column being measured (within the parent `Grid`). | +| children | Element or Function | ✓ | Either a React element as a child (eg `
`) or a function (eg. `({ measure }) =>
`). See [below](#using-cellmeasurer-with-images) for more detailed examples. | +| columnIndex | number | ✓ | Index of column being measured (within the parent `Grid`) or 0 (if used within a `List` or `Table`). | | parent | `Grid` | ✓ | Reference to the parent `Grid`; this value is passed by `Grid` to the `cellRenderer` and should be passed along as-is. | | rowIndex | number | ✓ | Index of row being measured (within the parent `Grid`). | @@ -34,6 +33,11 @@ It should be configured based on the type of measurements you need. It accepts t | minWidth | number | | Derived column width (of multiple cells) should not be less than this value | | keyMapper | KeyMapper | | Enables more intelligent mapping of a given column and row index to an item ID. This prevents a cell cache from being invalidated when its parent collection is modified. `(rowIndex: number, columnIndex: number) => any` | +Note that while all of the individual parameters above are optional, you must supply at least some of them. +`CellMeasurerCache` is not meant to measure cells that are both dyanmic width _and_ height. +It would be unefficient to do so since the size of a row (or column) is equal to the largest cell within that row. +See [below](#limitations-and-performance-considerations) for more information. + ### Examples ###### Grid diff --git a/docs/Grid.md b/docs/Grid.md index 3cb20907f..2524490c3 100644 --- a/docs/Grid.md +++ b/docs/Grid.md @@ -164,9 +164,10 @@ function cellRenderer ({ isScrolling, // The Grid is currently being scrolled isVisible, // This cell is visible within the grid (eg it is not an overscanned cell) key, // Unique key within array of cells + parent, // Reference to the parent Grid (instance) rowIndex, // Vertical (row) index of cell style // Style object to be applied to cell (to position it); - // this must be passed through to the rendered cell element + // This must be passed through to the rendered cell element. }) { // Grid data is a 2d array in this example... const user = list[rowIndex][columnIndex] @@ -179,11 +180,11 @@ function cellRenderer ({ // Style is required since it specifies how the cell is to be sized and positioned, // and React Virtualized depends on this sizing/positioning for proper scrolling behavior. // By default, the grid component specifies, calculates, and initializes the following style properties: - // height - // width + // position // left // top - // position + // height + // width // You can add additional class names or style properties as you would like. // Key is also required by React to more efficiently manage the array of cells. return ( From bbf99709b61848d07bf3321fd9d278a8bee1ef79 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 16 Feb 2017 21:17:25 -0800 Subject: [PATCH 31/31] Prepping 9.0.0 package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90d2eae8e..06fba25bf 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "React components for efficiently rendering large, scrollable lists and tabular data", "author": "Brian Vaughn ", "user": "bvaughn", - "version": "9.0.0-rc.2", + "version": "9.0.0", "homepage": "https://github.com/bvaughn/react-virtualized", "main": "dist/commonjs/index.js", "module": "dist/es/index.js",