diff --git a/.eslintrc b/.eslintrc index 3c4fa33..ac49a2c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -133,7 +133,7 @@ "padded-blocks": [1, "never"], "quote-props": [1, "as-needed"], "quotes": [1, 'single'], - "require-jsdoc": 1, + "require-jsdoc": 0, "semi-spacing": 1, "semi": 1, "sort-keys": 0, diff --git a/src/components/LazyLoadComponent.jsx b/src/components/LazyLoadComponent.jsx index f868e51..5bc064a 100644 --- a/src/components/LazyLoadComponent.jsx +++ b/src/components/LazyLoadComponent.jsx @@ -3,6 +3,7 @@ import { PropTypes } from 'prop-types'; import PlaceholderWithoutTracking from './PlaceholderWithoutTracking.jsx'; import PlaceholderWithTracking from './PlaceholderWithTracking.jsx'; +import isIntersectionObserverAvailable from '../utils/intersection-observer'; class LazyLoadComponent extends React.Component { constructor(props) { @@ -47,7 +48,7 @@ class LazyLoadComponent extends React.Component { const { className, height, placeholder, scrollPosition, style, threshold, width } = this.props; - if (this.isScrollTracked) { + if (this.isScrollTracked || isIntersectionObserverAvailable()) { return ( { + isIntersectionObserverAvailable.mockImplementation(() => false); + }); + + afterEach(() => { + window.IntersectionObserver = windowIntersectionObserver; + }); + it('renders a PlaceholderWithTracking when scrollPosition is undefined', function() { const lazyLoadComponent = mount( true); + window.IntersectionObserver = jest.fn(function() { + this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this + }); + + const lazyLoadComponent = mount( + +

Lorem Ipsum

+
+ ); + + 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( ); + const placeholderWithTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), PlaceholderWithTracking); const placeholderWithoutTracking = scryRenderedComponentsWithType( lazyLoadComponent.instance(), PlaceholderWithoutTracking); + expect(placeholderWithTracking.length).toEqual(0); expect(placeholderWithoutTracking.length).toEqual(1); }); diff --git a/src/components/PlaceholderWithoutTracking.jsx b/src/components/PlaceholderWithoutTracking.jsx index 3502d49..924b26a 100644 --- a/src/components/PlaceholderWithoutTracking.jsx +++ b/src/components/PlaceholderWithoutTracking.jsx @@ -1,18 +1,57 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { PropTypes } from 'prop-types'; +import isIntersectionObserverAvailable from '../utils/intersection-observer'; class PlaceholderWithoutTracking extends React.Component { constructor(props) { super(props); + + const supportsObserver = isIntersectionObserverAvailable(); + + this.LAZY_LOAD_OBSERVER = { supportsObserver }; + + if (supportsObserver) { + const { threshold } = props; + + this.LAZY_LOAD_OBSERVER.observer = new IntersectionObserver( + this.checkIntersections, { rootMargin: threshold + 'px' } + ); + } + } + + checkIntersections(entries) { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.onVisible(); + } + }); } componentDidMount() { - this.updateVisibility(); + if (this.placeholder && + this.LAZY_LOAD_OBSERVER && this.LAZY_LOAD_OBSERVER.observer) { + this.placeholder.onVisible = this.props.onVisible; + this.LAZY_LOAD_OBSERVER.observer.observe(this.placeholder); + } + + if (this.LAZY_LOAD_OBSERVER && + !this.LAZY_LOAD_OBSERVER.supportsObserver) { + this.updateVisibility(); + } + } + + componentWillUnMount() { + if (this.LAZY_LOAD_OBSERVER) { + this.LAZY_LOAD_OBSERVER.observer.unobserve(this.placeholder); + } } componentDidUpdate() { - this.updateVisibility(); + if (this.LAZY_LOAD_OBSERVER && + !this.LAZY_LOAD_OBSERVER.supportsObserver) { + this.updateVisibility(); + } } getPlaceholderBoundingBox(scrollPosition = this.props.scrollPosition) { @@ -77,14 +116,14 @@ class PlaceholderWithoutTracking extends React.Component { PlaceholderWithoutTracking.propTypes = { onVisible: PropTypes.func.isRequired, - scrollPosition: PropTypes.shape({ - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, - }).isRequired, className: PropTypes.string, height: PropTypes.number, placeholder: PropTypes.element, threshold: PropTypes.number, + scrollPosition: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }), width: PropTypes.number, }; diff --git a/src/components/PlaceholderWithoutTracking.spec.js b/src/components/PlaceholderWithoutTracking.spec.js index 39e6948..ad6dc9b 100644 --- a/src/components/PlaceholderWithoutTracking.spec.js +++ b/src/components/PlaceholderWithoutTracking.spec.js @@ -8,6 +8,9 @@ import { configure, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import PlaceholderWithoutTracking from './PlaceholderWithoutTracking.jsx'; +import isIntersectionObserverAvailable from '../utils/intersection-observer'; + +jest.mock('../utils/intersection-observer'); configure({ adapter: new Adapter() }); @@ -73,6 +76,16 @@ describe('PlaceholderWithoutTracking', function() { expect(placeholderWrapper.length).toEqual(numberOfPlaceholderWrappers); } + const windowIntersectionObserver = window.IntersectionObserver; + + beforeEach(() => { + isIntersectionObserverAvailable.mockImplementation(() => false); + }); + + afterEach(() => { + window.IntersectionObserver = windowIntersectionObserver; + }); + it('renders the default placeholder when it\'s not in the viewport', function() { const className = 'placeholder-wrapper'; const component = renderPlaceholderWithoutTracking({ @@ -168,4 +181,17 @@ describe('PlaceholderWithoutTracking', function() { expect(onVisible).toHaveBeenCalledTimes(1); }); + + it('doesn\'t track placeholder visibility if IntersectionObserver is available', function() { + isIntersectionObserverAvailable.mockImplementation(() => true); + window.IntersectionObserver = jest.fn(function() { + this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this + }); + const onVisible = jest.fn(); + const component = renderPlaceholderWithoutTracking({ + onVisible, + }); + + expect(onVisible).toHaveBeenCalledTimes(0); + }); }); diff --git a/src/hoc/trackWindowScroll.js b/src/hoc/trackWindowScroll.js index b4c20dc..832749b 100644 --- a/src/hoc/trackWindowScroll.js +++ b/src/hoc/trackWindowScroll.js @@ -2,12 +2,17 @@ import React from 'react'; import { PropTypes } from 'prop-types'; import debounce from 'lodash.debounce'; import throttle from 'lodash.throttle'; +import isIntersectionObserverAvailable from '../utils/intersection-observer'; const trackWindowScroll = (BaseComponent) => { class ScrollAwareComponent extends React.Component { constructor(props) { super(props); + if (isIntersectionObserverAvailable()) { + return; + } + const onChangeScroll = this.onChangeScroll.bind(this); if (props.delayMethod === 'debounce') { @@ -31,7 +36,7 @@ const trackWindowScroll = (BaseComponent) => { } componentDidMount() { - if (typeof window == 'undefined') { + if (typeof window == 'undefined' || isIntersectionObserverAvailable()) { return; } window.addEventListener('scroll', this.delayedScroll); @@ -39,7 +44,7 @@ const trackWindowScroll = (BaseComponent) => { } componentWillUnmount() { - if (typeof window === 'undefined') { + if (typeof window == 'undefined' || isIntersectionObserverAvailable()) { return; } window.removeEventListener('scroll', this.delayedScroll); @@ -47,6 +52,9 @@ const trackWindowScroll = (BaseComponent) => { } onChangeScroll() { + if (isIntersectionObserverAvailable()) { + return; + } this.setState({ scrollPosition: { x: (typeof window == 'undefined' ? @@ -63,10 +71,12 @@ const trackWindowScroll = (BaseComponent) => { render() { const { delayMethod, delayTime, ...props } = this.props; + const scrollPosition = isIntersectionObserverAvailable() ? + null : this.state.scrollPosition; return ( ); } diff --git a/src/utils/intersection-observer.js b/src/utils/intersection-observer.js new file mode 100644 index 0000000..97840e3 --- /dev/null +++ b/src/utils/intersection-observer.js @@ -0,0 +1,6 @@ +export default function() { + return ( + 'IntersectionObserver' in window && + 'isIntersecting' in window.IntersectionObserverEntry.prototype + ); +} diff --git a/src/utils/intersection-observer.spec.js b/src/utils/intersection-observer.spec.js new file mode 100644 index 0000000..fc5fd16 --- /dev/null +++ b/src/utils/intersection-observer.spec.js @@ -0,0 +1,24 @@ +import isIntersectionObserverAvailable from './intersection-observer'; + +describe('isIntersectionObserverAvailable', function() { + it('returns true if IntersectionObserver is available', function() { + window.IntersectionObserver = {}; + window.IntersectionObserverEntry = { + prototype: { + isIntersecting: () => null, + }, + }; + + expect(isIntersectionObserverAvailable()).toBe(true); + }); + + it('returns false if IntersectionObserver is not available', function() { + delete window.IntersectionObserver; + window.IntersectionObserverEntry = { + prototype: {}, + }; + delete window.IntersectionObserverEntry; + + expect(isIntersectionObserverAvailable()).toBe(false); + }); +});