react-virtualized
Advanced tools
Comparing version 4.4.3 to 4.5.0
Changelog | ||
------------ | ||
#### 4.5.0 | ||
Added `onScroll` callback to `Grid`, `FlexTable`, and `VirtualScroll`. | ||
Added `scrollToCell` method to `Grid` and `scrollToRow` to `FlexTable`, and `VirtualScroll`. | ||
#### 4.4.3 | ||
@@ -5,0 +9,0 @@ Added `-ms-flex` and `-webkit-flex` browser prefixes to `FlexTable` cells. |
@@ -21,2 +21,3 @@ FlexTable | ||
| onRowsRendered | | Function | Callback invoked with information about the slice of rows that were just rendered: `({ startIndex, stopIndex }): void` | | ||
| onScroll | Function | | Callback invoked whenever the scroll offset changes within the inner scrollable region: `({ scrollTop }): void` | | ||
| rowClassName | String or Function | | CSS class to apply to all table rows (including the header row). This value may be either a static string or a function with the signature `(rowIndex: number): string`. Note that for the header row an index of `-1` is provided. | | ||
@@ -38,6 +39,14 @@ | rowGetter | Function | ✓ | Callback responsible for returning a data row given an index. `(index: int): any` | | ||
##### scrollToRow | ||
##### scrollToRow(rowIndex) | ||
Scroll the list to ensure the row at the specified index is visible. This method exists so that a user can forcefully scroll to the same row twice. (The `scrollToIndex` property would not change in that case and so it would not be picked up by VirtualScroll.) | ||
##### setScrollTop(scrollTop) | ||
Set the `scrollTop` position within the inner scroll container. | ||
Normally it is best to let `FlexTable` manage this properties or to use a method like `scrollToRow`. | ||
This method enables `FlexTable` to be scroll-synced to another react-virtualized component though. | ||
It is appropriate to use in that case. | ||
### Class names | ||
@@ -44,0 +53,0 @@ |
@@ -16,2 +16,3 @@ Grid | ||
| onSectionRendered | Function | | Callback invoked with information about the section of the Grid that was just rendered: `({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }): void` | | ||
| onScroll | Function | | Callback invoked whenever the scroll offset changes within the inner scrollable region: `({ scrollLeft, scrollTop }): void` | | ||
| renderCell | Function | ✓ | Responsible for rendering a cell given an row and column index: `({ columnIndex: number, rowIndex: number }): PropTypes.node` | | ||
@@ -33,3 +34,3 @@ | rowsCount | Number | ✓ | Number of rows in grid. | | ||
##### scrollToCell | ||
##### scrollToCell({ scrollToColumn, scrollToRow }) | ||
@@ -40,2 +41,10 @@ Updates the Grid to ensure the cell at the specified row and column indices is visible. | ||
##### setScrollPosition({ scrollLeft, scrollTop }) | ||
Set the `scrollLeft` and `scrollTop` position within the inner scroll container. | ||
Normally it is best to let `Grid` manage these properties or to use a method like `scrollToCell`. | ||
This method enables a `Grid` to be scroll-synced to another react-virtualized component though. | ||
It is appropriate to use in that case. | ||
### Class names | ||
@@ -42,0 +51,0 @@ |
@@ -13,2 +13,3 @@ VirtualScroll | ||
| onRowsRendered | | Function | Callback invoked with information about the slice of rows that were just rendered: `({ startIndex, stopIndex }): void` | | ||
| onScroll | Function | | Callback invoked whenever the scroll offset changes within the inner scrollable region: `({ scrollTop }): void` | | ||
| rowHeight | Number or Function | ✓ | Either a fixed row height (number) or a function that returns the height of a row given its index: `(index: number): number` | | ||
@@ -26,6 +27,15 @@ | rowRenderer | Function | ✓ | Responsbile for rendering a row given an index. Signature should look like `(index: number): React.PropTypes.node` | | ||
##### scrollToRow | ||
##### scrollToRow(rowIndex) | ||
Scroll the list to ensure the row at the specified index is visible. This method exists so that a user can forcefully scroll to the same row twice. (The `scrollToIndex` property would not change in that case and so it would not be picked up by VirtualScroll.) | ||
##### setScrollTop(scrollTop) | ||
Set the `scrollTop` position within the inner scroll container. | ||
Normally it is best to let `VirtualScroll` manage this properties or to use a method like `scrollToRow`. | ||
This method enables `VirtualScroll` to be scroll-synced to another react-virtualized component though. | ||
It is appropriate to use in that case. | ||
### Class names | ||
@@ -32,0 +42,0 @@ |
@@ -6,3 +6,3 @@ { | ||
"user": "bvaughn", | ||
"version": "4.4.3", | ||
"version": "4.5.0", | ||
"scripts": { | ||
@@ -9,0 +9,0 @@ "build": "npm run build:css && npm run build:dist && npm run build:demo", |
@@ -70,2 +70,8 @@ /** @flow */ | ||
/** | ||
* Callback invoked whenever the scroll offset changes within the inner scrollable region. | ||
* This callback can be used to sync scrolling between lists, tables, or grids. | ||
* ({ scrollTop }): void | ||
*/ | ||
onScroll: PropTypes.func.isRequired, | ||
/** | ||
* Optional CSS class to apply to all table rows (including the header row). | ||
@@ -108,2 +114,3 @@ * This property can be a CSS class name (string) or a function that returns a class name. | ||
onRowsRendered: () => null, | ||
onScroll: () => null, | ||
verticalPadding: 0 | ||
@@ -132,2 +139,12 @@ } | ||
/** | ||
* Set the :scrollTop position within the inner scroll container. | ||
* Normally it is best to let FlexTable manage this properties or to use a method like :scrollToRow. | ||
* This method enables FlexTable to be scroll-synced to another react-virtualized component though. | ||
* It is appropriate to use in that case. | ||
*/ | ||
setScrollTop (scrollTop) { | ||
this.refs.VirtualScroll.setScrollTop(scrollTop) | ||
} | ||
render () { | ||
@@ -141,2 +158,3 @@ const { | ||
onRowsRendered, | ||
onScroll, | ||
rowClassName, | ||
@@ -178,2 +196,3 @@ rowHeight, | ||
onRowsRendered={onRowsRendered} | ||
onScroll={onScroll} | ||
rowHeight={rowHeight} | ||
@@ -180,0 +199,0 @@ rowRenderer={rowRenderer} |
import React from 'react' | ||
import { findDOMNode, render } from 'react-dom' | ||
import { Simulate } from 'react-addons-test-utils' | ||
import TestUtils from 'react-addons-test-utils' | ||
import { renderIntoDocument, Simulate } from 'react-addons-test-utils' | ||
import Immutable from 'immutable' | ||
@@ -38,22 +37,22 @@ import FlexColumn from './FlexColumn' | ||
function getMarkup ({ | ||
cellRenderer = undefined, | ||
cellDataGetter = undefined, | ||
className = undefined, | ||
cellRenderer, | ||
cellDataGetter, | ||
className, | ||
disableSort = false, | ||
headerClassName = undefined, | ||
headerClassName, | ||
headerHeight = 20, | ||
height = 100, | ||
noRowsRenderer = undefined, | ||
onHeaderClick = undefined, | ||
onRowClick = undefined, | ||
onRowsRendered = undefined, | ||
rowClassName = undefined, | ||
noRowsRenderer, | ||
onHeaderClick, | ||
onRowClick, | ||
onRowsRendered, | ||
rowClassName, | ||
rowGetter = immutableRowGetter, | ||
rowHeight = 10, | ||
rowsCount = list.size, | ||
scrollToIndex = undefined, | ||
sort = undefined, | ||
sortBy = undefined, | ||
sortDirection = undefined, | ||
styleSheet = undefined, | ||
scrollToIndex, | ||
sort, | ||
sortBy, | ||
sortDirection, | ||
styleSheet, | ||
width = 100 | ||
@@ -100,3 +99,3 @@ } = {}) { | ||
function renderTable (props) { | ||
const flexTable = TestUtils.renderIntoDocument(getMarkup(props)) | ||
const flexTable = renderIntoDocument(getMarkup(props)) | ||
@@ -110,3 +109,3 @@ // Allow initial setImmediate() to set :scrollTop | ||
// Use ReactDOM.render for certain tests so that props changes will update the existing component | ||
// TestUtils.renderIntoDocument creates a new component/instance each time | ||
// renderIntoDocument creates a new component/instance each time | ||
function renderOrUpdateTable (props) { | ||
@@ -502,2 +501,5 @@ let flexTable = render(getMarkup(props), node) | ||
}) | ||
// TODO Add tests for :scrollToRow and :setScrollTop. | ||
// This probably requires the creation of an inner test-only class with refs. | ||
}) |
@@ -41,3 +41,5 @@ /** @flow */ | ||
this._onScrollToRowChange = this._onScrollToRowChange.bind(this) | ||
this._renderCell = this._renderCell.bind(this) | ||
this._renderBodyCell = this._renderBodyCell.bind(this) | ||
this._renderHeaderCell = this._renderHeaderCell.bind(this) | ||
this._renderLeftSideCell = this._renderLeftSideCell.bind(this) | ||
} | ||
@@ -65,2 +67,11 @@ | ||
<ContentBoxParagraph> | ||
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. | ||
</ContentBoxParagraph> | ||
<ContentBoxParagraph> | ||
This example also shows 3 <code>Grid</code> components with synchronized scrolling to demonstrate fixed headers and columns. | ||
</ContentBoxParagraph> | ||
<ContentBoxParagraph> | ||
<label className={styles.checkboxLabel}> | ||
@@ -118,19 +129,59 @@ <input | ||
<div> | ||
<AutoSizer disableHeight> | ||
<Grid | ||
ref='Grid' | ||
className={styles.Grid} | ||
columnWidth={this._getColumnWidth} | ||
columnsCount={columnsCount} | ||
height={height} | ||
noContentRenderer={this._noContentRenderer} | ||
renderCell={this._renderCell} | ||
rowHeight={useDynamicRowHeights ? this._getRowHeight : rowHeight} | ||
rowsCount={rowsCount} | ||
scrollToColumn={scrollToColumn} | ||
scrollToRow={scrollToRow} | ||
width={0} | ||
/> | ||
</AutoSizer> | ||
<div className={styles.GridRow}> | ||
<div | ||
className={styles.LeftSideGridContainer} | ||
style={{ marginTop: rowHeight }} | ||
> | ||
<Grid | ||
ref='LeftSideGrid' | ||
className={styles.LeftSideGrid} | ||
columnWidth={50} | ||
columnsCount={1} | ||
height={height} | ||
onScroll={({ scrollLeft, scrollTop }) => this.refs.BodyGrid.setScrollPosition({ scrollTop })} | ||
renderCell={this._renderLeftSideCell} | ||
rowHeight={rowHeight} | ||
rowsCount={rowsCount} | ||
width={50} | ||
/> | ||
</div> | ||
<div className={styles.GridColumn}> | ||
<div> | ||
<AutoSizer disableHeight> | ||
<Grid | ||
ref='HeaderGrid' | ||
className={styles.HeaderGrid} | ||
columnWidth={this._getColumnWidth} | ||
columnsCount={columnsCount} | ||
height={rowHeight} | ||
renderCell={this._renderHeaderCell} | ||
rowHeight={rowHeight} | ||
rowsCount={1} | ||
width={0} | ||
/> | ||
</AutoSizer> | ||
</div> | ||
<div> | ||
<AutoSizer disableHeight> | ||
<Grid | ||
ref='BodyGrid' | ||
className={styles.BodyGrid} | ||
columnWidth={this._getColumnWidth} | ||
columnsCount={columnsCount} | ||
height={height} | ||
noContentRenderer={this._noContentRenderer} | ||
onScroll={({ scrollLeft, scrollTop }) => { | ||
this.refs.LeftSideGrid.setScrollPosition({ scrollTop }) | ||
this.refs.HeaderGrid.setScrollPosition({ scrollLeft }) | ||
}} | ||
renderCell={this._renderBodyCell} | ||
rowHeight={useDynamicRowHeights ? this._getRowHeight : rowHeight} | ||
rowsCount={rowsCount} | ||
scrollToColumn={scrollToColumn} | ||
scrollToRow={scrollToRow} | ||
width={0} | ||
/> | ||
</AutoSizer> | ||
</div> | ||
</div> | ||
</div> | ||
@@ -144,6 +195,4 @@ </ContentBox> | ||
case 0: | ||
return 40 | ||
return 100 | ||
case 1: | ||
return 100 | ||
case 2: | ||
return 300 | ||
@@ -172,3 +221,3 @@ default: | ||
_renderCell ({ columnIndex, rowIndex }) { | ||
_renderBodyCell ({ columnIndex, rowIndex }) { | ||
const { list } = this.props | ||
@@ -182,8 +231,5 @@ const rowClass = this._getRowClassName(rowIndex) | ||
case 0: | ||
content = datum.name.charAt(0) | ||
content = datum.name | ||
break | ||
case 1: | ||
content = datum.name | ||
break | ||
case 2: | ||
content = datum.random | ||
@@ -197,8 +243,46 @@ break | ||
const classNames = cn(rowClass, styles.cell, { | ||
[styles.centeredCell]: columnIndex > 2, | ||
[styles.centeredCell]: columnIndex > 2 | ||
}) | ||
return ( | ||
<div className={classNames}> | ||
{content} | ||
</div> | ||
) | ||
} | ||
_renderHeaderCell ({ columnIndex, rowIndex }) { | ||
let content | ||
switch (columnIndex) { | ||
case 0: | ||
content = 'Name' | ||
break | ||
case 1: | ||
content = 'Lorem Ipsum' | ||
break | ||
default: | ||
content = `C${columnIndex}` | ||
break | ||
} | ||
const classNames = cn(styles.headerCell, { | ||
[styles.centeredCell]: columnIndex > 2 | ||
}) | ||
return ( | ||
<div className={classNames}> | ||
{content} | ||
</div> | ||
) | ||
} | ||
_renderLeftSideCell ({ columnIndex, rowIndex }) { | ||
const { list } = this.props | ||
const datum = list.get(rowIndex) | ||
const classNames = cn(styles.cell, { | ||
[styles.letterCell]: columnIndex === 0 | ||
}) | ||
const style = columnIndex === 0 | ||
? { backgroundColor: datum.color } | ||
: {} | ||
const style = { backgroundColor: datum.color } | ||
@@ -210,3 +294,3 @@ return ( | ||
> | ||
{content} | ||
{datum.name.charAt(0)} | ||
</div> | ||
@@ -246,3 +330,3 @@ ) | ||
this.refs.Grid.scrollToCell({ scrollToColumn, scrollToRow }) | ||
this.refs.BodyGrid.scrollToCell({ scrollToColumn, scrollToRow }) | ||
} | ||
@@ -260,4 +344,4 @@ | ||
this.refs.Grid.scrollToCell({ scrollToColumn, scrollToRow }) | ||
this.refs.BodyGrid.scrollToCell({ scrollToColumn, scrollToRow }) | ||
} | ||
} |
/** @flow */ | ||
import { | ||
computeCellMetadataAndUpdateScrollOffsetHelper, | ||
createCallbackMemoizer, | ||
getUpdatedOffsetForIndex, | ||
getVisibleCellIndices, | ||
initCellMetadata, | ||
initOnSectionRenderedHelper, | ||
updateScrollIndexHelper | ||
@@ -56,2 +56,9 @@ } from '../utils' | ||
/** | ||
* Callback invoked whenever the scroll offset changes within the inner scrollable region. | ||
* This callback can be used to sync scrolling between lists, tables, or grids. | ||
* ({ scrollLeft, scrollTop }): void | ||
*/ | ||
onScroll: PropTypes.func.isRequired, | ||
/** | ||
* Callback invoked with information about the section of the Grid that was just rendered. | ||
@@ -97,2 +104,3 @@ * ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }): void | ||
noContentRenderer: () => null, | ||
onScroll: () => null, | ||
onSectionRendered: () => null | ||
@@ -112,3 +120,4 @@ } | ||
// Invokes onSectionRendered callback only when start/stop row or column indices change | ||
this._OnGridRenderedHelper = initOnSectionRenderedHelper() | ||
this._onGridRenderedMemoizer = createCallbackMemoizer() | ||
this._onScrollMemoizer = createCallbackMemoizer(false) | ||
@@ -146,2 +155,27 @@ // Bind functions to instance so they don't lose context when passed around | ||
/** | ||
* Set the :scrollLeft and :scrollTop position within the inner scroll container. | ||
* Normally it is best to let Grid manage these properties or to use a method like :scrollToCell. | ||
* This method enables Grid to be scroll-synced to another react-virtualized component though. | ||
* It is appropriate to use in that case. | ||
*/ | ||
setScrollPosition ({ scrollLeft, scrollTop }) { | ||
const props = {} | ||
if (scrollLeft >= 0) { | ||
props.scrollLeft = scrollLeft | ||
} | ||
if (scrollTop >= 0) { | ||
props.scrollTop = scrollTop | ||
} | ||
if ( | ||
scrollLeft >= 0 && scrollLeft !== this.state.scrollLeft || | ||
scrollTop >= 0 && scrollTop !== this.state.scrollTop | ||
) { | ||
this.setState(props) | ||
} | ||
} | ||
componentDidMount () { | ||
@@ -437,3 +471,3 @@ const { scrollToColumn, scrollToRow } = this.props | ||
this._OnGridRenderedHelper({ | ||
this._onGridRenderedMemoizer({ | ||
callback: onSectionRendered, | ||
@@ -650,3 +684,3 @@ indices: { | ||
// We can avoid that by doing some simple bounds checking to ensure that scrollTop never exceeds the total height. | ||
const { height, width } = this.props | ||
const { height, onScroll, width } = this.props | ||
const totalRowsHeight = this._getTotalRowsHeight() | ||
@@ -658,5 +692,15 @@ const totalColumnsWidth = this._getTotalColumnsWidth() | ||
this._setNextStateForScrollHelper({ scrollLeft, scrollTop }) | ||
this._onScrollMemoizer({ | ||
callback: onScroll, | ||
indices: { | ||
scrollLeft, | ||
scrollTop | ||
} | ||
}) | ||
} | ||
_onWheel (event) { | ||
const{ onScroll } = this.props | ||
const scrollLeft = this.refs.scrollingContainer.scrollLeft | ||
@@ -666,3 +710,11 @@ const scrollTop = this.refs.scrollingContainer.scrollTop | ||
this._setNextStateForScrollHelper({ scrollLeft, scrollTop }) | ||
this._onScrollMemoizer({ | ||
callback: onScroll, | ||
indices: { | ||
scrollLeft, | ||
scrollTop | ||
} | ||
}) | ||
} | ||
} |
import React from 'react' | ||
import { findDOMNode, render } from 'react-dom' | ||
import TestUtils from 'react-addons-test-utils' | ||
import { renderIntoDocument, Simulate } from 'react-addons-test-utils' | ||
import Grid from './Grid' | ||
@@ -18,12 +18,13 @@ | ||
function getMarkup ({ | ||
className = undefined, | ||
className, | ||
columnsCount = NUM_COLUMNS, | ||
columnWidth = 50, | ||
height = 100, | ||
noContentRenderer = undefined, | ||
onSectionRendered = undefined, | ||
noContentRenderer, | ||
onSectionRendered, | ||
onScroll, | ||
rowHeight = 20, | ||
rowsCount = NUM_ROWS, | ||
scrollToColumn = undefined, | ||
scrollToRow = undefined, | ||
scrollToColumn, | ||
scrollToRow, | ||
width = 200 | ||
@@ -47,2 +48,3 @@ } = {}) { | ||
onSectionRendered={onSectionRendered} | ||
onScroll={onScroll} | ||
renderCell={renderCell} | ||
@@ -59,3 +61,3 @@ rowHeight={rowHeight} | ||
function renderGrid (props) { | ||
const grid = TestUtils.renderIntoDocument(getMarkup(props)) | ||
const grid = renderIntoDocument(getMarkup(props)) | ||
@@ -69,3 +71,3 @@ // Allow initial setImmediate() to set :scrollTop | ||
// Use ReactDOM.render for certain tests so that props changes will update the existing component | ||
// TestUtils.renderIntoDocument creates a new component/instance each time | ||
// renderIntoDocument creates a new component/instance each time | ||
function renderOrUpdateGrid (props) { | ||
@@ -327,2 +329,39 @@ let grid = render(getMarkup(props), node) | ||
}) | ||
describe('onScroll', () => { | ||
function helper ({ grid, scrollLeft, scrollTop }) { | ||
const target = { scrollLeft, scrollTop } | ||
grid.refs.scrollingContainer = target // HACK to work around _onScroll target check | ||
Simulate.scroll(findDOMNode(grid), { target }) | ||
} | ||
it('should trigger callback when component scrolls horizontally', () => { | ||
const onScrollCalls = [] | ||
const grid = renderGrid({ | ||
onScroll: params => onScrollCalls.push(params) | ||
}) | ||
helper({ | ||
grid, | ||
scrollLeft: 100, | ||
scrollTop: 0 | ||
}) | ||
expect(onScrollCalls).toEqual([{ scrollLeft: 100, scrollTop: 0 }]) | ||
}) | ||
it('should trigger callback when component scrolls horizontally', () => { | ||
const onScrollCalls = [] | ||
const grid = renderGrid({ | ||
onScroll: params => onScrollCalls.push(params) | ||
}) | ||
helper({ | ||
grid, | ||
scrollLeft: 0, | ||
scrollTop: 100 | ||
}) | ||
expect(onScrollCalls).toEqual([{ scrollLeft: 0, scrollTop: 100 }]) | ||
}) | ||
}) | ||
// TODO Add tests for :scrollToCell and :setScrollPosition. | ||
// This probably requires the creation of an inner test-only class with refs. | ||
}) |
@@ -51,2 +51,24 @@ /** | ||
/** | ||
* Helper utility that updates the specified callback whenever any of the specified indices have changed. | ||
*/ | ||
export function createCallbackMemoizer (requireAllKeys = true) { | ||
let cachedIndices = {} | ||
return ({ | ||
callback, | ||
indices | ||
}) => { | ||
const keys = Object.keys(indices) | ||
const allInitialized = !requireAllKeys || keys.every(key => indices[key] >= 0) | ||
const indexChanged = keys.some(key => cachedIndices[key] !== indices[key]) | ||
cachedIndices = indices | ||
if (allInitialized && indexChanged) { | ||
callback(indices) | ||
} | ||
} | ||
} | ||
/** | ||
* Binary search function inspired by react-infinite. | ||
@@ -64,2 +86,4 @@ */ | ||
// TODO Add better guards here against NaN offset | ||
while (low <= high) { | ||
@@ -142,2 +166,4 @@ middle = low + Math.floor((high - low) / 2) | ||
// TODO Add better guards here against NaN offset | ||
let start = findNearestCell({ | ||
@@ -204,24 +230,2 @@ cellMetadata, | ||
/** | ||
* Helper utility that updates the specified callback whenever any of the specified indices have changed. | ||
*/ | ||
export function initOnSectionRenderedHelper () { | ||
let cachedIndices = {} | ||
return ({ | ||
callback, | ||
indices | ||
}) => { | ||
const keys = Object.keys(indices) | ||
const allInitialized = keys.every(key => indices[key] >= 0) | ||
const indexChanged = keys.some(key => cachedIndices[key] !== indices[key]) | ||
cachedIndices = indices | ||
if (allInitialized && indexChanged) { | ||
callback(indices) | ||
} | ||
} | ||
} | ||
/** | ||
* Helper function that determines when to update scroll offsets to ensure that a scroll-to-index remains visible. | ||
@@ -228,0 +232,0 @@ * |
import { | ||
computeCellMetadataAndUpdateScrollOffsetHelper, | ||
createCallbackMemoizer, | ||
getUpdatedOffsetForIndex, | ||
getVisibleCellIndices, | ||
initCellMetadata, | ||
initOnSectionRenderedHelper, | ||
updateScrollIndexHelper | ||
@@ -131,83 +131,3 @@ } from './utils' | ||
describe('getUpdatedOffsetForIndex', () => { | ||
function testHelper (targetIndex, currentOffset, cellMetadata = getCellMetadata()) { | ||
return getUpdatedOffsetForIndex({ | ||
cellMetadata, | ||
containerSize: 50, | ||
currentOffset, | ||
targetIndex | ||
}) | ||
} | ||
it('should scroll to the beginning', () => { | ||
expect(testHelper(0, 100)).toEqual(0) | ||
}) | ||
it('should scroll forward to the middle', () => { | ||
expect(testHelper(4, 0)).toEqual(20) | ||
}) | ||
it('should scroll backward to the middle', () => { | ||
expect(testHelper(2, 100)).toEqual(30) | ||
}) | ||
it('should not scroll if an item is already visible', () => { | ||
expect(testHelper(2, 20)).toEqual(20) | ||
}) | ||
it('should scroll to the end', () => { | ||
expect(testHelper(8, 0)).toEqual(110) | ||
}) | ||
it('should not scroll too far backward', () => { | ||
expect(testHelper(-5, 0)).toEqual(0) | ||
}) | ||
it('should not scroll too far forward', () => { | ||
expect(testHelper(105, 0)).toEqual(110) | ||
}) | ||
}) | ||
describe('getVisibleCellIndices', () => { | ||
function testHelper (currentOffset, cellMetadata = getCellMetadata()) { | ||
return getVisibleCellIndices({ | ||
cellCount: cellMetadata.length, | ||
cellMetadata, | ||
containerSize: 50, | ||
currentOffset | ||
}) | ||
} | ||
it('should handle unscrolled', () => { | ||
const { start, stop } = testHelper(0) | ||
expect(start).toEqual(0) | ||
expect(stop).toEqual(3) | ||
}) | ||
it('should handle scrolled to the middle', () => { | ||
const { start, stop } = testHelper(50) | ||
expect(start).toEqual(3) | ||
expect(stop).toEqual(5) | ||
}) | ||
it('should handle scrolled to the end', () => { | ||
const { start, stop } = testHelper(110) | ||
expect(start).toEqual(6) | ||
expect(stop).toEqual(8) | ||
}) | ||
it('should handle scrolled past the end', () => { | ||
const { start, stop } = testHelper(200) | ||
expect(start).toEqual(8) // TODO Should this actually be 6? | ||
expect(stop).toEqual(8) | ||
}) | ||
it('should handle scrolled past the beginning', () => { | ||
const { start, stop } = testHelper(-50) | ||
expect(start).toEqual(0) | ||
expect(stop).toEqual(3) | ||
}) | ||
}) | ||
describe('initOnSectionRenderedHelper', () => { | ||
describe('createCallbackMemoizer', () => { | ||
function OnRowsRendered () { | ||
@@ -232,3 +152,3 @@ let numCalls = 0 | ||
const util = new OnRowsRendered() | ||
const helper = initOnSectionRenderedHelper() | ||
const helper = createCallbackMemoizer() | ||
helper({ | ||
@@ -254,3 +174,3 @@ callback: util.update, | ||
const util = new OnRowsRendered() | ||
const helper = initOnSectionRenderedHelper() | ||
const helper = createCallbackMemoizer() | ||
helper({ | ||
@@ -268,5 +188,20 @@ callback: util.update, | ||
it('should call onRowsRendered if startIndex and stopIndex are invalid but :requireAllKeys is false', () => { | ||
const util = new OnRowsRendered() | ||
const helper = createCallbackMemoizer(false) | ||
helper({ | ||
callback: util.update, | ||
indices: { | ||
startIndex: undefined, | ||
stopIndex: 1 | ||
} | ||
}) | ||
expect(util.numCalls()).toEqual(1) | ||
expect(util.startIndex()).toEqual(undefined) | ||
expect(util.stopIndex()).toEqual(1) | ||
}) | ||
it('should not call onRowsRendered if startIndex or stopIndex have not changed', () => { | ||
const util = new OnRowsRendered() | ||
const helper = initOnSectionRenderedHelper() | ||
const helper = createCallbackMemoizer() | ||
helper({ | ||
@@ -294,3 +229,3 @@ callback: util.update, | ||
const util = new OnRowsRendered() | ||
const helper = initOnSectionRenderedHelper() | ||
const helper = createCallbackMemoizer() | ||
helper({ | ||
@@ -329,2 +264,82 @@ callback: util.update, | ||
describe('getUpdatedOffsetForIndex', () => { | ||
function testHelper (targetIndex, currentOffset, cellMetadata = getCellMetadata()) { | ||
return getUpdatedOffsetForIndex({ | ||
cellMetadata, | ||
containerSize: 50, | ||
currentOffset, | ||
targetIndex | ||
}) | ||
} | ||
it('should scroll to the beginning', () => { | ||
expect(testHelper(0, 100)).toEqual(0) | ||
}) | ||
it('should scroll forward to the middle', () => { | ||
expect(testHelper(4, 0)).toEqual(20) | ||
}) | ||
it('should scroll backward to the middle', () => { | ||
expect(testHelper(2, 100)).toEqual(30) | ||
}) | ||
it('should not scroll if an item is already visible', () => { | ||
expect(testHelper(2, 20)).toEqual(20) | ||
}) | ||
it('should scroll to the end', () => { | ||
expect(testHelper(8, 0)).toEqual(110) | ||
}) | ||
it('should not scroll too far backward', () => { | ||
expect(testHelper(-5, 0)).toEqual(0) | ||
}) | ||
it('should not scroll too far forward', () => { | ||
expect(testHelper(105, 0)).toEqual(110) | ||
}) | ||
}) | ||
describe('getVisibleCellIndices', () => { | ||
function testHelper (currentOffset, cellMetadata = getCellMetadata()) { | ||
return getVisibleCellIndices({ | ||
cellCount: cellMetadata.length, | ||
cellMetadata, | ||
containerSize: 50, | ||
currentOffset | ||
}) | ||
} | ||
it('should handle unscrolled', () => { | ||
const { start, stop } = testHelper(0) | ||
expect(start).toEqual(0) | ||
expect(stop).toEqual(3) | ||
}) | ||
it('should handle scrolled to the middle', () => { | ||
const { start, stop } = testHelper(50) | ||
expect(start).toEqual(3) | ||
expect(stop).toEqual(5) | ||
}) | ||
it('should handle scrolled to the end', () => { | ||
const { start, stop } = testHelper(110) | ||
expect(start).toEqual(6) | ||
expect(stop).toEqual(8) | ||
}) | ||
it('should handle scrolled past the end', () => { | ||
const { start, stop } = testHelper(200) | ||
expect(start).toEqual(8) // TODO Should this actually be 6? | ||
expect(stop).toEqual(8) | ||
}) | ||
it('should handle scrolled past the beginning', () => { | ||
const { start, stop } = testHelper(-50) | ||
expect(start).toEqual(0) | ||
expect(stop).toEqual(3) | ||
}) | ||
}) | ||
describe('updateScrollIndexHelper', () => { | ||
@@ -331,0 +346,0 @@ function helper ({ |
/** @flow */ | ||
import { | ||
computeCellMetadataAndUpdateScrollOffsetHelper, | ||
createCallbackMemoizer, | ||
getUpdatedOffsetForIndex, | ||
getVisibleCellIndices, | ||
initCellMetadata, | ||
initOnSectionRenderedHelper, | ||
updateScrollIndexHelper | ||
@@ -45,2 +45,8 @@ } from '../utils' | ||
/** | ||
* Callback invoked whenever the scroll offset changes within the inner scrollable region. | ||
* This callback can be used to sync scrolling between lists, tables, or grids. | ||
* ({ scrollTop }): void | ||
*/ | ||
onScroll: PropTypes.func.isRequired, | ||
/** | ||
* Either a fixed row height (number) or a function that returns the height of a row given its index. | ||
@@ -60,3 +66,4 @@ * (index: number): number | ||
noRowsRenderer: () => null, | ||
onRowsRendered: () => null | ||
onRowsRendered: () => null, | ||
onScroll: () => null | ||
} | ||
@@ -74,3 +81,4 @@ | ||
// Invokes onRowsRendered callback only when start/stop row indices change | ||
this._OnRowsRenderedHelper = initOnSectionRenderedHelper() | ||
this._onRowsRenderedMemoizer = createCallbackMemoizer() | ||
this._onScrollMemoizer = createCallbackMemoizer(false) | ||
@@ -106,2 +114,14 @@ // Bind functions to instance so they don't lose context when passed around | ||
/** | ||
* Set the :scrollTop position within the inner scroll container. | ||
* Normally it is best to let VirtualScroll manage this properties or to use a method like :scrollToRow. | ||
* This method enables VirtualScroll to be scroll-synced to another react-virtualized component though. | ||
* It is appropriate to use in that case. | ||
*/ | ||
setScrollTop (scrollTop) { | ||
scrollTop = Number.isNaN(scrollTop) ? 0 : scrollTop | ||
this.setState({ scrollTop }) | ||
} | ||
componentDidMount () { | ||
@@ -312,3 +332,3 @@ const { scrollToIndex } = this.props | ||
this._OnRowsRenderedHelper({ | ||
this._onRowsRenderedMemoizer({ | ||
callback: onRowsRendered, | ||
@@ -463,3 +483,3 @@ indices: { | ||
// We can avoid that by doing some simple bounds checking to ensure that scrollTop never exceeds the total height. | ||
const { height } = this.props | ||
const { height, onScroll } = this.props | ||
const totalRowsHeight = this._getTotalRowsHeight() | ||
@@ -469,9 +489,25 @@ const scrollTop = Math.min(totalRowsHeight - height, event.target.scrollTop) | ||
this._setNextStateForScrollHelper({ scrollTop }) | ||
this._onScrollMemoizer({ | ||
callback: onScroll, | ||
indices: { | ||
scrollTop | ||
} | ||
}) | ||
} | ||
_onWheel (event) { | ||
const{ onScroll } = this.props | ||
const scrollTop = this.refs.scrollingContainer.scrollTop | ||
this._setNextStateForScrollHelper({ scrollTop }) | ||
this._onScrollMemoizer({ | ||
callback: onScroll, | ||
indices: { | ||
scrollTop | ||
} | ||
}) | ||
} | ||
} |
import React from 'react' | ||
import { findDOMNode, render } from 'react-dom' | ||
import TestUtils from 'react-addons-test-utils' | ||
import { renderIntoDocument, Simulate } from 'react-addons-test-utils' | ||
import Immutable from 'immutable' | ||
@@ -22,10 +22,11 @@ import VirtualScroll from './VirtualScroll' | ||
function getMarkup ({ | ||
className = undefined, | ||
className, | ||
height = 100, | ||
noRowsRenderer = undefined, | ||
onRowsRendered = undefined, | ||
noRowsRenderer, | ||
onRowsRendered, | ||
onScroll, | ||
rowHeight = 10, | ||
rowsCount = list.size, | ||
scrollToIndex = undefined, | ||
styleSheet = undefined | ||
scrollToIndex, | ||
styleSheet | ||
} = {}) { | ||
@@ -49,2 +50,3 @@ function rowRenderer (index) { | ||
onRowsRendered={onRowsRendered} | ||
onScroll={onScroll} | ||
rowHeight={rowHeight} | ||
@@ -60,3 +62,3 @@ rowRenderer={rowRenderer} | ||
function renderList (props) { | ||
const virtualScroll = TestUtils.renderIntoDocument(getMarkup(props)) | ||
const virtualScroll = renderIntoDocument(getMarkup(props)) | ||
@@ -70,3 +72,3 @@ // Allow initial setImmediate() to set :scrollTop | ||
// Use ReactDOM.render for certain tests so that props changes will update the existing component | ||
// TestUtils.renderIntoDocument creates a new component/instance each time | ||
// renderIntoDocument creates a new component/instance each time | ||
function renderOrUpdateList (props) { | ||
@@ -243,2 +245,20 @@ let virtualScroll = render(getMarkup(props), node) | ||
}) | ||
describe('onScroll', () => { | ||
it('should trigger callback when component scrolls', () => { | ||
const onScrollCalls = [] | ||
const list = renderList({ | ||
onScroll: params => onScrollCalls.push(params) | ||
}) | ||
const target = { | ||
scrollTop: 100 | ||
} | ||
list.refs.scrollingContainer = target // HACK to work around _onScroll target check | ||
Simulate.scroll(findDOMNode(list), { target }) | ||
expect(onScrollCalls).toEqual([{ scrollTop: 100 }]) | ||
}) | ||
}) | ||
// TODO Add tests for :scrollToRow and :setScrollTop. | ||
// This probably requires the creation of an inner test-only class with refs. | ||
}) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
660956
8211