diff --git a/README.md b/README.md
index daffaa576..489e78a67 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,7 @@ Examples for each component can be seen in [the documentation](docs/README.md).
Here are some online demos of each component:
+* [ArrowKeyStepper](https://bvaughn.github.io/react-virtualized/?component=ArrowKeyStepper)
* [AutoSizer](https://bvaughn.github.io/react-virtualized/?component=AutoSizer)
* [ColumnSizer](https://bvaughn.github.io/react-virtualized/?component=ColumnSizer)
* [FlexTable](https://bvaughn.github.io/react-virtualized/?component=FlexTable)
diff --git a/docs/ArrowKeyStepper.md b/docs/ArrowKeyStepper.md
new file mode 100644
index 000000000..e88f15cc9
--- /dev/null
+++ b/docs/ArrowKeyStepper.md
@@ -0,0 +1,59 @@
+ArrowKeyStepper
+---------------
+
+High-order component that decorates another virtualized component and responds to arrow-key events by scrolling one row or column at a time.
+This provides a snap-to behavior rather than the default browser scrolling behavior.
+
+Note that unlike the other HOCs in react-virtualized, the `ArrowKeyStepper` adds a `
` element around its children in order to attach a key-down event handler.
+The appearance of this wrapper element can be customized using the `className` property.
+
+### Prop Types
+| Property | Type | Required? | Description |
+|:---|:---|:---:|:---|
+| children | Function | ✓ | Function respondible for rendering children. This function should implement the following signature: `({ onKeyDown, onSectionRendered, scrollToColumn, scrollToRow }) => PropTypes.element` |
+| className | String | | CSS class name to attach to the wrapper `
`. |
+| columnsCount | Number | ✓ | Number of columns in grid; for `FlexTable` and `VirtualScroll` this property should always be `1`. |
+| rowsCount | Number | ✓ | Number of rows in grid. |
+
+### Children function
+
+The child function is passed the following named parameters:
+
+| Parameter | Type | Description |
+|:---|:---|:---:|
+| onKeyDown | Function | Key-down event handler to be attached to the DOM hierarchy. |
+| onSectionRendered | Function | Pass-through callback to be attached to child component; informs the key-stepper which range of cells are currently visible. |
+| scrollToColumn | Number | Specifies which column in the child component should be visible |
+| scrollToRow | Number | Specifies which row in the child component should be visible |
+
+### Examples
+
+You can decorate any virtualized component (eg. `FlexTable`, `Grid`, or `VirtualScroll`) with arrow-key snapping like so:
+
+```javascript
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { ArrowKeyStepper, Grid } from 'react-virtualized';
+import 'react-virtualized/styles.css'; // only needs to be imported once
+
+ReactDOM.render(
+
+ {({ onKeyDown, onSectionRendered, scrollToColumn, scrollToRow }) => (
+
+
+
+ )}
+ ,
+ document.getElementById('example')
+);
+```
diff --git a/docs/AutoSizer.md b/docs/AutoSizer.md
index 5ddd7529c..e86c7ccc4 100644
--- a/docs/AutoSizer.md
+++ b/docs/AutoSizer.md
@@ -6,7 +6,7 @@ High-order component that automatically adjusts the width and height of a single
### Prop Types
| Property | Type | Required? | Description |
|:---|:---|:---:|:---|
-| children | PropTypes.Element | ✓ | Function respondible for rendering children. This function should implement the following signature: `({ height, width }) => PropTypes.element` |
+| children | Function | ✓ | Function respondible for rendering children. This function should implement the following signature: `({ height, width }) => PropTypes.element` |
| disableHeight | Boolean | | If true the child's `height` property will not be managed |
| disableWidth | Boolean | | If true the child's `width` property will not be managed |
| onResize | Function | Callback to be invoked on-resize; it is passed the following named parameters: `({ height, width })` |
diff --git a/docs/ColumnSizer.md b/docs/ColumnSizer.md
index 0b30bc854..f604bda90 100644
--- a/docs/ColumnSizer.md
+++ b/docs/ColumnSizer.md
@@ -6,7 +6,7 @@ High-order component that auto-calculates column-widths for `Grid` cells.
### Prop Types
| Property | Type | Required? | Description |
|:---|:---|:---:|:---|
-| children | PropTypes.Element | ✓ | Function respondible for rendering a virtualized Grid. This function should implement the following signature: `({ adjustedWidth, getColumnWidth, registerChild }) => PropTypes.element` |
+| children | Function | ✓ | Function respondible for rendering a virtualized Grid. This function should implement the following signature: `({ adjustedWidth, getColumnWidth, registerChild }) => PropTypes.element` |
| columnMaxWidth | Number | | Optional maximum allowed column width |
| columnMinWidth | Number | | Optional minimum allowed column width |
| width | Number | ✓ | Width of Grid or `FlexTable` child |
diff --git a/docs/README.md b/docs/README.md
index 68c835080..41ca5d8b9 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -10,6 +10,7 @@ Documentation
* [VirtualScroll](VirtualScroll.md)
### High-Order Components
+* [ArrowKeyStepper](ArrowKeyStepper.md)
* [AutoSizer](AutoSizer.md)
* [ColumnSizer](ColumnSizer.md)
* [InfiniteLoader](InfiniteLoader.md)
diff --git a/docs/ScrollSync.md b/docs/ScrollSync.md
index f5ec4fa2b..c213f6be5 100644
--- a/docs/ScrollSync.md
+++ b/docs/ScrollSync.md
@@ -6,7 +6,7 @@ High order component that simplifies the process of synchronizing scrolling betw
### Prop Types
| Property | Type | Required? | Description |
|:---|:---|:---:|:---|
-| children | PropTypes.Element | ✓ | Function respondible for rendering 2 or more virtualized components. This function should implement the following signature: `({ onScroll, scrollLeft, scrollTop }) => PropTypes.element` |
+| children | Function | ✓ | Function respondible for rendering 2 or more virtualized components. This function should implement the following signature: `({ onScroll, scrollLeft, scrollTop }) => PropTypes.element` |
### Children function
diff --git a/source/ArrowKeyStepper/ArrowKeyStepper.example.css b/source/ArrowKeyStepper/ArrowKeyStepper.example.css
new file mode 100644
index 000000000..ee3d156bf
--- /dev/null
+++ b/source/ArrowKeyStepper/ArrowKeyStepper.example.css
@@ -0,0 +1,17 @@
+.Grid {
+ border: 1px solid #e0e0e0;
+}
+
+.Cell {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-right: 1px solid #e0e0e0;
+ border-bottom: 1px solid #e0e0e0;
+}
+
+.FocusedCell {
+ background-color: #e0e0e0;
+ font-weight: bold;
+}
diff --git a/source/ArrowKeyStepper/ArrowKeyStepper.example.js b/source/ArrowKeyStepper/ArrowKeyStepper.example.js
new file mode 100644
index 000000000..f27050af6
--- /dev/null
+++ b/source/ArrowKeyStepper/ArrowKeyStepper.example.js
@@ -0,0 +1,100 @@
+/** @flow */
+import Immutable from 'immutable'
+import React, { Component, PropTypes } from 'react'
+import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/ContentBox'
+import ArrowKeyStepper from './ArrowKeyStepper'
+import AutoSizer from '../AutoSizer'
+import Grid from '../Grid'
+import shouldPureComponentUpdate from 'react-pure-render/function'
+import cn from 'classnames'
+import styles from './ArrowKeyStepper.example.css'
+
+export default class ArrowKeyStepperExample extends Component {
+ shouldComponentUpdate = shouldPureComponentUpdate
+
+ static propTypes = {
+ list: PropTypes.instanceOf(Immutable.List).isRequired
+ }
+
+ constructor (props) {
+ super(props)
+
+ this._getColumnWidth = this._getColumnWidth.bind(this)
+ this._getRowHeight = this._getRowHeight.bind(this)
+ this._renderCell = this._renderCell.bind(this)
+ }
+
+ render () {
+ const { list, ...props } = this.props
+
+ return (
+
+
+
+
+ This high-order component decorates a VirtualScroll
, FlexTable
, or Grid
and responds to arrow-key events by scrolling one row or column at a time.
+ Focus in the `Grid` below and use the left, right, up, or down arrow keys to move around within the grid.
+
+
+
+ Note that unlike the other HOCs in react-virtualized, the ArrowKeyStepper
adds a <div>
element around its children in order to attach a key-down event handler.
+
+
+
+ {({ onSectionRendered, scrollToColumn, scrollToRow }) => (
+
+
+ {`Most-recently-stepped column: ${scrollToColumn}, row: ${scrollToRow}`}
+
+
+
+ {({ width }) => (
+ this._renderCell({ columnIndex, rowIndex, scrollToColumn, scrollToRow }) }
+ rowHeight={this._getRowHeight}
+ rowsCount={100}
+ scrollToColumn={scrollToColumn}
+ scrollToRow={scrollToRow}
+ width={width}
+ />
+ )}
+
+
+ )}
+
+
+ )
+ }
+
+ _getColumnWidth (index) {
+ return (1 + (index % 3)) * 60
+ }
+
+ _getRowHeight (index) {
+ return (1 + (index % 3)) * 30
+ }
+
+ _renderCell ({ columnIndex, rowIndex, scrollToColumn, scrollToRow }) {
+ const className = cn(styles.Cell, {
+ [styles.FocusedCell]: columnIndex === scrollToColumn && rowIndex === scrollToRow
+ })
+
+ return (
+
+ {`r:${rowIndex}, c:${columnIndex}`}
+
+ )
+ }
+}
diff --git a/source/ArrowKeyStepper/ArrowKeyStepper.js b/source/ArrowKeyStepper/ArrowKeyStepper.js
new file mode 100644
index 000000000..b90d1f379
--- /dev/null
+++ b/source/ArrowKeyStepper/ArrowKeyStepper.js
@@ -0,0 +1,92 @@
+/** @flow */
+import React, { Component, PropTypes } from 'react'
+import shouldPureComponentUpdate from 'react-pure-render/function'
+
+/**
+ * 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 {
+ shouldComponentUpdate = shouldPureComponentUpdate
+
+ static propTypes = {
+ children: PropTypes.func.isRequired,
+ className: PropTypes.string,
+ columnsCount: PropTypes.number.isRequired,
+ rowsCount: PropTypes.number.isRequired
+ }
+
+ constructor (props, context) {
+ super(props, context)
+
+ this.state = {
+ scrollToColumn: 0,
+ scrollToRow: 0
+ }
+
+ this._columnStartIndex = 0
+ this._columnStopIndex = 0
+ this._rowStartIndex = 0
+ this._rowStopIndex = 0
+
+ this._onKeyDown = this._onKeyDown.bind(this)
+ this._onSectionRendered = this._onSectionRendered.bind(this)
+ }
+
+ render () {
+ const { className, children } = this.props
+ const { scrollToColumn, scrollToRow } = this.state
+
+ return (
+
+ {children({
+ onSectionRendered: this._onSectionRendered,
+ scrollToColumn,
+ scrollToRow
+ })}
+
+ )
+ }
+
+ _onKeyDown (event) {
+ const { columnsCount, rowsCount } = this.props
+
+ // The above cases all prevent default event event behavior.
+ // This is to keep the grid from scrolling after the snap-to update.
+ switch (event.key) {
+ case 'ArrowDown':
+ event.preventDefault()
+ this.setState({
+ scrollToRow: Math.min(this._rowStopIndex + 1, rowsCount - 1)
+ })
+ break
+ case 'ArrowLeft':
+ event.preventDefault()
+ this.setState({
+ scrollToColumn: Math.max(this._columnStartIndex - 1, 0)
+ })
+ break
+ case 'ArrowRight':
+ event.preventDefault()
+ this.setState({
+ scrollToColumn: Math.min(this._columnStopIndex + 1, columnsCount - 1)
+ })
+ break
+ case 'ArrowUp':
+ event.preventDefault()
+ this.setState({
+ scrollToRow: Math.max(this._rowStartIndex - 1, 0)
+ })
+ break
+ }
+ }
+
+ _onSectionRendered ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }) {
+ this._columnStartIndex = columnStartIndex
+ this._columnStopIndex = columnStopIndex
+ this._rowStartIndex = rowStartIndex
+ this._rowStopIndex = rowStopIndex
+ }
+}
diff --git a/source/ArrowKeyStepper/ArrowKeyStepper.test.js b/source/ArrowKeyStepper/ArrowKeyStepper.test.js
new file mode 100644
index 000000000..6225f063c
--- /dev/null
+++ b/source/ArrowKeyStepper/ArrowKeyStepper.test.js
@@ -0,0 +1,133 @@
+import React from 'react'
+import { findDOMNode } from 'react-dom'
+import { render } from '../TestUtils'
+import ArrowKeyStepper from './ArrowKeyStepper'
+import { Simulate } from 'react-addons-test-utils'
+
+function renderTextContent (scrollToColumn, scrollToRow) {
+ return `scrollToColumn:${scrollToColumn}, scrollToRow:${scrollToRow}`
+}
+
+function ChildComponent ({ scrollToColumn, scrollToRow }) {
+ return (
+
{renderTextContent(scrollToColumn, scrollToRow)}
+ )
+}
+
+describe('ArrowKeyStepper', () => {
+ function renderHelper ({
+ className,
+ columnsCount = 10,
+ rowsCount = 10
+ } = {}) {
+ let onSectionRenderedCallback
+
+ const node = findDOMNode(render(
+
+ {({ onSectionRendered, scrollToColumn, scrollToRow }) => {
+ onSectionRenderedCallback = onSectionRendered
+
+ return (
+
+ )
+ }}
+
+ ))
+
+ return {
+ node,
+ onSectionRendered: onSectionRenderedCallback
+ }
+ }
+
+ function assertCurrentScrollTo (node, scrollToColumn, scrollToRow) {
+ expect(node.textContent).toEqual(renderTextContent(scrollToColumn, scrollToRow))
+ }
+
+ it('should use a custom :className if one is specified', () => {
+ const { node } = renderHelper({ className: 'foo' })
+ expect(node.className).toEqual('foo')
+ })
+
+ it('should update :scrollToColumn and :scrollToRow in response to arrow keys', () => {
+ const { node } = renderHelper()
+ assertCurrentScrollTo(node, 0, 0)
+ Simulate.keyDown(node, {key: 'ArrowDown'})
+ assertCurrentScrollTo(node, 0, 1)
+ Simulate.keyDown(node, {key: 'ArrowRight'})
+ assertCurrentScrollTo(node, 1, 1)
+ Simulate.keyDown(node, {key: 'ArrowUp'})
+ assertCurrentScrollTo(node, 1, 0)
+ Simulate.keyDown(node, {key: 'ArrowLeft'})
+ assertCurrentScrollTo(node, 0, 0)
+ })
+
+ it('should not scroll past the row and column boundaries provided', () => {
+ const { node } = renderHelper({
+ columnsCount: 2,
+ rowsCount: 2
+ })
+ Simulate.keyDown(node, {key: 'ArrowDown'})
+ Simulate.keyDown(node, {key: 'ArrowDown'})
+ Simulate.keyDown(node, {key: 'ArrowDown'})
+ assertCurrentScrollTo(node, 0, 1)
+ Simulate.keyDown(node, {key: 'ArrowUp'})
+ Simulate.keyDown(node, {key: 'ArrowUp'})
+ Simulate.keyDown(node, {key: 'ArrowUp'})
+ assertCurrentScrollTo(node, 0, 0)
+ Simulate.keyDown(node, {key: 'ArrowRight'})
+ Simulate.keyDown(node, {key: 'ArrowRight'})
+ Simulate.keyDown(node, {key: 'ArrowRight'})
+ assertCurrentScrollTo(node, 1, 0)
+ Simulate.keyDown(node, {key: 'ArrowLeft'})
+ Simulate.keyDown(node, {key: 'ArrowLeft'})
+ Simulate.keyDown(node, {key: 'ArrowLeft'})
+ assertCurrentScrollTo(node, 0, 0)
+ })
+
+ it('should update :scrollToColumn and :scrollToRow relative to the most recent :onSectionRendered event', () => {
+ const { node, onSectionRendered } = renderHelper()
+ onSectionRendered({ // Simulate a scroll
+ columnStartIndex: 0,
+ columnStopIndex: 4,
+ rowStartIndex: 4,
+ rowStopIndex: 6
+ })
+ Simulate.keyDown(node, {key: 'ArrowDown'})
+ assertCurrentScrollTo(node, 0, 7)
+
+ onSectionRendered({ // Simulate a scroll
+ columnStartIndex: 5,
+ columnStopIndex: 10,
+ rowStartIndex: 2,
+ rowStopIndex: 4
+ })
+ Simulate.keyDown(node, {key: 'ArrowUp'})
+ assertCurrentScrollTo(node, 0, 1)
+
+ onSectionRendered({ // Simulate a scroll
+ columnStartIndex: 4,
+ columnStopIndex: 8,
+ rowStartIndex: 5,
+ rowStopIndex: 10
+ })
+ Simulate.keyDown(node, {key: 'ArrowRight'})
+ assertCurrentScrollTo(node, 9, 1)
+
+ onSectionRendered({ // Simulate a scroll
+ columnStartIndex: 2,
+ columnStopIndex: 4,
+ rowStartIndex: 2,
+ rowStopIndex: 4
+ })
+ Simulate.keyDown(node, {key: 'ArrowLeft'})
+ assertCurrentScrollTo(node, 1, 1)
+ })
+})
diff --git a/source/ArrowKeyStepper/index.js b/source/ArrowKeyStepper/index.js
new file mode 100644
index 000000000..bb6a72987
--- /dev/null
+++ b/source/ArrowKeyStepper/index.js
@@ -0,0 +1,2 @@
+export default from './ArrowKeyStepper'
+export ArrowKeyStepper from './ArrowKeyStepper'
diff --git a/source/FlexTable/FlexTable.js b/source/FlexTable/FlexTable.js
index 0ac4afcbd..8d5047cea 100644
--- a/source/FlexTable/FlexTable.js
+++ b/source/FlexTable/FlexTable.js
@@ -146,33 +146,7 @@ export default class FlexTable extends Component {
this.refs.Grid.recomputeGridSize()
}
- /**
- * See Grid#scrollToIndex
- */
- scrollToRow (scrollToIndex) {
- this.refs.Grid.scrollToCell({
- scrollToColumn: 0,
- scrollToRow: scrollToIndex
- })
- }
-
- /**
- * See Grid#setScrollPosition
- */
- setScrollTop (scrollTop) {
- this.refs.Grid.setScrollPosition({
- scrollLeft: 0,
- scrollTop
- })
- }
-
componentDidMount () {
- const { scrollTop } = this.props
-
- if (scrollTop >= 0) {
- this.setScrollTop(scrollTop)
- }
-
this._setScrollbarWidth()
}
@@ -180,12 +154,6 @@ export default class FlexTable extends Component {
this._setScrollbarWidth()
}
- componentWillUpdate (nextProps, nextState) {
- if (nextProps.scrollTop !== this.props.scrollTop) {
- this.setScrollTop(nextProps.scrollTop)
- }
- }
-
render () {
const {
className,
@@ -200,6 +168,7 @@ export default class FlexTable extends Component {
rowHeight,
rowsCount,
scrollToIndex,
+ scrollTop,
width
} = this.props
const { scrollbarWidth } = this.state
@@ -250,6 +219,7 @@ export default class FlexTable extends Component {
rowHeight={rowHeight}
rowsCount={rowsCount}
scrollToRow={scrollToIndex}
+ scrollTop={scrollTop}
width={width}
/>
diff --git a/source/FlexTable/FlexTable.test.js b/source/FlexTable/FlexTable.test.js
index 829ffef6d..3eb430528 100644
--- a/source/FlexTable/FlexTable.test.js
+++ b/source/FlexTable/FlexTable.test.js
@@ -632,7 +632,4 @@ describe('FlexTable', () => {
})
})
})
-
- // TODO Add tests for :scrollToRow and :setScrollTop.
- // This probably requires the creation of an inner test-only class with refs.
})
diff --git a/source/Grid/Grid.js b/source/Grid/Grid.js
index 69826f7f3..04a864e0a 100644
--- a/source/Grid/Grid.js
+++ b/source/Grid/Grid.js
@@ -7,7 +7,7 @@ import {
getVisibleCellIndices,
initCellMetadata,
updateScrollIndexHelper
-} from '../utils'
+} from './GridUtils'
import cn from 'classnames'
import raf from 'raf'
import getScrollbarSize from 'dom-helpers/util/scrollbarSize'
@@ -152,7 +152,6 @@ export default class Grid extends Component {
// Bind functions to instance so they don't lose context when passed around
this._computeGridMetadata = this._computeGridMetadata.bind(this)
this._invokeOnGridRenderedHelper = this._invokeOnGridRenderedHelper.bind(this)
- this._onKeyPress = this._onKeyPress.bind(this)
this._onScroll = this._onScroll.bind(this)
this._updateScrollLeftForScrollToColumn = this._updateScrollLeftForScrollToColumn.bind(this)
this._updateScrollTopForScrollToRow = this._updateScrollTopForScrollToRow.bind(this)
@@ -169,50 +168,13 @@ export default class Grid extends Component {
})
}
- /**
- * Updates the Grid to ensure the cell at the specified row and column indices is visible.
- * This method exists so that a user can forcefully scroll to the same cell twice.
- * (The :scrollToColumn and :scrollToRow properties would not change in that case so it would not be picked up by the component.)
- */
- scrollToCell ({ scrollToColumn, scrollToRow }) {
- this._updateScrollLeftForScrollToColumn(scrollToColumn)
- this._updateScrollTopForScrollToRow(scrollToRow)
- }
-
- /**
- * 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 newState = {
- scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.REQUESTED
- }
-
- if (scrollLeft >= 0) {
- newState.scrollLeft = scrollLeft
- }
-
- if (scrollTop >= 0) {
- newState.scrollTop = scrollTop
- }
-
- if (
- scrollLeft >= 0 && scrollLeft !== this.state.scrollLeft ||
- scrollTop >= 0 && scrollTop !== this.state.scrollTop
- ) {
- this.setState(newState)
- }
- }
-
componentDidMount () {
const { scrollLeft, scrollToColumn, scrollTop, scrollToRow } = this.props
this._scrollbarSize = getScrollbarSize()
if (scrollLeft >= 0 || scrollTop >= 0) {
- this.setScrollPosition({ scrollLeft, scrollTop })
+ this._setScrollPosition({ scrollLeft, scrollTop })
}
if (scrollToColumn >= 0 || scrollToRow >= 0) {
@@ -232,6 +194,11 @@ export default class Grid extends Component {
})
}
+ /**
+ * @private
+ * This method updates scrollLeft/scrollTop in state for the following conditions:
+ * 1) New scroll-to-cell props have been set
+ */
componentDidUpdate (prevProps, prevState) {
const { columnsCount, columnWidth, height, rowHeight, rowsCount, scrollToColumn, scrollToRow, width } = this.props
const { scrollLeft, scrollPositionChangeReason, scrollTop } = this.state
@@ -258,7 +225,7 @@ export default class Grid extends Component {
}
}
- // Update scrollLeft if appropriate
+ // Update scroll offsets if the current :scrollToColumn or :scrollToRow values requires it
updateScrollIndexHelper({
cellsCount: columnsCount,
cellMetadata: this._columnMetadata,
@@ -272,8 +239,6 @@ export default class Grid extends Component {
size: width,
updateScrollIndexCallback: this._updateScrollLeftForScrollToColumn
})
-
- // Update scrollTop if appropriate
updateScrollIndexHelper({
cellsCount: rowsCount,
cellMetadata: this._rowMetadata,
@@ -306,29 +271,35 @@ export default class Grid extends Component {
}
}
+ /**
+ * @private
+ * This method updates scrollLeft/scrollTop in state for the following conditions:
+ * 1) Empty content (0 rows or columns)
+ * 2) New scroll props overriding the current state
+ * 3) Cells-count or cells-size has changed, making previous scroll offsets invalid
+ */
componentWillUpdate (nextProps, nextState) {
if (
nextProps.columnsCount === 0 &&
- nextState.scrollLeft !== 0
- ) {
- this.setScrollPosition({ scrollLeft: 0 })
- }
-
- if (
+ nextState.scrollLeft !== 0 ||
nextProps.rowsCount === 0 &&
nextState.scrollTop !== 0
) {
- this.setScrollPosition({ scrollTop: 0 })
- }
-
- if (nextProps.scrollLeft !== this.props.scrollLeft) {
- this.setScrollPosition({ scrollLeft: nextProps.scrollLeft })
- }
-
- if (nextProps.scrollTop !== this.props.scrollTop) {
- this.setScrollPosition({ scrollTop: nextProps.scrollTop })
+ this._setScrollPosition({
+ scrollLeft: 0,
+ scrollTop: 0
+ })
+ } else if (
+ nextProps.scrollLeft !== this.props.scrollLeft ||
+ nextProps.scrollTop !== this.props.scrollTop
+ ) {
+ this._setScrollPosition({
+ scrollLeft: nextProps.scrollLeft,
+ scrollTop: nextProps.scrollTop
+ })
}
+ // Update scroll offsets if the size or number of cells have changed, invalidating the previous value
computeCellMetadataAndUpdateScrollOffsetHelper({
cellsCount: this.props.columnsCount,
cellSize: this.props.columnWidth,
@@ -341,7 +312,6 @@ export default class Grid extends Component {
scrollToIndex: this.props.scrollToColumn,
updateScrollOffsetForScrollToIndex: this._updateScrollLeftForScrollToColumn
})
-
computeCellMetadataAndUpdateScrollOffsetHelper({
cellsCount: this.props.rowsCount,
cellSize: this.props.rowHeight,
@@ -480,7 +450,6 @@ export default class Grid extends Component {
= 0) {
+ newState.scrollLeft = scrollLeft
+ }
+
+ if (scrollTop >= 0) {
+ newState.scrollTop = scrollTop
+ }
+
+ if (
+ scrollLeft >= 0 && scrollLeft !== this.state.scrollLeft ||
+ scrollTop >= 0 && scrollTop !== this.state.scrollTop
+ ) {
+ this.setState(newState)
+ }
+ }
+
_updateScrollLeftForScrollToColumn (scrollToColumnOverride) {
const scrollToColumn = scrollToColumnOverride != null
? scrollToColumnOverride
@@ -645,7 +635,7 @@ export default class Grid extends Component {
})
if (scrollLeft !== calculatedScrollLeft) {
- this.setScrollPosition({
+ this._setScrollPosition({
scrollLeft: calculatedScrollLeft
})
}
@@ -669,63 +659,13 @@ export default class Grid extends Component {
})
if (scrollTop !== calculatedScrollTop) {
- this.setScrollPosition({
+ this._setScrollPosition({
scrollTop: calculatedScrollTop
})
}
}
}
- /* ---------------------------- Event handlers ---------------------------- */
-
- _onKeyPress (event) {
- const { columnsCount, height, rowsCount, width } = this.props
- const { scrollLeft, scrollTop } = this.state
-
- let datum, newScrollLeft, newScrollTop
-
- if (columnsCount === 0 || rowsCount === 0) {
- return
- }
-
- switch (event.key) {
- case 'ArrowDown':
- datum = this._rowMetadata[this._renderedRowStartIndex]
- newScrollTop = Math.min(
- this._getTotalRowsHeight() - height,
- scrollTop + datum.size
- )
-
- this.setScrollPosition({
- scrollTop: newScrollTop
- })
- break
- case 'ArrowLeft':
- this.scrollToCell({
- scrollToColumn: Math.max(0, this._renderedColumnStartIndex - 1),
- scrollToRow: this.props.scrollToRow
- })
- break
- case 'ArrowRight':
- datum = this._columnMetadata[this._renderedColumnStartIndex]
- newScrollLeft = Math.min(
- this._getTotalColumnsWidth() - width,
- scrollLeft + datum.size
- )
-
- this.setScrollPosition({
- scrollLeft: newScrollLeft
- })
- break
- case 'ArrowUp':
- this.scrollToCell({
- scrollToColumn: this.props.scrollToColumn,
- scrollToRow: Math.max(0, this._renderedRowStartIndex - 1)
- })
- break
- }
- }
-
_onScroll (event) {
// In certain edge-cases React dispatches an onScroll event with an invalid target.scrollLeft / target.scrollTop.
// This invalid event can be detected by comparing event.target to this component's scrollable DOM element.
diff --git a/source/Grid/Grid.test.js b/source/Grid/Grid.test.js
index 6b7b84416..ac6bbf18a 100644
--- a/source/Grid/Grid.test.js
+++ b/source/Grid/Grid.test.js
@@ -137,6 +137,37 @@ describe('Grid', () => {
// Target offset for the last item then is 2,000 - 100
expect(grid.state.scrollTop).toEqual(1900)
})
+
+ it('should scroll to a row and column just added', () => {
+ let grid = render(getMarkup())
+ expect(grid.state.scrollLeft).toEqual(0)
+ expect(grid.state.scrollTop).toEqual(0)
+ grid = render(getMarkup({
+ columnsCount: NUM_COLUMNS + 1,
+ rowsCount: NUM_ROWS + 1,
+ scrollToColumn: NUM_COLUMNS,
+ scrollToRow: NUM_ROWS
+ }))
+ expect(grid.state.scrollLeft).toEqual(2350)
+ expect(grid.state.scrollTop).toEqual(1920)
+ })
+
+ it('should scroll back to a newly-added cell without a change in prop', () => {
+ let grid = render(getMarkup({
+ columnsCount: NUM_COLUMNS,
+ rowsCount: NUM_ROWS,
+ scrollToColumn: NUM_COLUMNS,
+ scrollToRow: NUM_ROWS
+ }))
+ grid = render(getMarkup({
+ columnsCount: NUM_COLUMNS + 1,
+ rowsCount: NUM_ROWS + 1,
+ scrollToColumn: NUM_COLUMNS,
+ scrollToRow: NUM_ROWS
+ }))
+ expect(grid.state.scrollLeft).toEqual(2350)
+ expect(grid.state.scrollTop).toEqual(1920)
+ })
})
describe('property updates', () => {
@@ -508,7 +539,4 @@ describe('Grid', () => {
expect(helper.rowStopIndex()).toEqual(4)
})
})
-
- // TODO Add tests for :scrollToCell and :setScrollPosition.
- // This probably requires the creation of an inner test-only class with refs.
})
diff --git a/source/utils.js b/source/Grid/GridUtils.js
similarity index 100%
rename from source/utils.js
rename to source/Grid/GridUtils.js
diff --git a/source/utils.test.js b/source/Grid/GridUtils.test.js
similarity index 99%
rename from source/utils.test.js
rename to source/Grid/GridUtils.test.js
index e7c38c343..7359089e5 100644
--- a/source/utils.test.js
+++ b/source/Grid/GridUtils.test.js
@@ -6,7 +6,7 @@ import {
getVisibleCellIndices,
initCellMetadata,
updateScrollIndexHelper
-} from './utils'
+} from './GridUtils'
// Default cell sizes and offsets for use in below tests
function getCellMetadata () {
diff --git a/source/VirtualScroll/VirtualScroll.js b/source/VirtualScroll/VirtualScroll.js
index 7c802cf59..0bbb3c786 100644
--- a/source/VirtualScroll/VirtualScroll.js
+++ b/source/VirtualScroll/VirtualScroll.js
@@ -73,20 +73,6 @@ export default class VirtualScroll extends Component {
overscanRowsCount: 10
}
- componentDidMount () {
- const { scrollTop } = this.props
-
- if (scrollTop >= 0) {
- this.setScrollTop(scrollTop)
- }
- }
-
- componentWillUpdate (nextProps, nextState) {
- if (nextProps.scrollTop !== this.props.scrollTop) {
- this.setScrollTop(nextProps.scrollTop)
- }
- }
-
/**
* See Grid#recomputeGridSize
*/
@@ -94,26 +80,6 @@ export default class VirtualScroll extends Component {
this.refs.Grid.recomputeGridSize()
}
- /**
- * See Grid#scrollToCell
- */
- scrollToRow (scrollToIndex) {
- this.refs.Grid.scrollToCell({
- scrollToColumn: 0,
- scrollToRow: scrollToIndex
- })
- }
-
- /**
- * See Grid#setScrollPosition
- */
- setScrollTop (scrollTop) {
- this.refs.Grid.setScrollPosition({
- scrollLeft: 0,
- scrollTop
- })
- }
-
render () {
const {
className,
@@ -126,6 +92,7 @@ export default class VirtualScroll extends Component {
overscanRowsCount,
rowsCount,
scrollToIndex,
+ scrollTop,
width
} = this.props
@@ -151,6 +118,7 @@ export default class VirtualScroll extends Component {
rowHeight={rowHeight}
rowsCount={rowsCount}
scrollToRow={scrollToIndex}
+ scrollTop={scrollTop}
width={width}
/>
)
diff --git a/source/VirtualScroll/VirtualScroll.test.js b/source/VirtualScroll/VirtualScroll.test.js
index 8edd21330..0bd328f37 100644
--- a/source/VirtualScroll/VirtualScroll.test.js
+++ b/source/VirtualScroll/VirtualScroll.test.js
@@ -322,7 +322,4 @@ describe('VirtualScroll', () => {
})
})
})
-
- // TODO Add tests for :scrollToRow and :setScrollTop.
- // This probably requires the creation of an inner test-only class with refs.
})
diff --git a/source/demo/Application.js b/source/demo/Application.js
index 20bc720d6..666052214 100644
--- a/source/demo/Application.js
+++ b/source/demo/Application.js
@@ -1,3 +1,4 @@
+import ArrowKeyStepperExample from '../ArrowKeyStepper/ArrowKeyStepper.example'
import AutoSizerExample from '../AutoSizer/AutoSizer.example'
import ColumnSizerExample from '../ColumnSizer/ColumnSizer.example'
import ComponentLink from './ComponentLink'
@@ -16,7 +17,7 @@ import shouldPureComponentUpdate from 'react-pure-render/function'
import '../../styles.css'
const COMPONENTS = ['Grid', 'FlexTable', 'VirtualScroll']
-const HIGH_ORDER_COMPONENTS = ['AutoSizer', 'ColumnSizer', 'InfiniteLoader', 'ScrollSync']
+const HIGH_ORDER_COMPONENTS = ['ArrowKeyStepper', 'AutoSizer', 'ColumnSizer', 'InfiniteLoader', 'ScrollSync']
// HACK Generate arbitrary data for use in example components :)
const list = Immutable.List(generateRandomList())
@@ -102,6 +103,12 @@ class Application extends Component {
+ {activeComponent === 'ArrowKeyStepper' &&
+
+ }
{activeComponent === 'AutoSizer' &&