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',
+ },
};