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' &&