diff --git a/source/Masonry/Masonry.example.css b/source/Masonry/Masonry.example.css index de61d1151..118ac5dc3 100644 --- a/source/Masonry/Masonry.example.css +++ b/source/Masonry/Masonry.example.css @@ -5,4 +5,14 @@ padding: 0.5rem; background-color: #f7f7f7; word-break: break-all; +} + +.checkboxLabel { + margin-left: .5rem; +} +.checkboxLabel:first-of-type { + margin-left: 0; +} +.checkbox { + margin-right: 5px; } \ No newline at end of file diff --git a/source/Masonry/Masonry.example.js b/source/Masonry/Masonry.example.js index fb0d321ca..ee4bd8d64 100644 --- a/source/Masonry/Masonry.example.js +++ b/source/Masonry/Masonry.example.js @@ -5,6 +5,7 @@ import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/Conte import { LabeledInput, InputRow } from '../demo/LabeledInput' import { CellMeasurer, CellMeasurerCache } from '../CellMeasurer' import AutoSizer from '../AutoSizer' +import WindowScroller from '../WindowScroller' import createCellPositioner from './createCellPositioner' import Masonry from './Masonry' import styles from './Masonry.example.css' @@ -30,11 +31,13 @@ export default class GridExample extends PureComponent { this.state = { columnWidth: 200, height: 300, - gutterSize: 10 + gutterSize: 10, + windowScrollerEnabled: false } this._cellRenderer = this._cellRenderer.bind(this) this._onResize = this._onResize.bind(this) + this._renderAutoSizer = this._renderAutoSizer.bind(this) this._renderMasonry = this._renderMasonry.bind(this) this._setMasonryRef = this._setMasonryRef.bind(this) } @@ -43,9 +46,22 @@ export default class GridExample extends PureComponent { const { columnWidth, height, - gutterSize + gutterSize, + windowScrollerEnabled } = this.state + let child + + if (windowScrollerEnabled) { + child = ( + + {this._renderAutoSizer} + + ) + } else { + child = this._renderAutoSizer({ height }) + } + return ( + + + + { this._calculateColumnCount() - this._initOrResetPositioner() + this._resetCellPositioner() this._masonry.clearCellPositions() }) }} @@ -96,7 +132,7 @@ export default class GridExample extends PureComponent { gutterSize: parseInt(event.target.value, 10) || 10 }, () => { this._calculateColumnCount() - this._initOrResetPositioner() + this._resetCellPositioner() this._masonry.recomputeCellPositions() }) }} @@ -104,13 +140,7 @@ export default class GridExample extends PureComponent { /> - - {this._renderMasonry} - + {child} ) } @@ -159,55 +189,82 @@ export default class GridExample extends PureComponent { ) } - _initOrResetPositioner () { - const { - columnWidth, - gutterSize - } = this.state - + _initCellPositioner () { if (typeof this._cellPositioner === 'undefined') { + const { + columnWidth, + gutterSize + } = this.state + this._cellPositioner = createCellPositioner({ cellMeasurerCache: this._cache, columnCount: this._columnCount, columnWidth, spacer: gutterSize }) - } else { - this._cellPositioner.reset({ - columnCount: this._columnCount, - columnWidth, - spacer: gutterSize - }) } } - _onResize (prevProps, prevState) { + _onResize ({ height, width }) { + this._width = width + this._columnHeights = {} this._calculateColumnCount() - this._initOrResetPositioner() + this._resetCellPositioner() this._masonry.recomputeCellPositions() } + _renderAutoSizer ({ height, scrollTop }) { + this._height = height + this._scrollTop = scrollTop + + return ( + + {this._renderMasonry} + + ) + } + _renderMasonry ({ width }) { this._width = width + this._calculateColumnCount() - this._initOrResetPositioner() + this._initCellPositioner() - const { height } = this.state + const { height, windowScrollerEnabled } = this.state return ( ) } + _resetCellPositioner () { + const { + columnWidth, + gutterSize + } = this.state + + this._cellPositioner.reset({ + columnCount: this._columnCount, + columnWidth, + spacer: gutterSize + }) + } + _setMasonryRef (ref) { this._masonry = ref } diff --git a/source/Masonry/Masonry.jest.js b/source/Masonry/Masonry.jest.js index 3291ada0b..6bd5dd427 100644 --- a/source/Masonry/Masonry.jest.js +++ b/source/Masonry/Masonry.jest.js @@ -138,11 +138,20 @@ describe('Masonry', () => { it('should measure additional cells on scroll when it runs out of measured cells', () => { const cellMeasurerCache = createCellMeasurerCache() - const rendered = findDOMNode(render(getMarkup({ cellMeasurerCache }))) + const renderCallback = jest.fn().mockImplementation(index => index) + const cellRenderer = createCellRenderer(cellMeasurerCache, renderCallback) + const rendered = findDOMNode(render(getMarkup({ cellRenderer, cellMeasurerCache }))) expect(cellMeasurerCache.has(9)).toBe(false) + + renderCallback.mockClear() + simulateScroll(rendered, 101) expect(cellMeasurerCache.has(9)).toBe(true) expect(cellMeasurerCache.has(10)).toBe(false) + + // The first batch-measured cell in the new block should be the 10th one + // Verify that we measured the correct cell... + expect(renderCallback.mock.calls[0][0]).toBe(9) }) it('should only render enough cells to fill the viewport plus overscanByPixels', () => { @@ -155,19 +164,66 @@ describe('Masonry', () => { simulateScroll(rendered, 1001) assertVisibleCells(rendered, '27,29,30,31,32,33,34,35') }) + + it('should still render correctly when autoHeight is true (eg WindowScroller)', () => { + // Share instances between renders to avoid resetting state in ways we don't intend + const cellMeasurerCache = createCellMeasurerCache() + const cellPositioner = createCellPositioner(cellMeasurerCache) + + let rendered = findDOMNode(render(getMarkup({ + autoHeight: true, + cellMeasurerCache, + cellPositioner + }))) + assertVisibleCells(rendered, '0,1,2,3,4,5') + rendered = findDOMNode(render(getMarkup({ + autoHeight: true, + cellMeasurerCache, + cellPositioner, + scrollTop: 51 + }))) + assertVisibleCells(rendered, '0,1,2,3,4,5,6') + rendered = findDOMNode(render(getMarkup({ + autoHeight: true, + cellMeasurerCache, + cellPositioner, + scrollTop: 101 + }))) + assertVisibleCells(rendered, '0,2,3,4,5,6,7,8') + rendered = findDOMNode(render(getMarkup({ + autoHeight: true, + cellMeasurerCache, + cellPositioner, + scrollTop: 1001 + }))) + assertVisibleCells(rendered, '27,29,30,31,32,33,34,35') + }) }) describe('recomputeCellPositions', () => { it('should refresh all cell positions', () => { + // Share instances between renders to avoid resetting state in ways we don't intend const cellMeasurerCache = createCellMeasurerCache() - let rendered = findDOMNode(render(getMarkup({ cellMeasurerCache }))) - assertVisibleCells(rendered, '0,1,2,3,4,5') - const component = render(getMarkup({ + const cellPositioner = jest.fn().mockImplementation( + createCellPositioner(cellMeasurerCache) + ) + + let rendered = findDOMNode(render(getMarkup({ cellMeasurerCache, - cellPositioner: index => ({ + cellPositioner + }))) + assertVisibleCells(rendered, '0,1,2,3,4,5') + + cellPositioner.mockImplementation( + index => ({ left: 0, top: index * CELL_SIZE_MULTIPLIER }) + ) + + const component = render(getMarkup({ + cellMeasurerCache, + cellPositioner })) rendered = findDOMNode(component) assertVisibleCells(rendered, '0,1,2,3,4,5') diff --git a/source/Masonry/Masonry.js b/source/Masonry/Masonry.js index 0c9afa775..9b4235c16 100644 --- a/source/Masonry/Masonry.js +++ b/source/Masonry/Masonry.js @@ -41,6 +41,7 @@ export default class Masonry extends PureComponent { props: Props; static defaultProps = { + autoHeight: false, keyMapper: identity, onCellsRendered: noop, onScroll: noop, @@ -109,8 +110,28 @@ export default class Masonry extends PureComponent { this._invokeOnCellsRenderedCallback() } + componentWillUnmount () { + if (this._debounceResetIsScrollingId) { + clearTimeout(this._debounceResetIsScrollingId) + } + } + + componentWillReceiveProps (nextProps) { + const { scrollTop } = this.props + + if (scrollTop !== nextProps.scrollTop) { + this._debounceResetIsScrolling() + + this.setState({ + isScrolling: true, + scrollTop: nextProps.scrollTop + }) + } + } + render () { const { + autoHeight, cellCount, cellMeasurerCache, cellRenderer, @@ -151,10 +172,10 @@ export default class Masonry extends PureComponent { ) ) - for (let index = 0; index < batchSize; index++) { + for (let index = measuredCellCount; index < measuredCellCount + batchSize; index++) { children.push( cellRenderer({ - index: index + measuredCellCount, + index: index, isScrolling, key: keyMapper(index), parent: this, @@ -213,7 +234,7 @@ export default class Masonry extends PureComponent { style={{ boxSizing: 'border-box', direction: 'ltr', - height, + height: autoHeight ? 'auto' : height, overflowX: 'hidden', overflowY: estimateTotalHeight < height ? 'hidden' : 'auto', position: 'relative', @@ -430,6 +451,7 @@ type Position = { export type Positioner = (index: number) => Position; type Props = { + autoHeight: boolean, cellCount: number, cellMeasurerCache: CellMeasurerCache, cellPositioner: Positioner,