Skip to content

Commit

Permalink
Masonry component now works with WindowScroller and external scrollTo…
Browse files Browse the repository at this point in the history
…p prop
  • Loading branch information
Brian Vaughn committed Mar 26, 2017
1 parent f2bce88 commit cba3b0b
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 36 deletions.
10 changes: 10 additions & 0 deletions source/Masonry/Masonry.example.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
113 changes: 85 additions & 28 deletions source/Masonry/Masonry.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)
}
Expand All @@ -43,9 +46,22 @@ export default class GridExample extends PureComponent {
const {
columnWidth,
height,
gutterSize
gutterSize,
windowScrollerEnabled
} = this.state

let child

if (windowScrollerEnabled) {
child = (
<WindowScroller>
{this._renderAutoSizer}
</WindowScroller>
)
} else {
child = this._renderAutoSizer({ height })
}

return (
<ContentBox>
<ContentBoxHeader
Expand All @@ -60,6 +76,26 @@ export default class GridExample extends PureComponent {
Sizes are cached so that resize/reflow is fast and does not require re-measuring.
</ContentBoxParagraph>

<ContentBoxParagraph>
<label className={styles.checkboxLabel}>
<input
aria-label='Use WindowScroller?'
checked={windowScrollerEnabled}
className={styles.checkbox}
type='checkbox'
onChange={event => {
// HACK Because this demo switches between using WindowScroller and not,
// It's easier to clear the cache when toggling modes to avoid a partially stale state.
this._cache.clearAll()
this.setState({
windowScrollerEnabled: event.target.checked
})
}}
/>
Use <code>WindowScroller</code>?
</label>
</ContentBoxParagraph>

<InputRow>
<LabeledInput
label='Height'
Expand All @@ -81,7 +117,7 @@ export default class GridExample extends PureComponent {
columnWidth: parseInt(event.target.value, 10) || 200
}, () => {
this._calculateColumnCount()
this._initOrResetPositioner()
this._resetCellPositioner()
this._masonry.clearCellPositions()
})
}}
Expand All @@ -96,21 +132,15 @@ export default class GridExample extends PureComponent {
gutterSize: parseInt(event.target.value, 10) || 10
}, () => {
this._calculateColumnCount()
this._initOrResetPositioner()
this._resetCellPositioner()
this._masonry.recomputeCellPositions()
})
}}
value={gutterSize}
/>
</InputRow>

<AutoSizer
disableHeight
height={height}
onResize={this._onResize}
>
{this._renderMasonry}
</AutoSizer>
{child}
</ContentBox>
)
}
Expand Down Expand Up @@ -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 (
<AutoSizer
disableHeight
onResize={this._onResize}
scrollTop={this._scrollTop}
>
{this._renderMasonry}
</AutoSizer>
)
}

_renderMasonry ({ width }) {
this._width = width

this._calculateColumnCount()
this._initOrResetPositioner()
this._initCellPositioner()

const { height } = this.state
const { height, windowScrollerEnabled } = this.state

return (
<Masonry
autoHeight={windowScrollerEnabled}
cellCount={1000}
cellMeasurerCache={this._cache}
cellPositioner={this._cellPositioner}
cellRenderer={this._cellRenderer}
height={height}
height={windowScrollerEnabled ? this._height : height}
ref={this._setMasonryRef}
scrollTop={this._scrollTop}
width={width}
/>
)
}

_resetCellPositioner () {
const {
columnWidth,
gutterSize
} = this.state

this._cellPositioner.reset({
columnCount: this._columnCount,
columnWidth,
spacer: gutterSize
})
}

_setMasonryRef (ref) {
this._masonry = ref
}
Expand Down
66 changes: 61 additions & 5 deletions source/Masonry/Masonry.jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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')
Expand Down
28 changes: 25 additions & 3 deletions source/Masonry/Masonry.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default class Masonry extends PureComponent {
props: Props;

static defaultProps = {
autoHeight: false,
keyMapper: identity,
onCellsRendered: noop,
onScroll: noop,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -430,6 +451,7 @@ type Position = {
export type Positioner = (index: number) => Position;

type Props = {
autoHeight: boolean,
cellCount: number,
cellMeasurerCache: CellMeasurerCache,
cellPositioner: Positioner,
Expand Down

0 comments on commit cba3b0b

Please sign in to comment.