From 9833d858a3b2e9b912c804cf9a87c996c0a99c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Thu, 1 Mar 2018 20:00:10 +0100 Subject: [PATCH] Make components track scroll position if not set in the props --- src/components/LazyLoadComponent.jsx | 132 ++--------- src/components/LazyLoadComponent.spec.js | 215 ++--------------- .../LazyLoadComponentWithTracking.jsx | 19 ++ .../LazyLoadComponentWithoutTracking.jsx | 134 +++++++++++ .../LazyLoadComponentWithoutTrackings.spec.js | 218 ++++++++++++++++++ src/components/LazyLoadImage.jsx | 32 +-- src/components/LazyLoadImage.spec.js | 14 -- 7 files changed, 409 insertions(+), 355 deletions(-) create mode 100644 src/components/LazyLoadComponentWithTracking.jsx create mode 100644 src/components/LazyLoadComponentWithoutTracking.jsx create mode 100644 src/components/LazyLoadComponentWithoutTrackings.spec.js diff --git a/src/components/LazyLoadComponent.jsx b/src/components/LazyLoadComponent.jsx index 6e45bf9..00462a7 100644 --- a/src/components/LazyLoadComponent.jsx +++ b/src/components/LazyLoadComponent.jsx @@ -1,134 +1,30 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import { PropTypes } from 'prop-types'; + +import LazyLoadComponentWithoutTracking + from './LazyLoadComponentWithoutTracking.jsx'; +import LazyLoadComponentWithTracking + from './LazyLoadComponentWithTracking.jsx'; class LazyLoadComponent extends React.Component { constructor(props) { super(props); - const { afterLoad, beforeLoad, visibleByDefault } = this.props; - - this.state = { - visible: visibleByDefault - }; - - if (visibleByDefault) { - beforeLoad(); - afterLoad(); - } - } - - componentDidMount() { - this.updateVisibility(); - } - - componentDidUpdate(prevProps, prevState) { - if (prevState.visible) { - return; - } - - if (this.state.visible) { - this.props.afterLoad(); - } - - this.updateVisibility(); - } - - getPlaceholderBoundingBox(scrollPosition = this.props.scrollPosition) { - const boundingRect = this.placeholder.getBoundingClientRect(); - const style = ReactDOM.findDOMNode(this.placeholder).style; - const margin = { - left: parseInt(style.getPropertyValue('margin-left'), 10) || 0, - top: parseInt(style.getPropertyValue('margin-top'), 10) || 0 - }; - - return { - bottom: scrollPosition.y + boundingRect.bottom + margin.top, - left: scrollPosition.x + boundingRect.left + margin.left, - right: scrollPosition.x + boundingRect.right + margin.left, - top: scrollPosition.y + boundingRect.top + margin.top - }; - } - - isPlaceholderInViewport() { - if (!this.placeholder) { - return false; - } - - const { scrollPosition, threshold } = this.props; - const boundingBox = this.getPlaceholderBoundingBox(scrollPosition); - const viewport = { - bottom: scrollPosition.y + window.innerHeight, - left: scrollPosition.x, - right: scrollPosition.x + window.innerWidth, - top: scrollPosition.y - }; + const { scrollPosition } = props; - return Boolean(viewport.top - threshold <= boundingBox.bottom && - viewport.bottom + threshold >= boundingBox.top && - viewport.left - threshold <= boundingBox.right && - viewport.right + threshold >= boundingBox.left); + this.isScrollTracked = (scrollPosition && + Number.isFinite(scrollPosition.x) && scrollPosition.x >= 0 && + Number.isFinite(scrollPosition.y) && scrollPosition.y >= 0); } - updateVisibility() { - if (this.state.visible || !this.isPlaceholderInViewport()) { - return; - } - - this.props.beforeLoad(); - - this.setState({ - visible: true - }); - } - - getPlaceholder() { - const { className, height, placeholder, style, width } = this.props; - - if (placeholder) { - return React.cloneElement(placeholder, - { ref: el => this.placeholder = el }); + render() { + if (this.isScrollTracked) { + return ; } - return ( - this.placeholder = el} - style={{ height, width, ...style }}> - - ); - } + const { scrollPosition, ...props } = this.props; - render() { - return this.state.visible ? - this.props.children : - this.getPlaceholder(); + return ; } } -LazyLoadComponent.propTypes = { - scrollPosition: PropTypes.shape({ - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired - }).isRequired, - afterLoad: PropTypes.func, - beforeLoad: PropTypes.func, - className: PropTypes.string, - height: PropTypes.number, - placeholder: PropTypes.element, - threshold: PropTypes.number, - visibleByDefault: PropTypes.bool, - width: PropTypes.number -}; - -LazyLoadComponent.defaultProps = { - afterLoad: () => ({}), - beforeLoad: () => ({}), - className: '', - height: 0, - placeholder: null, - threshold: 100, - visibleByDefault: false, - width: 0 -}; - export default LazyLoadComponent; diff --git a/src/components/LazyLoadComponent.spec.js b/src/components/LazyLoadComponent.spec.js index 427ee52..c60e623 100644 --- a/src/components/LazyLoadComponent.spec.js +++ b/src/components/LazyLoadComponent.spec.js @@ -4,215 +4,42 @@ import { configure, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import LazyLoadComponent from './LazyLoadComponent.jsx'; +import LazyLoadComponentWithTracking + from './LazyLoadComponentWithTracking.jsx'; +import LazyLoadComponentWithoutTracking + from './LazyLoadComponentWithoutTracking.jsx'; configure({ adapter: new Adapter() }); const { - scryRenderedDOMComponentsWithTag + scryRenderedComponentsWithType } = ReactTestUtils; describe('LazyLoadComponent', function() { - function renderLazyLoadComponent({ - afterLoad = () => null, - beforeLoad = () => null, - placeholder = null, - scrollPosition = {x: 0, y: 0}, - style = {}, - visibleByDefault = false - } = {}) { - return mount( - -

Lorem ipsum

+ it('renders a LazyLoadComponentWithTracking when scrollPosition is undefined', function() { + const lazyLoadComponent = mount( + +

Lorem Ipsum

); - } - - function simulateScroll(lazyLoadComponent, offsetX = 0, offsetY = 0) { - const myMock = jest.fn(); - - myMock.mockReturnValue({ - bottom: -offsetY, - height: 0, - left: -offsetX, - right: -offsetX, - top: -offsetY, - width: 0 - }); - - lazyLoadComponent.instance().placeholder.getBoundingClientRect = myMock; - - lazyLoadComponent.setProps({ - scrollPosition: {x: offsetX, y: offsetY} - }); - } - - function expectParagraphs(wrapper, numberOfParagraphs) { - const p = scryRenderedDOMComponentsWithTag(wrapper.instance(), 'p'); - - expect(p.length).toEqual(numberOfParagraphs); - } - function expectPlaceholders(wrapper, numberOfPlaceholders, placeholderTag = 'span') { - const placeholder = scryRenderedDOMComponentsWithTag(wrapper.instance(), placeholderTag); + const lazyLoadComponentWithTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), LazyLoadComponentWithTracking); - expect(placeholder.length).toEqual(numberOfPlaceholders); - } - - it('renders the default placeholder when it\'s not in the viewport', function() { - const lazyLoadComponent = renderLazyLoadComponent({ - style: {marginTop: 100000} - }); - - expectParagraphs(lazyLoadComponent, 0); - expectPlaceholders(lazyLoadComponent, 1); + expect(lazyLoadComponentWithTracking.length).toEqual(1); }); - it('renders the prop placeholder when it\'s not in the viewport', function() { - const style = {marginTop: 100000}; - const placeholder = ( - + it('renders a LazyLoadComponentWithoutTracking when scrollPosition is defined', function() { + const lazyLoadComponent = mount( + +

Lorem Ipsum

+
); - const lazyLoadComponent = renderLazyLoadComponent({ - placeholder, - style - }); - - expectParagraphs(lazyLoadComponent, 0); - expectPlaceholders(lazyLoadComponent, 1, 'strong'); - }); - - it('renders the image when it\'s in the viewport', function() { - const lazyLoadComponent = renderLazyLoadComponent(); - - expectParagraphs(lazyLoadComponent, 1); - expectPlaceholders(lazyLoadComponent, 0); - }); - - it('renders the image when it appears in the viewport', function() { - const offset = 100000; - const lazyLoadComponent = renderLazyLoadComponent({ - style: {marginTop: offset} - }); - - simulateScroll(lazyLoadComponent, 0, offset); - - expectParagraphs(lazyLoadComponent, 1); - expectPlaceholders(lazyLoadComponent, 0); - }); - - it('renders the image when it appears in the viewport horizontally', function() { - const offset = 100000; - const lazyLoadComponent = renderLazyLoadComponent({ - style: {marginLeft: offset} - }); - - simulateScroll(lazyLoadComponent, offset, 0); - - expectParagraphs(lazyLoadComponent, 1); - expectPlaceholders(lazyLoadComponent, 0); - }); - - it('renders the image when it\'s not in the viewport but visibleByDefault is true', function() { - const lazyLoadComponent = renderLazyLoadComponent({ - style: {marginTop: 100000}, - visibleByDefault: true - }); - - expectParagraphs(lazyLoadComponent, 1); - expectPlaceholders(lazyLoadComponent, 0); - }); - - it('doesn\'t trigger beforeLoad when the image is not the viewport', function() { - const beforeLoad = jest.fn(); - const lazyLoadComponent = renderLazyLoadComponent({ - beforeLoad, - style: {marginTop: 100000} - }); - - expect(beforeLoad).toHaveBeenCalledTimes(0); - }); - - it('triggers beforeLoad when the image is in the viewport', function() { - const beforeLoad = jest.fn(); - const lazyLoadComponent = renderLazyLoadComponent({ - beforeLoad - }); - - expect(beforeLoad).toHaveBeenCalledTimes(1); - }); - - it('triggers beforeLoad when the image appears in the viewport', function() { - const beforeLoad = jest.fn(); - const offset = 100000; - const lazyLoadComponent = renderLazyLoadComponent({ - beforeLoad, - style: {marginTop: offset} - }); - - simulateScroll(lazyLoadComponent, 0, offset); - - expect(beforeLoad).toHaveBeenCalledTimes(1); - }); - - it('triggers beforeLoad when visibleByDefault is true', function() { - const beforeLoad = jest.fn(); - const offset = 100000; - const lazyLoadComponent = renderLazyLoadComponent({ - beforeLoad, - style: {marginTop: offset}, - visibleByDefault: true - }); - - expect(beforeLoad).toHaveBeenCalledTimes(1); - }); - - it('doesn\'t trigger afterLoad when the image is not the viewport', function() { - const afterLoad = jest.fn(); - const lazyLoadComponent = renderLazyLoadComponent({ - afterLoad, - style: {marginTop: 100000} - }); - - expect(afterLoad).toHaveBeenCalledTimes(0); - }); - - it('triggers afterLoad when the image is in the viewport', function() { - const afterLoad = jest.fn(); - const lazyLoadComponent = renderLazyLoadComponent({ - afterLoad - }); - - expect(afterLoad).toHaveBeenCalledTimes(1); - }); - - it('triggers afterLoad when the image appears in the viewport', function() { - const afterLoad = jest.fn(); - const offset = 100000; - const lazyLoadComponent = renderLazyLoadComponent({ - afterLoad, - style: {marginTop: offset} - }); - - simulateScroll(lazyLoadComponent, 0, offset); - - expect(afterLoad).toHaveBeenCalledTimes(1); - }); - it('triggers afterLoad when visibleByDefault is true', function() { - const afterLoad = jest.fn(); - const offset = 100000; - const lazyLoadComponent = renderLazyLoadComponent({ - afterLoad, - style: {marginTop: offset}, - visibleByDefault: true - }); + const lazyLoadComponentWithoutTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), LazyLoadComponentWithoutTracking); - expect(afterLoad).toHaveBeenCalledTimes(1); + expect(lazyLoadComponentWithoutTracking.length).toEqual(1); }); }); diff --git a/src/components/LazyLoadComponentWithTracking.jsx b/src/components/LazyLoadComponentWithTracking.jsx new file mode 100644 index 0000000..573ba8f --- /dev/null +++ b/src/components/LazyLoadComponentWithTracking.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import LazyLoadComponentWithoutTracking + from './LazyLoadComponentWithoutTracking.jsx'; +import trackWindowScroll from '../hoc/trackWindowScroll.js'; + +class LazyLoadComponentWithTracking extends React.Component { + constructor(props) { + super(props); + } + + render() { + return ( + + ); + } +} + +export default trackWindowScroll(LazyLoadComponentWithTracking); diff --git a/src/components/LazyLoadComponentWithoutTracking.jsx b/src/components/LazyLoadComponentWithoutTracking.jsx new file mode 100644 index 0000000..5ead242 --- /dev/null +++ b/src/components/LazyLoadComponentWithoutTracking.jsx @@ -0,0 +1,134 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { PropTypes } from 'prop-types'; + +class LazyLoadComponentWithoutTracking extends React.Component { + constructor(props) { + super(props); + + const { afterLoad, beforeLoad, visibleByDefault } = this.props; + + this.state = { + visible: visibleByDefault + }; + + if (visibleByDefault) { + beforeLoad(); + afterLoad(); + } + } + + componentDidMount() { + this.updateVisibility(); + } + + componentDidUpdate(prevProps, prevState) { + if (prevState.visible) { + return; + } + + if (this.state.visible) { + this.props.afterLoad(); + } + + this.updateVisibility(); + } + + getPlaceholderBoundingBox(scrollPosition = this.props.scrollPosition) { + const boundingRect = this.placeholder.getBoundingClientRect(); + const style = ReactDOM.findDOMNode(this.placeholder).style; + const margin = { + left: parseInt(style.getPropertyValue('margin-left'), 10) || 0, + top: parseInt(style.getPropertyValue('margin-top'), 10) || 0 + }; + + return { + bottom: scrollPosition.y + boundingRect.bottom + margin.top, + left: scrollPosition.x + boundingRect.left + margin.left, + right: scrollPosition.x + boundingRect.right + margin.left, + top: scrollPosition.y + boundingRect.top + margin.top + }; + } + + isPlaceholderInViewport() { + if (!this.placeholder) { + return false; + } + + const { scrollPosition, threshold } = this.props; + const boundingBox = this.getPlaceholderBoundingBox(scrollPosition); + const viewport = { + bottom: scrollPosition.y + window.innerHeight, + left: scrollPosition.x, + right: scrollPosition.x + window.innerWidth, + top: scrollPosition.y + }; + + return Boolean(viewport.top - threshold <= boundingBox.bottom && + viewport.bottom + threshold >= boundingBox.top && + viewport.left - threshold <= boundingBox.right && + viewport.right + threshold >= boundingBox.left); + } + + updateVisibility() { + if (this.state.visible || !this.isPlaceholderInViewport()) { + return; + } + + this.props.beforeLoad(); + + this.setState({ + visible: true + }); + } + + getPlaceholder() { + const { className, height, placeholder, style, width } = this.props; + + if (placeholder) { + return React.cloneElement(placeholder, + { ref: el => this.placeholder = el }); + } + + return ( + this.placeholder = el} + style={{ height, width, ...style }}> + + ); + } + + render() { + return this.state.visible ? + this.props.children : + this.getPlaceholder(); + } +} + +LazyLoadComponentWithoutTracking.propTypes = { + scrollPosition: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }).isRequired, + afterLoad: PropTypes.func, + beforeLoad: PropTypes.func, + className: PropTypes.string, + height: PropTypes.number, + placeholder: PropTypes.element, + threshold: PropTypes.number, + visibleByDefault: PropTypes.bool, + width: PropTypes.number +}; + +LazyLoadComponentWithoutTracking.defaultProps = { + afterLoad: () => ({}), + beforeLoad: () => ({}), + className: '', + height: 0, + placeholder: null, + threshold: 100, + visibleByDefault: false, + width: 0 +}; + +export default LazyLoadComponentWithoutTracking; diff --git a/src/components/LazyLoadComponentWithoutTrackings.spec.js b/src/components/LazyLoadComponentWithoutTrackings.spec.js new file mode 100644 index 0000000..2042c19 --- /dev/null +++ b/src/components/LazyLoadComponentWithoutTrackings.spec.js @@ -0,0 +1,218 @@ +import React from 'react'; +import ReactTestUtils from 'react-dom/test-utils'; +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +import LazyLoadComponentWithoutTracking from './LazyLoadComponentWithoutTracking.jsx'; + +configure({ adapter: new Adapter() }); + +const { + scryRenderedDOMComponentsWithTag +} = ReactTestUtils; + +describe('LazyLoadComponentWithoutTracking', function() { + function renderLazyLoadComponentWithoutTracking({ + afterLoad = () => null, + beforeLoad = () => null, + placeholder = null, + scrollPosition = {x: 0, y: 0}, + style = {}, + visibleByDefault = false + } = {}) { + return mount( + +

Lorem ipsum

+
+ ); + } + + function simulateScroll(component, offsetX = 0, offsetY = 0) { + const myMock = jest.fn(); + + myMock.mockReturnValue({ + bottom: -offsetY, + height: 0, + left: -offsetX, + right: -offsetX, + top: -offsetY, + width: 0 + }); + + component.instance().placeholder.getBoundingClientRect = myMock; + + component.setProps({ + scrollPosition: {x: offsetX, y: offsetY} + }); + } + + function expectParagraphs(wrapper, numberOfParagraphs) { + const p = scryRenderedDOMComponentsWithTag(wrapper.instance(), 'p'); + + expect(p.length).toEqual(numberOfParagraphs); + } + + function expectPlaceholders(wrapper, numberOfPlaceholders, placeholderTag = 'span') { + const placeholder = scryRenderedDOMComponentsWithTag(wrapper.instance(), placeholderTag); + + expect(placeholder.length).toEqual(numberOfPlaceholders); + } + + it('renders the default placeholder when it\'s not in the viewport', function() { + const component = renderLazyLoadComponentWithoutTracking({ + style: {marginTop: 100000} + }); + + expectParagraphs(component, 0); + expectPlaceholders(component, 1); + }); + + it('renders the prop placeholder when it\'s not in the viewport', function() { + const style = {marginTop: 100000}; + const placeholder = ( + + ); + const component = renderLazyLoadComponentWithoutTracking({ + placeholder, + style + }); + + expectParagraphs(component, 0); + expectPlaceholders(component, 1, 'strong'); + }); + + it('renders the image when it\'s in the viewport', function() { + const component = renderLazyLoadComponentWithoutTracking(); + + expectParagraphs(component, 1); + expectPlaceholders(component, 0); + }); + + it('renders the image when it appears in the viewport', function() { + const offset = 100000; + const component = renderLazyLoadComponentWithoutTracking({ + style: {marginTop: offset} + }); + + simulateScroll(component, 0, offset); + + expectParagraphs(component, 1); + expectPlaceholders(component, 0); + }); + + it('renders the image when it appears in the viewport horizontally', function() { + const offset = 100000; + const component = renderLazyLoadComponentWithoutTracking({ + style: {marginLeft: offset} + }); + + simulateScroll(component, offset, 0); + + expectParagraphs(component, 1); + expectPlaceholders(component, 0); + }); + + it('renders the image when it\'s not in the viewport but visibleByDefault is true', function() { + const component = renderLazyLoadComponentWithoutTracking({ + style: {marginTop: 100000}, + visibleByDefault: true + }); + + expectParagraphs(component, 1); + expectPlaceholders(component, 0); + }); + + it('doesn\'t trigger beforeLoad when the image is not the viewport', function() { + const beforeLoad = jest.fn(); + const component = renderLazyLoadComponentWithoutTracking({ + beforeLoad, + style: {marginTop: 100000} + }); + + expect(beforeLoad).toHaveBeenCalledTimes(0); + }); + + it('triggers beforeLoad when the image is in the viewport', function() { + const beforeLoad = jest.fn(); + const component = renderLazyLoadComponentWithoutTracking({ + beforeLoad + }); + + expect(beforeLoad).toHaveBeenCalledTimes(1); + }); + + it('triggers beforeLoad when the image appears in the viewport', function() { + const beforeLoad = jest.fn(); + const offset = 100000; + const component = renderLazyLoadComponentWithoutTracking({ + beforeLoad, + style: {marginTop: offset} + }); + + simulateScroll(component, 0, offset); + + expect(beforeLoad).toHaveBeenCalledTimes(1); + }); + + it('triggers beforeLoad when visibleByDefault is true', function() { + const beforeLoad = jest.fn(); + const offset = 100000; + const component = renderLazyLoadComponentWithoutTracking({ + beforeLoad, + style: {marginTop: offset}, + visibleByDefault: true + }); + + expect(beforeLoad).toHaveBeenCalledTimes(1); + }); + + it('doesn\'t trigger afterLoad when the image is not the viewport', function() { + const afterLoad = jest.fn(); + const component = renderLazyLoadComponentWithoutTracking({ + afterLoad, + style: {marginTop: 100000} + }); + + expect(afterLoad).toHaveBeenCalledTimes(0); + }); + + it('triggers afterLoad when the image is in the viewport', function() { + const afterLoad = jest.fn(); + const component = renderLazyLoadComponentWithoutTracking({ + afterLoad + }); + + expect(afterLoad).toHaveBeenCalledTimes(1); + }); + + it('triggers afterLoad when the image appears in the viewport', function() { + const afterLoad = jest.fn(); + const offset = 100000; + const component = renderLazyLoadComponentWithoutTracking({ + afterLoad, + style: {marginTop: offset} + }); + + simulateScroll(component, 0, offset); + + expect(afterLoad).toHaveBeenCalledTimes(1); + }); + + it('triggers afterLoad when visibleByDefault is true', function() { + const afterLoad = jest.fn(); + const offset = 100000; + const component = renderLazyLoadComponentWithoutTracking({ + afterLoad, + style: {marginTop: offset}, + visibleByDefault: true + }); + + expect(afterLoad).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/LazyLoadImage.jsx b/src/components/LazyLoadImage.jsx index 16a3f1c..7397908 100644 --- a/src/components/LazyLoadImage.jsx +++ b/src/components/LazyLoadImage.jsx @@ -1,6 +1,4 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import { PropTypes } from 'prop-types'; import LazyLoadComponent from './LazyLoadComponent.jsx'; @@ -18,41 +16,17 @@ class LazyLoadImage extends React.Component { afterLoad={afterLoad} beforeLoad={beforeLoad} className={this.props.className} + height={this.props.height} placeholder={placeholder} scrollPosition={scrollPosition} threshold={threshold} visibleByDefault={visibleByDefault} - style={this.props.style}> + style={this.props.style} + width={this.props.width}>
); } } -LazyLoadImage.propTypes = { - scrollPosition: PropTypes.shape({ - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired - }).isRequired, - afterLoad: PropTypes.func, - beforeLoad: PropTypes.func, - className: PropTypes.string, - height: PropTypes.number, - placeholder: PropTypes.element, - threshold: PropTypes.number, - visibleByDefault: PropTypes.bool, - width: PropTypes.number -}; - -LazyLoadImage.defaultProps = { - afterLoad: () => ({}), - beforeLoad: () => ({}), - className: '', - height: 0, - placeholder: null, - threshold: 100, - visibleByDefault: false, - width: 0 -}; - export default LazyLoadImage; diff --git a/src/components/LazyLoadImage.spec.js b/src/components/LazyLoadImage.spec.js index 427cc64..bcd8e46 100644 --- a/src/components/LazyLoadImage.spec.js +++ b/src/components/LazyLoadImage.spec.js @@ -14,20 +14,6 @@ const { } = ReactTestUtils; describe('LazyLoadImage', function() { - function renderLazyLoadImage({ - } = {}) { - return mount( - - ); - } - it('renders a LazyLoadComponent with the correct props', function() { const props = { afterLoad: () => null,