diff --git a/README.md b/README.md index 40a451b..ddde8f9 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ export default MyImage; | placeholder | `ReactClass` | `<span>` | React element to use as a placeholder. | | placeholderSrc | `String` | | Image src to display while the image is not visible or loaded. | | threshold | `Number` | 100 | Threshold in pixels. So the image starts loading before it appears in the viewport. | +| useIntersectionObserver | `Boolean` | true | Whether to use browser's IntersectionObserver when available. | | visibleByDefault | `Boolean` | false | Whether the image must be visible from the beginning. | | wrapperClassName | `String` | | In some occasions (for example, when using a placeholderSrc) a wrapper span tag is rendered. This prop allows setting a class to that element. | | ... | | | Any other image attribute | @@ -141,6 +142,7 @@ export default Article; | delayTime | `Number` | 300 | Time in ms sent to the delayMethod from lodash. | | placeholder | `ReactClass` | `<span>` | React element to use as a placeholder. | | threshold | `Number` | 100 | Threshold in pixels. So the component starts loading before it appears in the viewport. | +| useIntersectionObserver | `Boolean` | true | Whether to use browser's IntersectionObserver when available. | | visibleByDefault | `Boolean` | false | Whether the component must be visible from the beginning. | @@ -198,6 +200,7 @@ Component wrapped with `trackWindowScroll` (in the example, `Gallery`) |:---|:---|:---|:---| | delayMethod | `String` | `throttle` | Method from lodash to use to delay the scroll/resize events. It can be `throttle` or `debounce`. | | delayTime | `Number` | 300 | Time in ms sent to the delayMethod from lodash. | +| useIntersectionObserver | `Boolean` | true | Whether to use browser's IntersectionObserver when available. | Notice you can do the same replacing `LazyLoadImage` with `LazyLoadComponent`. diff --git a/src/components/LazyLoadComponent.jsx b/src/components/LazyLoadComponent.jsx index fb381ce..05300fa 100644 --- a/src/components/LazyLoadComponent.jsx +++ b/src/components/LazyLoadComponent.jsx @@ -22,7 +22,7 @@ class LazyLoadComponent extends React.Component { this.onVisible = this.onVisible.bind(this); - this.isScrollTracked = (scrollPosition && + this.isScrollTracked = Boolean(scrollPosition && Number.isFinite(scrollPosition.x) && scrollPosition.x >= 0 && Number.isFinite(scrollPosition.y) && scrollPosition.y >= 0); } @@ -45,10 +45,14 @@ class LazyLoadComponent extends React.Component { return this.props.children; } - const { className, delayMethod, delayTime, height, placeholder, - scrollPosition, style, threshold, width } = this.props; + const { className, delayMethod, delayTime, height, + placeholder, scrollPosition, style, threshold, + useIntersectionObserver, width } = this.props; - if (this.isScrollTracked || isIntersectionObserverAvailable()) { + if ( + this.isScrollTracked || + (useIntersectionObserver && isIntersectionObserverAvailable()) + ) { return ( <PlaceholderWithoutTracking className={className} @@ -58,6 +62,7 @@ class LazyLoadComponent extends React.Component { scrollPosition={scrollPosition} style={style} threshold={threshold} + useIntersectionObserver={useIntersectionObserver} width={width} /> ); } @@ -80,12 +85,14 @@ class LazyLoadComponent extends React.Component { LazyLoadComponent.propTypes = { afterLoad: PropTypes.func, beforeLoad: PropTypes.func, + useIntersectionObserver: PropTypes.bool, visibleByDefault: PropTypes.bool, }; LazyLoadComponent.defaultProps = { afterLoad: () => ({}), beforeLoad: () => ({}), + useIntersectionObserver: true, visibleByDefault: false, }; diff --git a/src/components/LazyLoadComponent.spec.js b/src/components/LazyLoadComponent.spec.js index e236b63..c0f2cf0 100644 --- a/src/components/LazyLoadComponent.spec.js +++ b/src/components/LazyLoadComponent.spec.js @@ -29,60 +29,6 @@ describe('LazyLoadComponent', function() { window.IntersectionObserver = windowIntersectionObserver; }); - it('renders a PlaceholderWithTracking when scrollPosition is undefined', function() { - const lazyLoadComponent = mount( - <LazyLoadComponent - style={{ marginTop: 100000 }}> - <p>Lorem Ipsum</p> - </LazyLoadComponent> - ); - - const placeholderWithTracking = scryRenderedComponentsWithType( - lazyLoadComponent.instance(), PlaceholderWithTracking); - - expect(placeholderWithTracking.length).toEqual(1); - }); - - it('renders a PlaceholderWithoutTracking when scrollPosition is undefined but IntersectionObserver is available', function() { - isIntersectionObserverAvailable.mockImplementation(() => true); - window.IntersectionObserver = jest.fn(function() { - this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this - }); - - const lazyLoadComponent = mount( - <LazyLoadComponent - style={{ marginTop: 100000 }}> - <p>Lorem Ipsum</p> - </LazyLoadComponent> - ); - - const placeholderWithTracking = scryRenderedComponentsWithType( - lazyLoadComponent.instance(), PlaceholderWithTracking); - const placeholderWithoutTracking = scryRenderedComponentsWithType( - lazyLoadComponent.instance(), PlaceholderWithoutTracking); - - expect(placeholderWithTracking.length).toEqual(0); - expect(placeholderWithoutTracking.length).toEqual(1); - }); - - it('renders a PlaceholderWithoutTracking when scrollPosition is defined', function() { - const lazyLoadComponent = mount( - <LazyLoadComponent - scrollPosition={{ x: 0, y: 0 }} - style={{ marginTop: 100000 }}> - <p>Lorem Ipsum</p> - </LazyLoadComponent> - ); - - const placeholderWithTracking = scryRenderedComponentsWithType( - lazyLoadComponent.instance(), PlaceholderWithTracking); - const placeholderWithoutTracking = scryRenderedComponentsWithType( - lazyLoadComponent.instance(), PlaceholderWithoutTracking); - - expect(placeholderWithTracking.length).toEqual(0); - expect(placeholderWithoutTracking.length).toEqual(1); - }); - it('renders children when visible', function() { const lazyLoadComponent = mount( <LazyLoadComponent> @@ -98,51 +44,149 @@ describe('LazyLoadComponent', function() { expect(paragraphs.length).toEqual(1); }); - it('triggers beforeLoad when onVisible is triggered', function() { - const beforeLoad = jest.fn(); - const lazyLoadComponent = mount( - <LazyLoadComponent - beforeLoad={beforeLoad} - style={{ marginTop: 100000 }}> - <p>Lorem Ipsum</p> - </LazyLoadComponent> - ); + describe('placeholders', function() { + it('renders a PlaceholderWithTracking when scrollPosition is undefined', function() { + const lazyLoadComponent = mount( + <LazyLoadComponent + style={{ marginTop: 100000 }} + > + <p>Lorem Ipsum</p> + </LazyLoadComponent> + ); + + const placeholderWithTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithTracking + ); + + expect(placeholderWithTracking.length).toEqual(1); + }); - lazyLoadComponent.instance().onVisible(); + it('renders a PlaceholderWithTracking when when IntersectionObserver is available but useIntersectionObserver is set to false', function() { + isIntersectionObserverAvailable.mockImplementation(() => true); + window.IntersectionObserver = jest.fn(function() { + this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this + }); + + const lazyLoadComponent = mount( + <LazyLoadComponent + useIntersectionObserver={false} + style={{ marginTop: 100000 }} + > + <p>Lorem Ipsum</p> + </LazyLoadComponent> + ); + + const placeholderWithTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithTracking + ); + const placeholderWithoutTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithoutTracking + ); + + expect(placeholderWithTracking.length).toEqual(1); + }); - expect(beforeLoad).toHaveBeenCalledTimes(1); + it('renders a PlaceholderWithoutTracking when scrollPosition is undefined but IntersectionObserver is available', function() { + isIntersectionObserverAvailable.mockImplementation(() => true); + window.IntersectionObserver = jest.fn(function() { + this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this + }); + + const lazyLoadComponent = mount( + <LazyLoadComponent + style={{ marginTop: 100000 }} + > + <p>Lorem Ipsum</p> + </LazyLoadComponent> + ); + + const placeholderWithTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithTracking + ); + const placeholderWithoutTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithoutTracking + ); + + expect(placeholderWithTracking.length).toEqual(0); + expect(placeholderWithoutTracking.length).toEqual(1); + }); + + it('renders a PlaceholderWithoutTracking when scrollPosition is defined', function() { + const lazyLoadComponent = mount( + <LazyLoadComponent + scrollPosition={{ x: 0, y: 0 }} + style={{ marginTop: 100000 }} + > + <p>Lorem Ipsum</p> + </LazyLoadComponent> + ); + + const placeholderWithTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithTracking + ); + const placeholderWithoutTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithoutTracking + ); + + expect(placeholderWithTracking.length).toEqual(0); + expect(placeholderWithoutTracking.length).toEqual(1); + }); }); - it('triggers afterLoad when onVisible is triggered', function() { - const afterLoad = jest.fn(); - const lazyLoadComponent = mount( - <LazyLoadComponent - afterLoad={afterLoad} - style={{ marginTop: 100000 }}> - <p>Lorem Ipsum</p> - </LazyLoadComponent> - ); + describe('beforeLoad/afterLoad', function() { + it('triggers beforeLoad when onVisible is triggered', function() { + const beforeLoad = jest.fn(); + const lazyLoadComponent = mount( + <LazyLoadComponent + beforeLoad={beforeLoad} + style={{ marginTop: 100000 }}> + <p>Lorem Ipsum</p> + </LazyLoadComponent> + ); - lazyLoadComponent.instance().onVisible(); + lazyLoadComponent.instance().onVisible(); - expect(afterLoad).toHaveBeenCalledTimes(1); - }); + expect(beforeLoad).toHaveBeenCalledTimes(1); + }); - it('triggers beforeLoad and afterLoad when visibleByDefault is true', function() { - const afterLoad = jest.fn(); - const beforeLoad = jest.fn(); - const lazyLoadComponent = mount( - <LazyLoadComponent - afterLoad={afterLoad} - beforeLoad={beforeLoad} - style={{ marginTop: 100000 }}> - <p>Lorem Ipsum</p> - </LazyLoadComponent> - ); + it('triggers afterLoad when onVisible is triggered', function() { + const afterLoad = jest.fn(); + const lazyLoadComponent = mount( + <LazyLoadComponent + afterLoad={afterLoad} + style={{ marginTop: 100000 }}> + <p>Lorem Ipsum</p> + </LazyLoadComponent> + ); - lazyLoadComponent.instance().onVisible(); + lazyLoadComponent.instance().onVisible(); - expect(afterLoad).toHaveBeenCalledTimes(1); - expect(beforeLoad).toHaveBeenCalledTimes(1); + expect(afterLoad).toHaveBeenCalledTimes(1); + }); + + it('triggers beforeLoad and afterLoad when visibleByDefault is true', function() { + const afterLoad = jest.fn(); + const beforeLoad = jest.fn(); + const lazyLoadComponent = mount( + <LazyLoadComponent + afterLoad={afterLoad} + beforeLoad={beforeLoad} + style={{ marginTop: 100000 }}> + <p>Lorem Ipsum</p> + </LazyLoadComponent> + ); + + lazyLoadComponent.instance().onVisible(); + + expect(afterLoad).toHaveBeenCalledTimes(1); + expect(beforeLoad).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/src/components/LazyLoadImage.jsx b/src/components/LazyLoadImage.jsx index 2fcf9e2..56cb826 100644 --- a/src/components/LazyLoadImage.jsx +++ b/src/components/LazyLoadImage.jsx @@ -29,7 +29,8 @@ class LazyLoadImage extends React.Component { getImg() { const { afterLoad, beforeLoad, delayMethod, delayTime, effect, placeholder, placeholderSrc, scrollPosition, threshold, - visibleByDefault, wrapperClassName, ...imgProps } = this.props; + useIntersectionObserver, visibleByDefault, wrapperClassName, + ...imgProps } = this.props; return <img onLoad={this.onImageLoad()} {...imgProps} />; } @@ -37,7 +38,7 @@ class LazyLoadImage extends React.Component { getLazyLoadImage(image) { const { beforeLoad, className, delayMethod, delayTime, height, placeholder, scrollPosition, style, threshold, - visibleByDefault, width } = this.props; + useIntersectionObserver, visibleByDefault, width } = this.props; return ( <LazyLoadComponent @@ -50,6 +51,7 @@ class LazyLoadImage extends React.Component { scrollPosition={scrollPosition} style={style} threshold={threshold} + useIntersectionObserver={useIntersectionObserver} visibleByDefault={visibleByDefault} width={width}> {image} @@ -107,6 +109,7 @@ LazyLoadImage.propTypes = { effect: PropTypes.string, placeholderSrc: PropTypes.string, threshold: PropTypes.number, + useIntersectionObserver: PropTypes.bool, visibleByDefault: PropTypes.bool, wrapperClassName: PropTypes.string, }; @@ -119,6 +122,7 @@ LazyLoadImage.defaultProps = { effect: '', placeholderSrc: '', threshold: 100, + useIntersectionObserver: true, visibleByDefault: false, wrapperClassName: '', }; diff --git a/src/components/PlaceholderWithoutTracking.jsx b/src/components/PlaceholderWithoutTracking.jsx index 924b26a..935ac97 100644 --- a/src/components/PlaceholderWithoutTracking.jsx +++ b/src/components/PlaceholderWithoutTracking.jsx @@ -7,7 +7,8 @@ class PlaceholderWithoutTracking extends React.Component { constructor(props) { super(props); - const supportsObserver = isIntersectionObserverAvailable(); + const supportsObserver = !props.scrollPosition && + props.useIntersectionObserver && isIntersectionObserverAvailable(); this.LAZY_LOAD_OBSERVER = { supportsObserver }; @@ -120,6 +121,7 @@ PlaceholderWithoutTracking.propTypes = { height: PropTypes.number, placeholder: PropTypes.element, threshold: PropTypes.number, + useIntersectionObserver: PropTypes.bool, scrollPosition: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, @@ -132,6 +134,7 @@ PlaceholderWithoutTracking.defaultProps = { height: 0, placeholder: null, threshold: 100, + useIntersectionObserver: true, width: 0, }; diff --git a/src/components/PlaceholderWithoutTracking.spec.js b/src/components/PlaceholderWithoutTracking.spec.js index ad6dc9b..c829908 100644 --- a/src/components/PlaceholderWithoutTracking.spec.js +++ b/src/components/PlaceholderWithoutTracking.spec.js @@ -190,6 +190,22 @@ describe('PlaceholderWithoutTracking', function() { const onVisible = jest.fn(); const component = renderPlaceholderWithoutTracking({ onVisible, + scrollPosition: null, + }); + + expect(onVisible).toHaveBeenCalledTimes(0); + }); + + it('tracks placeholder visibility when IntersectionObserver is available but scrollPosition is set', function() { + isIntersectionObserverAvailable.mockImplementation(() => true); + window.IntersectionObserver = jest.fn(function() { + this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this + }); + const offset = 100000; + const onVisible = jest.fn(); + const component = renderPlaceholderWithoutTracking({ + onVisible, + style: { marginLeft: offset }, }); expect(onVisible).toHaveBeenCalledTimes(0); diff --git a/src/hoc/trackWindowScroll.js b/src/hoc/trackWindowScroll.js index de731ff..72c4e37 100644 --- a/src/hoc/trackWindowScroll.js +++ b/src/hoc/trackWindowScroll.js @@ -16,7 +16,9 @@ const trackWindowScroll = (BaseComponent) => { constructor(props) { super(props); - if (isIntersectionObserverAvailable()) { + this.useIntersectionObserver = + props.useIntersectionObserver && isIntersectionObserverAvailable(); + if (this.useIntersectionObserver) { return; } @@ -47,7 +49,7 @@ const trackWindowScroll = (BaseComponent) => { } componentDidUpdate() { - if (typeof window === 'undefined' || isIntersectionObserverAvailable()) { + if (typeof window === 'undefined' || this.useIntersectionObserver) { return; } @@ -62,7 +64,7 @@ const trackWindowScroll = (BaseComponent) => { } addListeners() { - if (typeof window === 'undefined' || isIntersectionObserverAvailable()) { + if (typeof window === 'undefined' || this.useIntersectionObserver) { return; } @@ -91,7 +93,7 @@ const trackWindowScroll = (BaseComponent) => { } removeListeners() { - if (typeof window == 'undefined' || isIntersectionObserverAvailable()) { + if (typeof window == 'undefined' || this.useIntersectionObserver) { return; } @@ -104,7 +106,7 @@ const trackWindowScroll = (BaseComponent) => { } onChangeScroll() { - if (isIntersectionObserverAvailable()) { + if (this.useIntersectionObserver) { return; } @@ -118,7 +120,7 @@ const trackWindowScroll = (BaseComponent) => { render() { const { delayMethod, delayTime, ...props } = this.props; - const scrollPosition = isIntersectionObserverAvailable() ? + const scrollPosition = this.useIntersectionObserver ? null : this.state.scrollPosition; return ( @@ -133,11 +135,13 @@ const trackWindowScroll = (BaseComponent) => { ScrollAwareComponent.propTypes = { delayMethod: PropTypes.oneOf(['debounce', 'throttle']), delayTime: PropTypes.number, + useIntersectionObserver: PropTypes.bool, }; ScrollAwareComponent.defaultProps = { delayMethod: 'throttle', delayTime: 300, + useIntersectionObserver: true, }; return ScrollAwareComponent;