diff --git a/docs/ScrollSync.md b/docs/ScrollSync.md index ebab0f124..16c04cce5 100644 --- a/docs/ScrollSync.md +++ b/docs/ScrollSync.md @@ -14,9 +14,13 @@ The child function is passed the following named parameters: | Parameter | Type | Description | |:---|:---|:---:| +| clientHeight | Number | Height of the visible portion of the `Grid` (or other scroll-synced component) | +| clientWidth | Number | Width of the visible portion of the `Grid` (or other scroll-synced component) | | onScroll | Function | This function should be passed through to at least one of the virtualized child components. Updates to it will trigger updates to the scroll ofset parameters which will in turn update the other virtualized children. | -| scrollLeft | Number | The current left scroll offset. | -| scrollTop | Number | The current top scroll offset. | +| scrollHeight | Number | Total height of all rows in the `Grid` (or other scroll-synced component) | +| scrollLeft | Number | The current scroll-left offset. | +| scrollTop | Number | The current scroll-top offset. | +| scrollWidth | Number | Total width of all rows in the `Grid` (or other scroll-synced component) | ### Examples @@ -28,7 +32,7 @@ import { Grid, ScrollSync, VirtualScroll } from 'react-virtualized' function render (props) { return ( - {({ onScroll, scrollLeft, scrollTop }) => ( + {({ clientHeight, clientWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth }) => (
{ }) describe('onScroll', () => { + it('should trigger callback when component initially mounts', () => { + const onScrollCalls = [] + renderTable({ + onScroll: params => onScrollCalls.push(params) + }) + expect(onScrollCalls).toEqual([{ + clientHeight: 80, + scrollHeight: 1000, + scrollTop: 0 + }]) + }) + it('should trigger callback when component scrolls', () => { const onScrollCalls = [] const table = renderTable({ @@ -662,11 +674,12 @@ describe('FlexTable', () => { } table.refs.Grid.refs.scrollingContainer = target // HACK to work around _onScroll target check Simulate.scroll(findDOMNode(table.refs.Grid), { target }) - expect(onScrollCalls).toEqual([{ + expect(onScrollCalls.length).toEqual(2) + expect(onScrollCalls[1]).toEqual({ clientHeight: 80, scrollHeight: 1000, scrollTop: 100 - }]) + }) }) }) diff --git a/source/Grid/Grid.js b/source/Grid/Grid.js index 09f98cdbe..5ac5008cd 100644 --- a/source/Grid/Grid.js +++ b/source/Grid/Grid.js @@ -223,6 +223,14 @@ export default class Grid extends Component { // Update onRowsRendered callback this._invokeOnGridRenderedHelper() + + // Initialize onScroll callback + this._invokeOnScrollMemoizer({ + scrollLeft: scrollLeft || 0, + scrollTop: scrollTop || 0, + totalColumnsWidth: this._getTotalColumnsWidth(), + totalRowsHeight: this._getTotalRowsHeight() + }) } componentDidUpdate (prevProps, prevState) { @@ -606,6 +614,27 @@ export default class Grid extends Component { }) } + _invokeOnScrollMemoizer ({ scrollLeft, scrollTop, totalColumnsWidth, totalRowsHeight }) { + const { height, onScroll, width } = this.props + + this._onScrollMemoizer({ + callback: ({ scrollLeft, scrollTop }) => { + onScroll({ + clientHeight: height, + clientWidth: width, + scrollHeight: totalRowsHeight, + scrollLeft, + scrollTop, + scrollWidth: totalColumnsWidth + }) + }, + indices: { + scrollLeft, + scrollTop + } + }) + } + /** * Updates the state during the next animation frame. * Use this method to avoid multiple renders in a small span of time. @@ -773,7 +802,7 @@ export default class Grid extends Component { // Gradually converging on a scrollTop that is within the bounds of the new, smaller height. // This causes a series of rapid renders that is slow for long lists. // We can avoid that by doing some simple bounds checking to ensure that scrollTop never exceeds the total height. - const { height, onScroll, width } = this.props + const { height, width } = this.props const totalRowsHeight = this._getTotalRowsHeight() const totalColumnsWidth = this._getTotalColumnsWidth() const scrollLeft = Math.min(totalColumnsWidth - width, event.target.scrollLeft) @@ -809,21 +838,6 @@ export default class Grid extends Component { }) } - this._onScrollMemoizer({ - callback: ({ scrollLeft, scrollTop }) => { - onScroll({ - clientHeight: height, - clientWidth: width, - scrollHeight: totalRowsHeight, - scrollLeft, - scrollTop, - scrollWidth: totalColumnsWidth - }) - }, - indices: { - scrollLeft, - scrollTop - } - }) + this._invokeOnScrollMemoizer({ scrollLeft, scrollTop, totalColumnsWidth, totalRowsHeight }) } } diff --git a/source/Grid/Grid.test.js b/source/Grid/Grid.test.js index 3d7a93103..26fef421f 100644 --- a/source/Grid/Grid.test.js +++ b/source/Grid/Grid.test.js @@ -397,6 +397,23 @@ describe('Grid', () => { Simulate.scroll(findDOMNode(grid), { target }) } + it('should trigger callback when component is mounted', () => { + const onScrollCalls = [] + renderGrid({ + onScroll: params => onScrollCalls.push(params), + scrollLeft: 50, + scrollTop: 100 + }) + expect(onScrollCalls).toEqual([{ + clientHeight: 100, + clientWidth: 200, + scrollHeight: 2000, + scrollLeft: 50, + scrollTop: 100, + scrollWidth: 2500 + }]) + }) + it('should trigger callback when component scrolls horizontally', () => { const onScrollCalls = [] const grid = renderGrid({ @@ -407,17 +424,18 @@ describe('Grid', () => { scrollLeft: 100, scrollTop: 0 }) - expect(onScrollCalls).toEqual([{ + expect(onScrollCalls.length).toEqual(2) + expect(onScrollCalls[1]).toEqual({ clientHeight: 100, clientWidth: 200, scrollHeight: 2000, scrollLeft: 100, scrollTop: 0, scrollWidth: 2500 - }]) + }) }) - it('should trigger callback when component scrolls horizontally', () => { + it('should trigger callback when component scrolls vertically', () => { const onScrollCalls = [] const grid = renderGrid({ onScroll: params => onScrollCalls.push(params) @@ -427,14 +445,15 @@ describe('Grid', () => { scrollLeft: 0, scrollTop: 100 }) - expect(onScrollCalls).toEqual([{ + expect(onScrollCalls.length).toEqual(2) + expect(onScrollCalls[1]).toEqual({ clientHeight: 100, clientWidth: 200, scrollHeight: 2000, scrollLeft: 0, scrollTop: 100, scrollWidth: 2500 - }]) + }) }) }) diff --git a/source/ScrollSync/ScrollSync.example.css b/source/ScrollSync/ScrollSync.example.css index 98a79c084..b9e94fc35 100644 --- a/source/ScrollSync/ScrollSync.example.css +++ b/source/ScrollSync/ScrollSync.example.css @@ -17,7 +17,6 @@ .HeaderGrid { width: 100%; overflow: hidden !important; - background-color: #fafafa; } .BodyGrid { width: 100%; @@ -43,6 +42,5 @@ } .headerCell, .leftCell { - background-color: #fafafa; font-weight: bold; } diff --git a/source/ScrollSync/ScrollSync.example.js b/source/ScrollSync/ScrollSync.example.js index 8cb5afc21..af46c4523 100644 --- a/source/ScrollSync/ScrollSync.example.js +++ b/source/ScrollSync/ScrollSync.example.js @@ -22,12 +22,12 @@ export default class GridExample extends Component { this.state = { columnWidth: 75, - columnsCount: 1000, + columnsCount: 50, height: 300, overscanColumnsCount: 0, overscanRowsCount: 5, rowHeight: 40, - rowsCount: 1000 + rowsCount: 100 } this._renderBodyCell = this._renderBodyCell.bind(this) @@ -62,65 +62,86 @@ export default class GridExample extends Component { This example shows two Grids and one VirtualScroll configured to mimic a spreadsheet with a fixed header and first column. + It also shows how a scroll callback can be used to control UI properties such as background color. - {({ onScroll, scrollLeft, scrollTop }) => ( -
-
- -
-
- - {({ width }) => ( -
-
- -
+ {({ clientHeight, clientWidth, onScroll, scrollHeight, scrollLeft, scrollTop, scrollWidth }) => { + const x = scrollLeft / (scrollWidth - clientWidth) + const y = scrollTop / (scrollHeight - clientHeight) + + const leftBackgroundColor = fadeBetweenRGB([65, 65, 65], [0, 0, 0], y) + const leftColor = `#ffffff` + + const topBackgroundColor = fadeBetweenRGB([77, 182, 172], [23, 146, 135], x) + const topColor = `#ffffff` + + return ( +
+
+ +
+
+ + {({ width }) => (
- +
+ +
+
+ +
-
- )} - + )} + +
-
- )} + ) + }} ) @@ -157,3 +178,11 @@ export default class GridExample extends Component { ) } } + +function fadeBetweenRGB (rgbFrom, rgbTo, amount) { + const r = Math.round(rgbFrom[0] + (rgbTo[0] - rgbFrom[0]) * amount) + const g = Math.round(rgbFrom[1] + (rgbTo[1] - rgbFrom[1]) * amount) + const b = Math.round(rgbFrom[2] + (rgbTo[2] - rgbFrom[2]) * amount) + + return `rgb(${r}, ${g}, ${b})` +} diff --git a/source/ScrollSync/ScrollSync.js b/source/ScrollSync/ScrollSync.js index 021c7a827..46aa1d137 100644 --- a/source/ScrollSync/ScrollSync.js +++ b/source/ScrollSync/ScrollSync.js @@ -20,8 +20,12 @@ export default class ScrollSync extends Component { super(props, context) this.state = { + clientHeight: 0, + clientWidth: 0, + scrollHeight: 0, scrollLeft: 0, - scrollTop: 0 + scrollTop: 0, + scrollWidth: 0 } this._onScroll = this._onScroll.bind(this) @@ -29,16 +33,20 @@ export default class ScrollSync extends Component { render () { const { children } = this.props - const { scrollLeft, scrollTop } = this.state + const { clientHeight, clientWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth } = this.state return children({ + clientHeight, + clientWidth, onScroll: this._onScroll, + scrollHeight, scrollLeft, - scrollTop + scrollTop, + scrollWidth }) } - _onScroll ({ scrollLeft, scrollTop }) { - this.setState({ scrollLeft, scrollTop }) + _onScroll ({ clientHeight, clientWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth }) { + this.setState({ clientHeight, clientWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth }) } } diff --git a/source/ScrollSync/ScrollSync.test.js b/source/ScrollSync/ScrollSync.test.js index 0ef2142d9..9fa38f21f 100644 --- a/source/ScrollSync/ScrollSync.test.js +++ b/source/ScrollSync/ScrollSync.test.js @@ -2,9 +2,16 @@ import React from 'react' import { findDOMNode, render } from 'react-dom' import ScrollSync from './ScrollSync' -function ChildComponent ({ scrollLeft, scrollTop }) { +function ChildComponent ({ clientHeight, clientWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth }) { return ( -
{`scrollLeft:${scrollLeft}, scrollTop:${scrollTop}`}
+
+ {`clientHeight:${clientHeight}`} + {`clientWidth:${clientWidth}`} + {`scrollHeight:${scrollHeight}`} + {`scrollLeft:${scrollLeft}`} + {`scrollTop:${scrollTop}`} + {`scrollWidth:${scrollWidth}`} +
) } @@ -29,16 +36,24 @@ describe('ScrollSync', () => { it('should pass through an initial value of 0 for :scrollLeft and :scrollTop', () => { const component = renderOrUpdateComponent( - {({ onScroll, scrollLeft, scrollTop }) => ( + {({ clientHeight, clientWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth }) => ( )} ) + expect(findDOMNode(component).textContent).toContain('clientHeight:0') + expect(findDOMNode(component).textContent).toContain('clientWidth:0') + expect(findDOMNode(component).textContent).toContain('scrollHeight:0') expect(findDOMNode(component).textContent).toContain('scrollLeft:0') expect(findDOMNode(component).textContent).toContain('scrollTop:0') + expect(findDOMNode(component).textContent).toContain('scrollWidth:0') }) it('should update :scrollLeft and :scrollTop when :onScroll is called', () => { @@ -51,8 +66,19 @@ describe('ScrollSync', () => { }} ) - onScroll({ scrollLeft: 100, scrollTop: 200 }) - expect(findDOMNode(component).textContent).toContain('scrollLeft:100') - expect(findDOMNode(component).textContent).toContain('scrollTop:200') + onScroll({ + clientHeight: 400, + clientWidth: 200, + scrollHeight: 1000, + scrollLeft: 50, + scrollTop: 100, + scrollWidth: 500 + }) + expect(findDOMNode(component).textContent).toContain('clientHeight:400') + expect(findDOMNode(component).textContent).toContain('clientWidth:200') + expect(findDOMNode(component).textContent).toContain('scrollHeight:1000') + expect(findDOMNode(component).textContent).toContain('scrollLeft:50') + expect(findDOMNode(component).textContent).toContain('scrollTop:100') + expect(findDOMNode(component).textContent).toContain('scrollWidth:500') }) }) diff --git a/source/VirtualScroll/VirtualScroll.test.js b/source/VirtualScroll/VirtualScroll.test.js index 9cc1f90d8..f5e176dce 100644 --- a/source/VirtualScroll/VirtualScroll.test.js +++ b/source/VirtualScroll/VirtualScroll.test.js @@ -322,6 +322,18 @@ describe('VirtualScroll', () => { }) describe('onScroll', () => { + it('should trigger callback when component initially mounts', () => { + const onScrollCalls = [] + renderList({ + onScroll: params => onScrollCalls.push(params) + }) + expect(onScrollCalls).toEqual([{ + clientHeight: 100, + scrollHeight: 1000, + scrollTop: 0 + }]) + }) + it('should trigger callback when component scrolls', () => { const onScrollCalls = [] const list = renderList({ @@ -332,11 +344,12 @@ describe('VirtualScroll', () => { } list.refs.Grid.refs.scrollingContainer = target // HACK to work around _onScroll target check Simulate.scroll(findDOMNode(list), { target }) - expect(onScrollCalls).toEqual([{ + expect(onScrollCalls.length).toEqual(2) + expect(onScrollCalls[1]).toEqual({ clientHeight: 100, scrollHeight: 1000, scrollTop: 100 - }]) + }) }) })