From e3d013523467672529e585a384b489ba54c964d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Tue, 24 Dec 2019 15:33:28 +0100 Subject: [PATCH] Add Prettier --- .eslintrc | 12 +- .prettierrc | 12 + package.json | 141 ++++--- src/components/LazyLoadComponent.jsx | 184 ++++---- src/components/LazyLoadComponent.spec.js | 345 +++++++-------- src/components/LazyLoadImage.jsx | 264 +++++++----- src/components/LazyLoadImage.spec.js | 290 +++++++------ src/components/PlaceholderWithTracking.jsx | 14 +- src/components/PlaceholderWithoutTracking.jsx | 267 ++++++------ .../PlaceholderWithoutTracking.spec.js | 395 +++++++++--------- src/hoc/trackWindowScroll.js | 278 ++++++------ src/utils/get-scroll-element.js | 63 +-- src/utils/intersection-observer.js | 10 +- src/utils/intersection-observer.spec.js | 34 +- webpack.config.js | 74 ++-- 15 files changed, 1264 insertions(+), 1119 deletions(-) create mode 100644 .prettierrc diff --git a/.eslintrc b/.eslintrc index ac49a2c..b17f89b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -92,7 +92,7 @@ "func-style": 0, "id-length": 0, "id-match": 1, - "indent": [1, 2, { "SwitchCase": 1 }], + "indent": [1, "tab", { "SwitchCase": 1 }], "jsx-quotes": 1, "key-spacing": 1, "line-comment-position": 0, @@ -129,10 +129,16 @@ "one-var-declaration-per-line": 0, "one-var": 0, "operator-assignment": 1, - "operator-linebreak": [1, "after"], + "operator-linebreak": [1, "after", + { + "overrides": { + "?": "before", + ":": "before" + } + }], "padded-blocks": [1, "never"], "quote-props": [1, "as-needed"], - "quotes": [1, 'single'], + "quotes": [1, "single", { "avoidEscape": true }], "require-jsdoc": 0, "semi-spacing": 1, "semi": 1, diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2ec75ba --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +printWidth: 80 +tabWidth: 4 +useTabs: true +semi: true +singleQuote: true +quoteProps: "as-needed" +jsxSingleQuote: false +trailingComma: es5 +jsxBracketSameLine: false +bracketSpacing: true +arrowParens: avoid +endOfLine: "lf" diff --git a/package.json b/package.json index f5b57c9..098090c 100644 --- a/package.json +++ b/package.json @@ -1,68 +1,77 @@ { - "name": "react-lazy-load-image-component", - "version": "1.4.0-beta.1", - "description": " React Component to lazy load images using a HOC to track window scroll position. ", - "main": "build/index.js", - "peerDependencies": { - "react": "^15.x.x || ^16.x.x", - "react-dom": "^15.x.x || ^16.x.x" - }, - "dependencies": { - "lodash.debounce": "^4.0.8", - "lodash.throttle": "^4.1.1" - }, - "devDependencies": { - "babel-cli": "^6.24.1", - "babel-core": "^6.26.0", - "babel-eslint": "^8.2.2", - "babel-jest": "^22.4.1", - "babel-loader": "^7.1.4", - "babel-plugin-transform-object-rest-spread": "^6.26.0", - "babel-preset-env": "^1.6.1", - "babel-preset-react": "^6.24.1", - "css-loader": "^0.28.10", - "enzyme": "^3.4.4", - "enzyme-adapter-react-16": "^1.2.0", - "eslint": "^4.18.2", - "eslint-loader": "^2.0.0", - "eslint-plugin-babel": "^4.1.2", - "eslint-plugin-react": "^7.11.1", - "jest": "^23.5.0", - "path": "^0.12.7", - "react": "^16.2.0", - "react-dom": "^16.2.0", - "style-loader": "^0.20.3", - "webpack": "^4.17.1", - "webpack-cli": "^3.1.2" - }, - "scripts": { - "test": "jest", - "start": "webpack --watch", - "build": "webpack" - }, - "jest": { - "verbose": true, - "testURL": "http://localhost/" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/Aljullu/react-lazy-load-image-component.git" - }, - "keywords": [ - "react", - "react-component", - "lazyload", - "lazyloading", - "lazy-loading", - "lazyload-images" - ], - "author": { - "name": "Albert Juhé Lluveras", - "email": "contact@albertjuhe.com" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/Aljullu/react-lazy-load-image-component/issues" - }, - "homepage": "https://github.com/Aljullu/react-lazy-load-image-component#readme" + "name": "react-lazy-load-image-component", + "version": "1.4.0-beta.1", + "description": " React Component to lazy load images using a HOC to track window scroll position. ", + "main": "build/index.js", + "peerDependencies": { + "react": "^15.x.x || ^16.x.x", + "react-dom": "^15.x.x || ^16.x.x" + }, + "dependencies": { + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1" + }, + "devDependencies": { + "babel-cli": "^6.24.1", + "babel-core": "^6.26.0", + "babel-eslint": "^8.2.2", + "babel-jest": "^22.4.1", + "babel-loader": "^7.1.4", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-preset-env": "^1.6.1", + "babel-preset-react": "^6.24.1", + "css-loader": "^0.28.10", + "enzyme": "^3.4.4", + "enzyme-adapter-react-16": "^1.2.0", + "eslint": "^4.18.2", + "eslint-loader": "^2.0.0", + "eslint-plugin-babel": "^4.1.2", + "eslint-plugin-react": "^7.11.1", + "husky": "^3.1.0", + "jest": "^23.5.0", + "path": "^0.12.7", + "prettier": "^1.19.1", + "react": "^16.2.0", + "react-dom": "^16.2.0", + "style-loader": "^0.20.3", + "webpack": "^4.17.1", + "webpack-cli": "^3.1.2" + }, + "scripts": { + "test": "jest", + "start": "webpack --watch", + "build": "webpack", + "prettier": "prettier --write \"**/*.{js,jsx,json}\"", + "lint": "eslint src --ext=js,jsx" + }, + "jest": { + "verbose": true, + "testURL": "http://localhost/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Aljullu/react-lazy-load-image-component.git" + }, + "keywords": [ + "react", + "react-component", + "lazyload", + "lazyloading", + "lazy-loading", + "lazyload-images" + ], + "author": { + "name": "Albert Juhé Lluveras", + "email": "contact@albertjuhe.com" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/Aljullu/react-lazy-load-image-component/issues" + }, + "homepage": "https://github.com/Aljullu/react-lazy-load-image-component#readme", + "husky": { + "hooks": { + "pre-commit": "npm run prettier && npm run lint" + } + } } diff --git a/src/components/LazyLoadComponent.jsx b/src/components/LazyLoadComponent.jsx index 05300fa..2b59e41 100644 --- a/src/components/LazyLoadComponent.jsx +++ b/src/components/LazyLoadComponent.jsx @@ -6,94 +6,114 @@ import PlaceholderWithTracking from './PlaceholderWithTracking.jsx'; import isIntersectionObserverAvailable from '../utils/intersection-observer'; class LazyLoadComponent extends React.Component { - constructor(props) { - super(props); - - const { afterLoad, beforeLoad, scrollPosition, visibleByDefault } = props; - - this.state = { - visible: visibleByDefault, - }; - - if (visibleByDefault) { - beforeLoad(); - afterLoad(); - } - - this.onVisible = this.onVisible.bind(this); - - this.isScrollTracked = Boolean(scrollPosition && - Number.isFinite(scrollPosition.x) && scrollPosition.x >= 0 && - Number.isFinite(scrollPosition.y) && scrollPosition.y >= 0); - } - - componentDidUpdate(prevProps, prevState) { - if (prevState.visible !== this.state.visible) { - this.props.afterLoad(); - } - } - - onVisible() { - this.props.beforeLoad(); - this.setState({ - visible: true, - }); - } - - render() { - if (this.state.visible) { - return this.props.children; - } - - const { className, delayMethod, delayTime, height, - placeholder, scrollPosition, style, threshold, - useIntersectionObserver, width } = this.props; - - if ( - this.isScrollTracked || - (useIntersectionObserver && isIntersectionObserverAvailable()) - ) { - return ( - - ); - } - - return ( - - ); - } + constructor(props) { + super(props); + + const { + afterLoad, + beforeLoad, + scrollPosition, + visibleByDefault, + } = props; + + this.state = { + visible: visibleByDefault, + }; + + if (visibleByDefault) { + beforeLoad(); + afterLoad(); + } + + this.onVisible = this.onVisible.bind(this); + + this.isScrollTracked = Boolean( + scrollPosition && + Number.isFinite(scrollPosition.x) && + scrollPosition.x >= 0 && + Number.isFinite(scrollPosition.y) && + scrollPosition.y >= 0 + ); + } + + componentDidUpdate(prevProps, prevState) { + if (prevState.visible !== this.state.visible) { + this.props.afterLoad(); + } + } + + onVisible() { + this.props.beforeLoad(); + this.setState({ + visible: true, + }); + } + + render() { + if (this.state.visible) { + return this.props.children; + } + + const { + className, + delayMethod, + delayTime, + height, + placeholder, + scrollPosition, + style, + threshold, + useIntersectionObserver, + width, + } = this.props; + + if ( + this.isScrollTracked || + (useIntersectionObserver && isIntersectionObserverAvailable()) + ) { + return ( + + ); + } + + return ( + + ); + } } LazyLoadComponent.propTypes = { - afterLoad: PropTypes.func, - beforeLoad: PropTypes.func, - useIntersectionObserver: PropTypes.bool, - visibleByDefault: PropTypes.bool, + afterLoad: PropTypes.func, + beforeLoad: PropTypes.func, + useIntersectionObserver: PropTypes.bool, + visibleByDefault: PropTypes.bool, }; LazyLoadComponent.defaultProps = { - afterLoad: () => ({}), - beforeLoad: () => ({}), - useIntersectionObserver: true, - visibleByDefault: false, + afterLoad: () => ({}), + beforeLoad: () => ({}), + useIntersectionObserver: true, + visibleByDefault: false, }; export default LazyLoadComponent; diff --git a/src/components/LazyLoadComponent.spec.js b/src/components/LazyLoadComponent.spec.js index c0f2cf0..658d20b 100644 --- a/src/components/LazyLoadComponent.spec.js +++ b/src/components/LazyLoadComponent.spec.js @@ -14,179 +14,180 @@ jest.mock('../utils/intersection-observer'); configure({ adapter: new Adapter() }); const { - scryRenderedComponentsWithType, - scryRenderedDOMComponentsWithTag, + scryRenderedComponentsWithType, + scryRenderedDOMComponentsWithTag, } = ReactTestUtils; describe('LazyLoadComponent', function() { - const windowIntersectionObserver = window.IntersectionObserver; - - beforeEach(() => { - isIntersectionObserverAvailable.mockImplementation(() => false); - }); - - afterEach(() => { - window.IntersectionObserver = windowIntersectionObserver; - }); - - it('renders children when visible', function() { - const lazyLoadComponent = mount( - -

Lorem Ipsum

-
- ); - - lazyLoadComponent.instance().onVisible(); - - const paragraphs = scryRenderedDOMComponentsWithTag( - lazyLoadComponent.instance(), 'p'); - - expect(paragraphs.length).toEqual(1); - }); - - describe('placeholders', function() { - it('renders a PlaceholderWithTracking when scrollPosition is undefined', function() { - const lazyLoadComponent = mount( - -

Lorem Ipsum

-
- ); - - const placeholderWithTracking = scryRenderedComponentsWithType( - lazyLoadComponent.instance(), - PlaceholderWithTracking - ); - - expect(placeholderWithTracking.length).toEqual(1); - }); - - 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( - -

Lorem Ipsum

-
- ); - - const placeholderWithTracking = scryRenderedComponentsWithType( - lazyLoadComponent.instance(), - PlaceholderWithTracking - ); - const placeholderWithoutTracking = scryRenderedComponentsWithType( - lazyLoadComponent.instance(), - PlaceholderWithoutTracking - ); - - 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( - -

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( - -

Lorem Ipsum

-
- ); - - const placeholderWithTracking = scryRenderedComponentsWithType( - lazyLoadComponent.instance(), - PlaceholderWithTracking - ); - const placeholderWithoutTracking = scryRenderedComponentsWithType( - lazyLoadComponent.instance(), - PlaceholderWithoutTracking - ); - - expect(placeholderWithTracking.length).toEqual(0); - expect(placeholderWithoutTracking.length).toEqual(1); - }); - }); - - describe('beforeLoad/afterLoad', function() { - it('triggers beforeLoad when onVisible is triggered', function() { - const beforeLoad = jest.fn(); - const lazyLoadComponent = mount( - -

Lorem Ipsum

-
- ); - - lazyLoadComponent.instance().onVisible(); - - expect(beforeLoad).toHaveBeenCalledTimes(1); - }); - - it('triggers afterLoad when onVisible is triggered', function() { - const afterLoad = jest.fn(); - const lazyLoadComponent = mount( - -

Lorem Ipsum

-
- ); - - lazyLoadComponent.instance().onVisible(); - - 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( - -

Lorem Ipsum

-
- ); - - lazyLoadComponent.instance().onVisible(); - - expect(afterLoad).toHaveBeenCalledTimes(1); - expect(beforeLoad).toHaveBeenCalledTimes(1); - }); - }); + const windowIntersectionObserver = window.IntersectionObserver; + + beforeEach(() => { + isIntersectionObserverAvailable.mockImplementation(() => false); + }); + + afterEach(() => { + window.IntersectionObserver = windowIntersectionObserver; + }); + + it('renders children when visible', function() { + const lazyLoadComponent = mount( + +

Lorem Ipsum

+
+ ); + + lazyLoadComponent.instance().onVisible(); + + const paragraphs = scryRenderedDOMComponentsWithTag( + lazyLoadComponent.instance(), + 'p' + ); + + expect(paragraphs.length).toEqual(1); + }); + + describe('placeholders', function() { + it('renders a PlaceholderWithTracking when scrollPosition is undefined', function() { + const lazyLoadComponent = mount( + +

Lorem Ipsum

+
+ ); + + const placeholderWithTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithTracking + ); + + expect(placeholderWithTracking.length).toEqual(1); + }); + + 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( + +

Lorem Ipsum

+
+ ); + + const placeholderWithTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithTracking + ); + const placeholderWithoutTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithoutTracking + ); + + 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( + +

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( + +

Lorem Ipsum

+
+ ); + + const placeholderWithTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithTracking + ); + const placeholderWithoutTracking = scryRenderedComponentsWithType( + lazyLoadComponent.instance(), + PlaceholderWithoutTracking + ); + + expect(placeholderWithTracking.length).toEqual(0); + expect(placeholderWithoutTracking.length).toEqual(1); + }); + }); + + describe('beforeLoad/afterLoad', function() { + it('triggers beforeLoad when onVisible is triggered', function() { + const beforeLoad = jest.fn(); + const lazyLoadComponent = mount( + +

Lorem Ipsum

+
+ ); + + lazyLoadComponent.instance().onVisible(); + + expect(beforeLoad).toHaveBeenCalledTimes(1); + }); + + it('triggers afterLoad when onVisible is triggered', function() { + const afterLoad = jest.fn(); + const lazyLoadComponent = mount( + +

Lorem Ipsum

+
+ ); + + lazyLoadComponent.instance().onVisible(); + + 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( + +

Lorem Ipsum

+
+ ); + + lazyLoadComponent.instance().onVisible(); + + expect(afterLoad).toHaveBeenCalledTimes(1); + expect(beforeLoad).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/src/components/LazyLoadImage.jsx b/src/components/LazyLoadImage.jsx index a59eda2..39a3317 100644 --- a/src/components/LazyLoadImage.jsx +++ b/src/components/LazyLoadImage.jsx @@ -4,127 +4,161 @@ import { PropTypes } from 'prop-types'; import LazyLoadComponent from './LazyLoadComponent.jsx'; class LazyLoadImage extends React.Component { - constructor(props) { - super(props); - - this.state = { - loaded: false, - }; - } - - onImageLoad() { - if (this.state.loaded) { - return null; - } - - return () => { - this.props.afterLoad(); - - this.setState({ - loaded: true, - }); - }; - } - - getImg() { - const { afterLoad, beforeLoad, delayMethod, delayTime, effect, - placeholder, placeholderSrc, scrollPosition, threshold, - useIntersectionObserver, visibleByDefault, wrapperClassName, - ...imgProps } = this.props; - - return ; - } - - getLazyLoadImage(image) { - const { beforeLoad, className, delayMethod, delayTime, - height, placeholder, scrollPosition, style, threshold, - useIntersectionObserver, visibleByDefault, width } = this.props; - - return ( - - {image} - - ); - } - - getWrappedLazyLoadImage(lazyLoadImage) { - const { effect, height, placeholderSrc, - width, wrapperClassName } = this.props; - const { loaded } = this.state; - - const loadedClassName = loaded ? - ' lazy-load-image-loaded' : - ''; - - return ( - - {lazyLoadImage} - - ); - } - - render() { - const { effect, placeholderSrc, visibleByDefault } = this.props; - const { loaded } = this.state; - - const image = this.getImg(); - const lazyLoadImage = loaded ? - image : this.getLazyLoadImage(image); - - if ((!effect && !placeholderSrc) || visibleByDefault) { - return lazyLoadImage; - } - - return this.getWrappedLazyLoadImage(lazyLoadImage); - } + constructor(props) { + super(props); + + this.state = { + loaded: false, + }; + } + + onImageLoad() { + if (this.state.loaded) { + return null; + } + + return () => { + this.props.afterLoad(); + + this.setState({ + loaded: true, + }); + }; + } + + getImg() { + const { + afterLoad, + beforeLoad, + delayMethod, + delayTime, + effect, + placeholder, + placeholderSrc, + scrollPosition, + threshold, + useIntersectionObserver, + visibleByDefault, + wrapperClassName, + ...imgProps + } = this.props; + + return ; + } + + getLazyLoadImage(image) { + const { + beforeLoad, + className, + delayMethod, + delayTime, + height, + placeholder, + scrollPosition, + style, + threshold, + useIntersectionObserver, + visibleByDefault, + width, + } = this.props; + + return ( + + {image} + + ); + } + + getWrappedLazyLoadImage(lazyLoadImage) { + const { + effect, + height, + placeholderSrc, + width, + wrapperClassName, + } = this.props; + const { loaded } = this.state; + + const loadedClassName = loaded ? ' lazy-load-image-loaded' : ''; + + return ( + + {lazyLoadImage} + + ); + } + + render() { + const { effect, placeholderSrc, visibleByDefault } = this.props; + const { loaded } = this.state; + + const image = this.getImg(); + const lazyLoadImage = loaded ? image : this.getLazyLoadImage(image); + + if ((!effect && !placeholderSrc) || visibleByDefault) { + return lazyLoadImage; + } + + return this.getWrappedLazyLoadImage(lazyLoadImage); + } } LazyLoadImage.propTypes = { - afterLoad: PropTypes.func, - beforeLoad: PropTypes.func, - delayMethod: PropTypes.string, - delayTime: PropTypes.number, - effect: PropTypes.string, - placeholderSrc: PropTypes.string, - threshold: PropTypes.number, - useIntersectionObserver: PropTypes.bool, - visibleByDefault: PropTypes.bool, - wrapperClassName: PropTypes.string, + afterLoad: PropTypes.func, + beforeLoad: PropTypes.func, + delayMethod: PropTypes.string, + delayTime: PropTypes.number, + effect: PropTypes.string, + placeholderSrc: PropTypes.string, + threshold: PropTypes.number, + useIntersectionObserver: PropTypes.bool, + visibleByDefault: PropTypes.bool, + wrapperClassName: PropTypes.string, }; LazyLoadImage.defaultProps = { - afterLoad: () => ({}), - beforeLoad: () => ({}), - delayMethod: 'throttle', - delayTime: 300, - effect: '', - placeholderSrc: null, - threshold: 100, - useIntersectionObserver: true, - visibleByDefault: false, - wrapperClassName: '', + afterLoad: () => ({}), + beforeLoad: () => ({}), + delayMethod: 'throttle', + delayTime: 300, + effect: '', + placeholderSrc: null, + threshold: 100, + useIntersectionObserver: true, + visibleByDefault: false, + wrapperClassName: '', }; export default LazyLoadImage; diff --git a/src/components/LazyLoadImage.spec.js b/src/components/LazyLoadImage.spec.js index 2515579..7c92225 100644 --- a/src/components/LazyLoadImage.spec.js +++ b/src/components/LazyLoadImage.spec.js @@ -11,135 +11,169 @@ import LazyLoadComponent from './LazyLoadComponent.jsx'; configure({ adapter: new Adapter() }); const { - findRenderedComponentWithType, - findRenderedDOMComponentWithClass, - findRenderedDOMComponentWithTag, - scryRenderedDOMComponentsWithClass, - scryRenderedDOMComponentsWithTag, - Simulate, + findRenderedComponentWithType, + findRenderedDOMComponentWithClass, + findRenderedDOMComponentWithTag, + scryRenderedDOMComponentsWithClass, + scryRenderedDOMComponentsWithTag, + Simulate, } = ReactTestUtils; describe('LazyLoadImage', function() { - it('renders a LazyLoadComponent with the correct props', function() { - const props = { - beforeLoad: () => null, - delayMethod: 'debounce', - delayTime: 600, - placeholder: null, - scrollPosition: { x: 0, y: 0 }, - style: {}, - src: 'http://localhost/lorem-ipsum.jpg', - visibleByDefault: false, - }; - const lazyLoadImage = mount( - - ); - - const lazyLoadComponent = findRenderedComponentWithType(lazyLoadImage.instance(), LazyLoadComponent); - const img = findRenderedDOMComponentWithTag(lazyLoadImage.instance(), 'img'); - - expect(lazyLoadComponent.props.beforeLoad).toEqual(props.beforeLoad); - expect(lazyLoadComponent.props.delayMethod).toEqual(props.delayMethod); - expect(lazyLoadComponent.props.delayTime).toEqual(props.delayTime); - expect(lazyLoadComponent.props.placeholder).toEqual(props.placeholder); - expect(lazyLoadComponent.props.scrollPosition).toEqual(props.scrollPosition); - expect(lazyLoadComponent.props.style).toEqual(props.style); - expect(lazyLoadComponent.props.visibleByDefault).toEqual(props.visibleByDefault); - expect(img.src).toEqual(props.src); - }); - - it('updates state and calls afterLoad when img triggers onLoad', function() { - const afterLoad = jest.fn(); - const lazyLoadImage = mount( - - ); - - const img = findRenderedDOMComponentWithTag(lazyLoadImage.instance(), 'img'); - - Simulate.load(img); - - expect(lazyLoadImage.instance().state.loaded); - expect(afterLoad).toHaveBeenCalledTimes(1); - }); - - it('sets loaded class to wrapper when img triggers onLoad', function() { - const lazyLoadImage = mount( - - ); - - const img = findRenderedDOMComponentWithTag(lazyLoadImage.instance(), 'img'); - - Simulate.load(img); - - const loadedWrapper = scryRenderedDOMComponentsWithClass(lazyLoadImage.instance(), 'lazy-load-image-loaded'); - - expect(loadedWrapper.length).toEqual(1); - }); - - it('resets the background-image and background-size when img triggers onLoad', function() { - const lazyLoadImage = mount( - - ); - - const img = findRenderedDOMComponentWithTag(lazyLoadImage.instance(), 'img'); - - Simulate.load(img); - - const loadedWrapper = findRenderedDOMComponentWithClass(lazyLoadImage.instance(), 'lazy-load-image-loaded'); - - expect(loadedWrapper.style.getPropertyValue('background-image')).toEqual(''); - expect(loadedWrapper.style.getPropertyValue('background-size')).toEqual(''); - }); - - it('adds the effect class', function() { - const lazyLoadImage = mount( - - ); - - const blurSpan = scryRenderedDOMComponentsWithClass(lazyLoadImage.instance(), 'blur'); - - expect(blurSpan.length).toEqual(1); - }); - - it('doesn\'t render placeholder background when not defined', function() { - const lazyLoadImage = mount( - - ); - - const span = scryRenderedDOMComponentsWithTag(lazyLoadImage.instance(), 'span'); - - expect(span.length).toEqual(0); - }); - - it('renders placeholder background when defined', function() { - const lazyLoadImage = mount( - - ); - - const span = scryRenderedDOMComponentsWithTag(lazyLoadImage.instance(), 'span'); - - expect(span.length).toEqual(1); - }); - - it('doesn\'t render placeholder background when visibleByDefault is true', function() { - const lazyLoadImage = mount( - - ); - - const span = scryRenderedDOMComponentsWithTag(lazyLoadImage.instance(), 'span'); - - expect(span.length).toEqual(0); - }); + it('renders a LazyLoadComponent with the correct props', function() { + const props = { + beforeLoad: () => null, + delayMethod: 'debounce', + delayTime: 600, + placeholder: null, + scrollPosition: { x: 0, y: 0 }, + style: {}, + src: 'http://localhost/lorem-ipsum.jpg', + visibleByDefault: false, + }; + const lazyLoadImage = mount( + + ); + + const lazyLoadComponent = findRenderedComponentWithType( + lazyLoadImage.instance(), + LazyLoadComponent + ); + const img = findRenderedDOMComponentWithTag( + lazyLoadImage.instance(), + 'img' + ); + + expect(lazyLoadComponent.props.beforeLoad).toEqual(props.beforeLoad); + expect(lazyLoadComponent.props.delayMethod).toEqual(props.delayMethod); + expect(lazyLoadComponent.props.delayTime).toEqual(props.delayTime); + expect(lazyLoadComponent.props.placeholder).toEqual(props.placeholder); + expect(lazyLoadComponent.props.scrollPosition).toEqual( + props.scrollPosition + ); + expect(lazyLoadComponent.props.style).toEqual(props.style); + expect(lazyLoadComponent.props.visibleByDefault).toEqual( + props.visibleByDefault + ); + expect(img.src).toEqual(props.src); + }); + + it('updates state and calls afterLoad when img triggers onLoad', function() { + const afterLoad = jest.fn(); + const lazyLoadImage = mount(); + + const img = findRenderedDOMComponentWithTag( + lazyLoadImage.instance(), + 'img' + ); + + Simulate.load(img); + + expect(lazyLoadImage.instance().state.loaded); + expect(afterLoad).toHaveBeenCalledTimes(1); + }); + + it('sets loaded class to wrapper when img triggers onLoad', function() { + const lazyLoadImage = mount(); + + const img = findRenderedDOMComponentWithTag( + lazyLoadImage.instance(), + 'img' + ); + + Simulate.load(img); + + const loadedWrapper = scryRenderedDOMComponentsWithClass( + lazyLoadImage.instance(), + 'lazy-load-image-loaded' + ); + + expect(loadedWrapper.length).toEqual(1); + }); + + it('resets the background-image and background-size when img triggers onLoad', function() { + const lazyLoadImage = mount(); + + const img = findRenderedDOMComponentWithTag( + lazyLoadImage.instance(), + 'img' + ); + + Simulate.load(img); + + const loadedWrapper = findRenderedDOMComponentWithClass( + lazyLoadImage.instance(), + 'lazy-load-image-loaded' + ); + + expect( + loadedWrapper.style.getPropertyValue('background-image') + ).toEqual(''); + expect(loadedWrapper.style.getPropertyValue('background-size')).toEqual( + '' + ); + }); + + it('adds the effect class', function() { + const lazyLoadImage = mount(); + + const blurSpan = scryRenderedDOMComponentsWithClass( + lazyLoadImage.instance(), + 'blur' + ); + + expect(blurSpan.length).toEqual(1); + }); + + it("doesn't render placeholder background when not defined", function() { + const lazyLoadImage = mount(); + + const span = scryRenderedDOMComponentsWithTag( + lazyLoadImage.instance(), + 'span' + ); + + expect(span.length).toEqual(0); + }); + + it('renders placeholder background when defined', function() { + const lazyLoadImage = mount( + + ); + + const span = scryRenderedDOMComponentsWithTag( + lazyLoadImage.instance(), + 'span' + ); + + expect(span.length).toEqual(1); + }); + + it("doesn't render placeholder background when visibleByDefault is true", function() { + const lazyLoadImage = mount( + + ); + + const span = scryRenderedDOMComponentsWithTag( + lazyLoadImage.instance(), + 'span' + ); + + expect(span.length).toEqual(0); + }); }); diff --git a/src/components/PlaceholderWithTracking.jsx b/src/components/PlaceholderWithTracking.jsx index 02cb87c..06a9649 100644 --- a/src/components/PlaceholderWithTracking.jsx +++ b/src/components/PlaceholderWithTracking.jsx @@ -4,15 +4,13 @@ import PlaceholderWithoutTracking from './PlaceholderWithoutTracking.jsx'; import trackWindowScroll from '../hoc/trackWindowScroll.js'; class PlaceholderWithTracking extends React.Component { - constructor(props) { - super(props); - } + constructor(props) { + super(props); + } - render() { - return ( - - ); - } + render() { + return ; + } } export default trackWindowScroll(PlaceholderWithTracking); diff --git a/src/components/PlaceholderWithoutTracking.jsx b/src/components/PlaceholderWithoutTracking.jsx index 935ac97..89ee10a 100644 --- a/src/components/PlaceholderWithoutTracking.jsx +++ b/src/components/PlaceholderWithoutTracking.jsx @@ -4,138 +4,153 @@ import { PropTypes } from 'prop-types'; import isIntersectionObserverAvailable from '../utils/intersection-observer'; class PlaceholderWithoutTracking extends React.Component { - constructor(props) { - super(props); - - const supportsObserver = !props.scrollPosition && - props.useIntersectionObserver && 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() { - 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() { - if (this.LAZY_LOAD_OBSERVER && - !this.LAZY_LOAD_OBSERVER.supportsObserver) { - 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 (typeof window === 'undefined' || !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.isPlaceholderInViewport()) { - this.props.onVisible(); - } - } - - render() { - const { className, height, placeholder, style, width } = this.props; - - if (placeholder && typeof placeholder.type !== 'function') { - return React.cloneElement(placeholder, - { ref: el => this.placeholder = el }); - } - - return ( - this.placeholder = el} - style={{ display: 'inline-block', height, width, ...style }}> - {placeholder} - - ); - } + constructor(props) { + super(props); + + const supportsObserver = + !props.scrollPosition && + props.useIntersectionObserver && + 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() { + 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() { + if ( + this.LAZY_LOAD_OBSERVER && + !this.LAZY_LOAD_OBSERVER.supportsObserver + ) { + 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 (typeof window === 'undefined' || !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.isPlaceholderInViewport()) { + this.props.onVisible(); + } + } + + render() { + const { className, height, placeholder, style, width } = this.props; + + if (placeholder && typeof placeholder.type !== 'function') { + return React.cloneElement(placeholder, { + ref: el => (this.placeholder = el), + }); + } + + return ( + (this.placeholder = el)} + style={{ display: 'inline-block', height, width, ...style }} + > + {placeholder} + + ); + } } PlaceholderWithoutTracking.propTypes = { - onVisible: PropTypes.func.isRequired, - className: PropTypes.string, - height: PropTypes.number, - placeholder: PropTypes.element, - threshold: PropTypes.number, - useIntersectionObserver: PropTypes.bool, - scrollPosition: PropTypes.shape({ - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, - }), - width: PropTypes.number, + onVisible: PropTypes.func.isRequired, + className: PropTypes.string, + height: PropTypes.number, + placeholder: PropTypes.element, + threshold: PropTypes.number, + useIntersectionObserver: PropTypes.bool, + scrollPosition: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }), + width: PropTypes.number, }; PlaceholderWithoutTracking.defaultProps = { - className: '', - height: 0, - placeholder: null, - threshold: 100, - useIntersectionObserver: true, - width: 0, + className: '', + height: 0, + placeholder: null, + threshold: 100, + useIntersectionObserver: true, + width: 0, }; export default PlaceholderWithoutTracking; diff --git a/src/components/PlaceholderWithoutTracking.spec.js b/src/components/PlaceholderWithoutTracking.spec.js index c829908..d6d4ae3 100644 --- a/src/components/PlaceholderWithoutTracking.spec.js +++ b/src/components/PlaceholderWithoutTracking.spec.js @@ -15,199 +15,210 @@ jest.mock('../utils/intersection-observer'); configure({ adapter: new Adapter() }); const { - scryRenderedDOMComponentsWithClass, - scryRenderedDOMComponentsWithTag, + scryRenderedDOMComponentsWithClass, + scryRenderedDOMComponentsWithTag, } = ReactTestUtils; describe('PlaceholderWithoutTracking', function() { - function renderPlaceholderWithoutTracking({ - onVisible = () => null, - placeholder = null, - scrollPosition = { x: 0, y: 0 }, - style = {}, - className = '', - } = {}) { - 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); - } - - function expectPlaceholderWrappers(wrapper, numberOfPlaceholderWrappers, className) { - const placeholderWrapper = scryRenderedDOMComponentsWithClass(wrapper.instance(), className); - - 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({ - style: { marginTop: 100000 }, - className, - }); - - expectParagraphs(component, 0); - expectPlaceholders(component, 1); - expectPlaceholderWrappers(component, 1, className); - }); - - it('renders the prop placeholder when it\'s not in the viewport', function() { - const style = { marginTop: 100000 }; - const className = 'placeholder-wrapper'; - const placeholder = ( - - ); - const component = renderPlaceholderWithoutTracking({ - placeholder, - style, - className, - }); - - expectParagraphs(component, 0); - expectPlaceholders(component, 1, 'strong'); - expectPlaceholderWrappers(component, 0, className); - }); - - it('renders the prop placeholder (React class) when it\'s not in the viewport', function() { - const style = { marginTop: 100000 }; - const className = 'placeholder-wrapper'; - class MyComponent extends React.Component { - render() { - return ( - - ); - } - } - const placeholder = (); - const component = renderPlaceholderWithoutTracking({ - placeholder, - style, - className, - }); - - expectParagraphs(component, 0); - expectPlaceholders(component, 1, 'strong'); - expectPlaceholderWrappers(component, 1, className); - }); - - it('doesn\'t trigger onVisible when the image is not the viewport', function() { - const onVisible = jest.fn(); - const component = renderPlaceholderWithoutTracking({ - onVisible, - style: { marginTop: 100000 }, - }); - - expect(onVisible).toHaveBeenCalledTimes(0); - }); - - it('triggers onVisible when the image is in the viewport', function() { - const onVisible = jest.fn(); - const component = renderPlaceholderWithoutTracking({ - onVisible, - }); - - expect(onVisible).toHaveBeenCalledTimes(1); - }); - - it('triggers onVisible when the image appears in the viewport', function() { - const onVisible = jest.fn(); - const offset = 100000; - const component = renderPlaceholderWithoutTracking({ - onVisible, - style: { marginTop: offset }, - }); - - simulateScroll(component, 0, offset); - - expect(onVisible).toHaveBeenCalledTimes(1); - }); - - it('triggers onVisible when the image appears in the viewport', function() { - const onVisible = jest.fn(); - const offset = 100000; - const component = renderPlaceholderWithoutTracking({ - onVisible, - style: { marginLeft: offset }, - }); - - simulateScroll(component, offset, 0); - - 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, - 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); - }); + function renderPlaceholderWithoutTracking({ + onVisible = () => null, + placeholder = null, + scrollPosition = { x: 0, y: 0 }, + style = {}, + className = '', + } = {}) { + 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); + } + + function expectPlaceholderWrappers( + wrapper, + numberOfPlaceholderWrappers, + className + ) { + const placeholderWrapper = scryRenderedDOMComponentsWithClass( + wrapper.instance(), + className + ); + + 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({ + style: { marginTop: 100000 }, + className, + }); + + expectParagraphs(component, 0); + expectPlaceholders(component, 1); + expectPlaceholderWrappers(component, 1, className); + }); + + it("renders the prop placeholder when it's not in the viewport", function() { + const style = { marginTop: 100000 }; + const className = 'placeholder-wrapper'; + const placeholder = ; + const component = renderPlaceholderWithoutTracking({ + placeholder, + style, + className, + }); + + expectParagraphs(component, 0); + expectPlaceholders(component, 1, 'strong'); + expectPlaceholderWrappers(component, 0, className); + }); + + it("renders the prop placeholder (React class) when it's not in the viewport", function() { + const style = { marginTop: 100000 }; + const className = 'placeholder-wrapper'; + class MyComponent extends React.Component { + render() { + return ; + } + } + const placeholder = ; + const component = renderPlaceholderWithoutTracking({ + placeholder, + style, + className, + }); + + expectParagraphs(component, 0); + expectPlaceholders(component, 1, 'strong'); + expectPlaceholderWrappers(component, 1, className); + }); + + it("doesn't trigger onVisible when the image is not the viewport", function() { + const onVisible = jest.fn(); + const component = renderPlaceholderWithoutTracking({ + onVisible, + style: { marginTop: 100000 }, + }); + + expect(onVisible).toHaveBeenCalledTimes(0); + }); + + it('triggers onVisible when the image is in the viewport', function() { + const onVisible = jest.fn(); + const component = renderPlaceholderWithoutTracking({ + onVisible, + }); + + expect(onVisible).toHaveBeenCalledTimes(1); + }); + + it('triggers onVisible when the image appears in the viewport', function() { + const onVisible = jest.fn(); + const offset = 100000; + const component = renderPlaceholderWithoutTracking({ + onVisible, + style: { marginTop: offset }, + }); + + simulateScroll(component, 0, offset); + + expect(onVisible).toHaveBeenCalledTimes(1); + }); + + it('triggers onVisible when the image appears in the viewport', function() { + const onVisible = jest.fn(); + const offset = 100000; + const component = renderPlaceholderWithoutTracking({ + onVisible, + style: { marginLeft: offset }, + }); + + simulateScroll(component, offset, 0); + + 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, + 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 72c4e37..c789f55 100644 --- a/src/hoc/trackWindowScroll.js +++ b/src/hoc/trackWindowScroll.js @@ -6,145 +6,145 @@ import throttle from 'lodash.throttle'; import isIntersectionObserverAvailable from '../utils/intersection-observer'; import getScrollElement from '../utils/get-scroll-element'; -const getScrollX = () => typeof window === 'undefined' ? - 0 : (window.scrollX || window.pageXOffset); -const getScrollY = () => typeof window === 'undefined' ? - 0 : (window.scrollY || window.pageYOffset); - -const trackWindowScroll = (BaseComponent) => { - class ScrollAwareComponent extends React.Component { - constructor(props) { - super(props); - - this.useIntersectionObserver = - props.useIntersectionObserver && isIntersectionObserverAvailable(); - if (this.useIntersectionObserver) { - return; - } - - const onChangeScroll = this.onChangeScroll.bind(this); - - if (props.delayMethod === 'debounce') { - this.delayedScroll = debounce(onChangeScroll, props.delayTime); - } else if (props.delayMethod === 'throttle') { - this.delayedScroll = throttle(onChangeScroll, props.delayTime); - } - - this.state = { - scrollPosition: { - x: getScrollX(), - y: getScrollY(), - }, - }; - - this.baseComponentRef = React.createRef(); - } - - componentDidMount() { - this.addListeners(); - } - - componentWillUnmount() { - this.removeListeners(); - } - - componentDidUpdate() { - if (typeof window === 'undefined' || this.useIntersectionObserver) { - return; - } - - const scrollElement = getScrollElement( - ReactDom.findDOMNode(this.baseComponentRef.current) - ); - - if (scrollElement !== this.scrollElement) { - this.removeListeners(); - this.addListeners(); - } - } - - addListeners() { - if (typeof window === 'undefined' || this.useIntersectionObserver) { - return; - } - - this.scrollElement = getScrollElement( - ReactDom.findDOMNode(this.baseComponentRef.current) - ); - - this.scrollElement.addEventListener( - 'scroll', - this.delayedScroll, - { passive: true } - ); - window.addEventListener( - 'resize', - this.delayedScroll, - { passive: true } - ); - - if (this.scrollElement !== window) { - window.addEventListener( - 'scroll', - this.delayedScroll, - { passive: true } - ); - } - } - - removeListeners() { - if (typeof window == 'undefined' || this.useIntersectionObserver) { - return; - } - - this.scrollElement.removeEventListener('scroll', this.delayedScroll); - window.removeEventListener('resize', this.delayedScroll); - - if (this.scrollElement !== window) { - window.removeEventListener('scroll', this.delayedScroll); - } - } - - onChangeScroll() { - if (this.useIntersectionObserver) { - return; - } - - this.setState({ - scrollPosition: { - x: getScrollX(), - y: getScrollY(), - }, - }); - } - - render() { - const { delayMethod, delayTime, ...props } = this.props; - const scrollPosition = this.useIntersectionObserver ? - null : this.state.scrollPosition; - - return ( - - ); - } - } - - ScrollAwareComponent.propTypes = { - delayMethod: PropTypes.oneOf(['debounce', 'throttle']), - delayTime: PropTypes.number, - useIntersectionObserver: PropTypes.bool, - }; - - ScrollAwareComponent.defaultProps = { - delayMethod: 'throttle', - delayTime: 300, - useIntersectionObserver: true, - }; - - return ScrollAwareComponent; +const getScrollX = () => + typeof window === 'undefined' ? 0 : window.scrollX || window.pageXOffset; +const getScrollY = () => + typeof window === 'undefined' ? 0 : window.scrollY || window.pageYOffset; + +const trackWindowScroll = BaseComponent => { + class ScrollAwareComponent extends React.Component { + constructor(props) { + super(props); + + this.useIntersectionObserver = + props.useIntersectionObserver && + isIntersectionObserverAvailable(); + if (this.useIntersectionObserver) { + return; + } + + const onChangeScroll = this.onChangeScroll.bind(this); + + if (props.delayMethod === 'debounce') { + this.delayedScroll = debounce(onChangeScroll, props.delayTime); + } else if (props.delayMethod === 'throttle') { + this.delayedScroll = throttle(onChangeScroll, props.delayTime); + } + + this.state = { + scrollPosition: { + x: getScrollX(), + y: getScrollY(), + }, + }; + + this.baseComponentRef = React.createRef(); + } + + componentDidMount() { + this.addListeners(); + } + + componentWillUnmount() { + this.removeListeners(); + } + + componentDidUpdate() { + if (typeof window === 'undefined' || this.useIntersectionObserver) { + return; + } + + const scrollElement = getScrollElement( + ReactDom.findDOMNode(this.baseComponentRef.current) + ); + + if (scrollElement !== this.scrollElement) { + this.removeListeners(); + this.addListeners(); + } + } + + addListeners() { + if (typeof window === 'undefined' || this.useIntersectionObserver) { + return; + } + + this.scrollElement = getScrollElement( + ReactDom.findDOMNode(this.baseComponentRef.current) + ); + + this.scrollElement.addEventListener('scroll', this.delayedScroll, { + passive: true, + }); + window.addEventListener('resize', this.delayedScroll, { + passive: true, + }); + + if (this.scrollElement !== window) { + window.addEventListener('scroll', this.delayedScroll, { + passive: true, + }); + } + } + + removeListeners() { + if (typeof window == 'undefined' || this.useIntersectionObserver) { + return; + } + + this.scrollElement.removeEventListener( + 'scroll', + this.delayedScroll + ); + window.removeEventListener('resize', this.delayedScroll); + + if (this.scrollElement !== window) { + window.removeEventListener('scroll', this.delayedScroll); + } + } + + onChangeScroll() { + if (this.useIntersectionObserver) { + return; + } + + this.setState({ + scrollPosition: { + x: getScrollX(), + y: getScrollY(), + }, + }); + } + + render() { + const { delayMethod, delayTime, ...props } = this.props; + const scrollPosition = this.useIntersectionObserver + ? null + : this.state.scrollPosition; + + return ( + + ); + } + } + + ScrollAwareComponent.propTypes = { + delayMethod: PropTypes.oneOf(['debounce', 'throttle']), + delayTime: PropTypes.number, + useIntersectionObserver: PropTypes.bool, + }; + + ScrollAwareComponent.defaultProps = { + delayMethod: 'throttle', + delayTime: 300, + useIntersectionObserver: true, + }; + + return ScrollAwareComponent; }; export default trackWindowScroll; diff --git a/src/utils/get-scroll-element.js b/src/utils/get-scroll-element.js index d3e7553..8f3c4c0 100644 --- a/src/utils/get-scroll-element.js +++ b/src/utils/get-scroll-element.js @@ -1,36 +1,39 @@ // copyright https://github.com/loktar00/react-lazy-load/blob/master/src/utils/parentScroll.js const style = (element, prop) => - typeof getComputedStyle === 'undefined' ? element.style[prop] : - getComputedStyle(element, null).getPropertyValue(prop); - -const overflow = (element) => - style(element, 'overflow') + - style(element, 'overflow-y') + - style(element, 'overflow-x'); - -const scrollParent = (element) => { - if (!(element instanceof HTMLElement)) { - return window; - } - - let parent = element; - - while (parent) { - if (parent === document.body || - parent === document.documentElement || - !parent.parentNode) { - break; - } - - if (/(scroll|auto)/.test(overflow(parent))) { - return parent; - } - - parent = parent.parentNode; - } - - return window; + typeof getComputedStyle === 'undefined' + ? element.style[prop] + : getComputedStyle(element, null).getPropertyValue(prop); + +const overflow = element => + style(element, 'overflow') + + style(element, 'overflow-y') + + style(element, 'overflow-x'); + +const scrollParent = element => { + if (!(element instanceof HTMLElement)) { + return window; + } + + let parent = element; + + while (parent) { + if ( + parent === document.body || + parent === document.documentElement || + !parent.parentNode + ) { + break; + } + + if (/(scroll|auto)/.test(overflow(parent))) { + return parent; + } + + parent = parent.parentNode; + } + + return window; }; export default scrollParent; diff --git a/src/utils/intersection-observer.js b/src/utils/intersection-observer.js index 9af89c7..1a06144 100644 --- a/src/utils/intersection-observer.js +++ b/src/utils/intersection-observer.js @@ -1,7 +1,7 @@ export default function() { - return ( - typeof window !== 'undefined' && - 'IntersectionObserver' in window && - 'isIntersecting' in window.IntersectionObserverEntry.prototype - ); + return ( + typeof window !== 'undefined' && + 'IntersectionObserver' in window && + 'isIntersecting' in window.IntersectionObserverEntry.prototype + ); } diff --git a/src/utils/intersection-observer.spec.js b/src/utils/intersection-observer.spec.js index fc5fd16..cdca209 100644 --- a/src/utils/intersection-observer.spec.js +++ b/src/utils/intersection-observer.spec.js @@ -1,24 +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, - }, - }; + it('returns true if IntersectionObserver is available', function() { + window.IntersectionObserver = {}; + window.IntersectionObserverEntry = { + prototype: { + isIntersecting: () => null, + }, + }; - expect(isIntersectionObserverAvailable()).toBe(true); - }); + expect(isIntersectionObserverAvailable()).toBe(true); + }); - it('returns false if IntersectionObserver is not available', function() { - delete window.IntersectionObserver; - window.IntersectionObserverEntry = { - prototype: {}, - }; - delete window.IntersectionObserverEntry; + it('returns false if IntersectionObserver is not available', function() { + delete window.IntersectionObserver; + window.IntersectionObserverEntry = { + prototype: {}, + }; + delete window.IntersectionObserverEntry; - expect(isIntersectionObserverAvailable()).toBe(false); - }); + expect(isIntersectionObserverAvailable()).toBe(false); + }); }); diff --git a/webpack.config.js b/webpack.config.js index 91a0179..8617b1e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,40 +2,42 @@ const webpack = require('webpack'); const path = require('path'); module.exports = { - mode: 'production', - entry: './src/index.js', - output: { - path: path.resolve(__dirname, 'build'), - filename: 'index.js', - libraryTarget: 'commonjs2', - }, - module: { - rules: [ - { - enforce: 'pre', - test: /\.jsx?$/, - loaders: ['eslint-loader'], - include: path.resolve(__dirname, 'src'), - exclude: /(node_modules|bower_components|build)/, - }, { - test: /\.jsx?$/, - include: path.resolve(__dirname, 'src'), - exclude: /(node_modules|bower_components|build)/, - use: { - loader: 'babel-loader', - options: { - presets: ['env'], - }, - }, - }, { - test: /\.css$/, - loaders: ['style-loader', 'css-loader'], - exclude: /node_modules/, - }, - ], - }, - externals: { - react: 'commonjs react', - 'react-dom': 'commonjs react-dom', - }, + mode: 'production', + entry: './src/index.js', + output: { + path: path.resolve(__dirname, 'build'), + filename: 'index.js', + libraryTarget: 'commonjs2', + }, + module: { + rules: [ + { + enforce: 'pre', + test: /\.jsx?$/, + loaders: ['eslint-loader'], + include: path.resolve(__dirname, 'src'), + exclude: /(node_modules|bower_components|build)/, + }, + { + test: /\.jsx?$/, + include: path.resolve(__dirname, 'src'), + exclude: /(node_modules|bower_components|build)/, + use: { + loader: 'babel-loader', + options: { + presets: ['env'], + }, + }, + }, + { + test: /\.css$/, + loaders: ['style-loader', 'css-loader'], + exclude: /node_modules/, + }, + ], + }, + externals: { + react: 'commonjs react', + 'react-dom': 'commonjs react-dom', + }, };