diff --git a/source/WindowScroller/WindowScroller.example.js b/source/WindowScroller/WindowScroller.example.js index 592149c40..4374c12a9 100644 --- a/source/WindowScroller/WindowScroller.example.js +++ b/source/WindowScroller/WindowScroller.example.js @@ -82,35 +82,38 @@ export default class WindowScrollerExample extends PureComponent<{}, State> { value={scrollToIndex || ''} /> -
- - {({height, isScrolling, onChildScroll, scrollTop}) => ( + + + {({height, isScrolling, childRef, onChildScroll, scrollTop}) => ( +
{({width}) => ( - { - window.listEl = el; - }} - autoHeight - className={styles.List} - height={height} - isScrolling={isScrolling} - onScroll={onChildScroll} - overscanRowCount={2} - rowCount={list.size} - rowHeight={30} - rowRenderer={this._rowRenderer} - scrollToIndex={scrollToIndex} - scrollTop={scrollTop} - width={width} - /> +
+ { + window.listEl = el; + }} + autoHeight + className={styles.List} + height={height} + isScrolling={isScrolling} + onScroll={onChildScroll} + overscanRowCount={2} + rowCount={list.size} + rowHeight={30} + rowRenderer={this._rowRenderer} + scrollToIndex={scrollToIndex} + scrollTop={scrollTop} + width={width} + /> +
)}
- )} - -
+
+ )} + ); } diff --git a/source/WindowScroller/WindowScroller.jest.js b/source/WindowScroller/WindowScroller.jest.js index 12f63261d..adb7a815d 100644 --- a/source/WindowScroller/WindowScroller.jest.js +++ b/source/WindowScroller/WindowScroller.jest.js @@ -6,16 +6,6 @@ import ReactDOMServer from 'react-dom/server'; import {render} from '../TestUtils'; import WindowScroller, {IS_SCROLLING_TIMEOUT} from './WindowScroller'; -class ChildComponent extends React.Component { - render() { - const {scrollTop, isScrolling, height, width} = this.props; - - return ( -
{`scrollTop:${scrollTop}, isScrolling:${isScrolling}, height:${height}, width:${width}`}
- ); - } -} - function mockGetBoundingClientRectForHeader({ documentOffset = 0, height, @@ -33,19 +23,10 @@ function mockGetBoundingClientRectForHeader({ })); } -function getMarkup({headerElements, documentOffset, childRef, ...props} = {}) { +function getMarkup({headerElements, documentOffset, renderFn, ...props} = {}) { const windowScroller = ( - {({width, height, isScrolling, onChildScroll, scrollTop}) => ( - - )} + {params =>
{renderFn && renderFn(params)}
}
); @@ -101,6 +82,26 @@ describe('WindowScroller', () => { expect(component._positionFromLeft).toEqual(left); }); + it('should allow passing child element with childRef of children function param', () => { + const scrollElement = document.createElement('div'); + scrollElement.scrollTop = 100; + scrollElement.scrollLeft = 150; + scrollElement.getBoundingClientRect = () => ({ + top: 200, + left: 250, + }); + const child = document.createElement('div'); + child.getBoundingClientRect = () => ({ + top: 300, + left: 350, + }); + const renderFn = jest.fn(); + const component = render(getMarkup({scrollElement, renderFn})); + renderFn.mock.calls[0][0].childRef(child); + expect(component._positionFromTop).toEqual(300 + 100 - 200); + expect(component._positionFromLeft).toEqual(350 + 150 - 250); + }); + // Test edge-case reported in bvaughn/react-virtualized/pull/346 it('should have correct top and left properties to be defined on :_positionFromTop and :_positionFromLeft if documentElement is scrolled', () => { render.unmount(); @@ -120,12 +121,16 @@ describe('WindowScroller', () => { }); it('inherits the window height and passes it to child component', () => { - const component = render(getMarkup()); - const rendered = findDOMNode(component); + const renderFn = jest.fn(); + const component = render(getMarkup({renderFn})); expect(component.state.height).toEqual(window.innerHeight); expect(component.state.height).toEqual(500); - expect(rendered.textContent).toContain('height:500'); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + height: 500, + }), + ); }); it('should restore pointerEvents on body after IS_SCROLLING_TIMEOUT', async () => { @@ -149,7 +154,7 @@ describe('WindowScroller', () => { }); describe('onScroll', () => { - it('should trigger callback when window scrolls', async done => { + it('should trigger callback when window scrolls', async () => { const onScroll = jest.fn(); render(getMarkup({onScroll})); @@ -175,16 +180,19 @@ describe('WindowScroller', () => { scrollLeft: 2500, scrollTop: 5000, }); - - done(); }); - it('should update :scrollTop when window is scrolled', async done => { - const component = render(getMarkup()); + it('should update :scrollTop when window is scrolled', async () => { + const renderFn = jest.fn(); + const component = render(getMarkup({renderFn})); const rendered = findDOMNode(component); // Initial load of the component should have 0 scrollTop - expect(rendered.textContent).toContain('scrollTop:0'); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + scrollTop: 0, + }), + ); simulateWindowScroll({scrollY: 5000}); @@ -193,52 +201,80 @@ describe('WindowScroller', () => { const componentScrollTop = window.scrollY - component._positionFromTop; expect(component.state.scrollTop).toEqual(componentScrollTop); - expect(rendered.textContent).toContain(`scrollTop:${componentScrollTop}`); - - done(); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + scrollTop: componentScrollTop, + }), + ); }); - it('should specify :isScrolling when scrolling and reset after scrolling', async done => { - const component = render(getMarkup()); - const rendered = findDOMNode(component); + it('should specify :isScrolling when scrolling and reset after scrolling', async () => { + const renderFn = jest.fn(); + render(getMarkup({renderFn})); simulateWindowScroll({scrollY: 5000}); - expect(rendered.textContent).toContain('isScrolling:true'); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + isScrolling: true, + }), + ); await new Promise(resolve => setTimeout(resolve, 250)); - expect(rendered.textContent).toContain('isScrolling:false'); - - done(); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + isScrolling: false, + }), + ); }); it('should support a custom :scrollingResetTimeInterval prop', async () => { - const component = render( + const renderFn = jest.fn(); + render( getMarkup({ scrollingResetTimeInterval: 500, + renderFn, }), ); - const rendered = findDOMNode(component); - - expect(rendered.textContent).toContain('isScrolling:false'); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + isScrolling: false, + }), + ); simulateWindowScroll({scrollY: 5000}); - expect(rendered.textContent).toContain('isScrolling:true'); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + isScrolling: true, + }), + ); await new Promise(resolve => setTimeout(resolve, 100)); - expect(rendered.textContent).toContain('isScrolling:true'); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + isScrolling: true, + }), + ); await new Promise(resolve => setTimeout(resolve, 100)); - expect(rendered.textContent).toContain('isScrolling:true'); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + isScrolling: true, + }), + ); await new Promise(resolve => setTimeout(resolve, 400)); - expect(rendered.textContent).toContain('isScrolling:false'); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + isScrolling: false, + }), + ); }); }); @@ -254,19 +290,27 @@ describe('WindowScroller', () => { }); it('should update height when window resizes', () => { - const component = render(getMarkup()); - const rendered = findDOMNode(component); + const renderFn = jest.fn(); + const component = render(getMarkup({renderFn})); // Initial load of the component should have the same window height = 500 expect(component.state.height).toEqual(window.innerHeight); expect(component.state.height).toEqual(500); - expect(rendered.textContent).toContain('height:500'); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + height: 500, + }), + ); simulateWindowResize({height: 1000}); expect(component.state.height).toEqual(window.innerHeight); expect(component.state.height).toEqual(1000); - expect(rendered.textContent).toContain('height:1000'); + expect(renderFn).lastCalledWith( + expect.objectContaining({ + height: 1000, + }), + ); }); }); @@ -356,46 +400,40 @@ describe('WindowScroller', () => { }); it('should scroll the scrollElement (when it is window) the desired amount', () => { - let windowScroller, childComponent; + const renderFn = jest.fn(); + let windowScroller; render( getMarkup({ ref: ref => { windowScroller = ref; }, - childRef: ref => { - childComponent = ref; - }, + renderFn, }), ); - childComponent.props.onScroll({scrollTop: 200}); + renderFn.mock.calls[0][0].onChildScroll({scrollTop: 200}); expect(window.scrollY).toEqual(200 + windowScroller._positionFromTop); }); it('should not scroll the scrollElement if trying to scroll to where we already are', () => { - let childComponent; + const renderFn = jest.fn(); - render( - getMarkup({ - childRef: ref => { - childComponent = ref; - }, - }), - ); + render(getMarkup({renderFn})); simulateWindowScroll({scrollY: 200}); window.scrollTo = jest.fn(); - childComponent.props.onScroll({scrollTop: 200}); + renderFn.mock.calls[0][0].onChildScroll({scrollTop: 200}); expect(window.scrollTo).not.toHaveBeenCalled(); }); it('should scroll the scrollElement (when it is an element) the desired amount', () => { - let windowScroller, childComponent; + let windowScroller; + const renderFn = jest.fn(); const divEl = document.createElement('div'); render( @@ -403,49 +441,48 @@ describe('WindowScroller', () => { ref: ref => { windowScroller = ref; }, + renderFn, scrollElement: divEl, - childRef: ref => { - childComponent = ref; - }, }), ); - childComponent.props.onScroll({scrollTop: 200}); + renderFn.mock.calls[0][0].onChildScroll({scrollTop: 200}); expect(divEl.scrollTop).toEqual(200 + windowScroller._positionFromTop); }); it('should update own scrollTop', () => { - let windowScroller, childComponent; + const renderFn = jest.fn(); - render( - getMarkup({ - ref: ref => { - windowScroller = ref; - }, - childRef: ref => { - childComponent = ref; - }, - }), - ); + render(getMarkup({renderFn})); - childComponent.props.onScroll({scrollTop: 200}); + renderFn.mock.calls[0][0].onChildScroll({scrollTop: 200}); - expect(windowScroller.state.scrollTop).toEqual(200); + expect(renderFn).lastCalledWith( + expect.objectContaining({scrollTop: 200}), + ); }); }); describe('server-side rendering', () => { it('should render content with default widths and heights initially', () => { - const rendered = ReactDOMServer.renderToString( + const renderFn = jest.fn(); + + render( getMarkup({ + renderFn, scrollElement: null, serverHeight: 100, serverWidth: 200, }), ); - expect(rendered).toContain('height:100'); - expect(rendered).toContain('width:200'); + + expect(renderFn).lastCalledWith( + expect.objectContaining({ + height: 100, + width: 200, + }), + ); }); }); }); diff --git a/source/WindowScroller/WindowScroller.js b/source/WindowScroller/WindowScroller.js index 4c041f665..4f8a742f9 100644 --- a/source/WindowScroller/WindowScroller.js +++ b/source/WindowScroller/WindowScroller.js @@ -21,7 +21,8 @@ type Props = { * ({ height, isScrolling, scrollLeft, scrollTop, width }) => PropTypes.element */ children: (params: { - onChildScroll: (params: {scrollTop: number}) => void, + onChildScroll: ({scrollTop: number}) => void, + childRef: (?Element) => void, height: number, isScrolling: boolean, scrollLeft: number, @@ -36,7 +37,8 @@ type Props = { onScroll: (params: {scrollLeft: number, scrollTop: number}) => void, /** Element to attach scroll event listeners. Defaults to window. */ - scrollElement: ?(typeof window | Element), + scrollElement: ?Element, + /** * Wait this amount of time after the last scroll event before resetting child `pointer-events`. */ @@ -87,6 +89,7 @@ export default class WindowScroller extends React.PureComponent { _positionFromTop = 0; _positionFromLeft = 0; _detectElementResize: DetectElementResize = createDetectElementResize(); + _child: ?Element; state = { ...getDimensions(this.props.scrollElement, this.props), @@ -102,7 +105,7 @@ export default class WindowScroller extends React.PureComponent { const {onResize} = this.props; const {height, width} = this.state; - const thisNode = ReactDOM.findDOMNode(this); + const thisNode = this._child || ReactDOM.findDOMNode(this); if (thisNode instanceof Element && scrollElement) { const offset = getPositionOffset(thisNode, scrollElement); this._positionFromTop = offset.top; @@ -170,6 +173,7 @@ export default class WindowScroller extends React.PureComponent { return children({ onChildScroll: this._onChildScroll, + childRef: this._childRef, height, isScrolling, scrollLeft, @@ -178,6 +182,11 @@ export default class WindowScroller extends React.PureComponent { }); } + _childRef = element => { + this._child = element; + this.updatePosition(); + }; + _onChildScroll = ({scrollTop}) => { if (this.state.scrollTop === scrollTop) { return;