Skip to content

Commit

Permalink
Make components track scroll position if not set in the props
Browse files Browse the repository at this point in the history
  • Loading branch information
Aljullu committed Mar 1, 2018
1 parent 49f180f commit 9833d85
Show file tree
Hide file tree
Showing 7 changed files with 409 additions and 355 deletions.
132 changes: 14 additions & 118 deletions src/components/LazyLoadComponent.jsx
@@ -1,134 +1,30 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { PropTypes } from 'prop-types';

import LazyLoadComponentWithoutTracking
from './LazyLoadComponentWithoutTracking.jsx';
import LazyLoadComponentWithTracking
from './LazyLoadComponentWithTracking.jsx';

class LazyLoadComponent extends React.Component {
constructor(props) {
super(props);

const { afterLoad, beforeLoad, visibleByDefault } = this.props;

this.state = {
visible: visibleByDefault
};

if (visibleByDefault) {
beforeLoad();
afterLoad();
}
}

componentDidMount() {
this.updateVisibility();
}

componentDidUpdate(prevProps, prevState) {
if (prevState.visible) {
return;
}

if (this.state.visible) {
this.props.afterLoad();
}

this.updateVisibility();
}

getPlaceholderBoundingBox(scrollPosition = this.props.scrollPosition) {
const boundingRect = this.placeholder.getBoundingClientRect();
const style = ReactDOM.findDOMNode(this.placeholder).style;
const margin = {
left: parseInt(style.getPropertyValue('margin-left'), 10) || 0,
top: parseInt(style.getPropertyValue('margin-top'), 10) || 0
};

return {
bottom: scrollPosition.y + boundingRect.bottom + margin.top,
left: scrollPosition.x + boundingRect.left + margin.left,
right: scrollPosition.x + boundingRect.right + margin.left,
top: scrollPosition.y + boundingRect.top + margin.top
};
}

isPlaceholderInViewport() {
if (!this.placeholder) {
return false;
}

const { scrollPosition, threshold } = this.props;
const boundingBox = this.getPlaceholderBoundingBox(scrollPosition);
const viewport = {
bottom: scrollPosition.y + window.innerHeight,
left: scrollPosition.x,
right: scrollPosition.x + window.innerWidth,
top: scrollPosition.y
};
const { scrollPosition } = props;

return Boolean(viewport.top - threshold <= boundingBox.bottom &&
viewport.bottom + threshold >= boundingBox.top &&
viewport.left - threshold <= boundingBox.right &&
viewport.right + threshold >= boundingBox.left);
this.isScrollTracked = (scrollPosition &&
Number.isFinite(scrollPosition.x) && scrollPosition.x >= 0 &&
Number.isFinite(scrollPosition.y) && scrollPosition.y >= 0);
}

updateVisibility() {
if (this.state.visible || !this.isPlaceholderInViewport()) {
return;
}

this.props.beforeLoad();

this.setState({
visible: true
});
}

getPlaceholder() {
const { className, height, placeholder, style, width } = this.props;

if (placeholder) {
return React.cloneElement(placeholder,
{ ref: el => this.placeholder = el });
render() {
if (this.isScrollTracked) {
return <LazyLoadComponentWithoutTracking {...this.props} />;
}

return (
<span className={className}
ref={el => this.placeholder = el}
style={{ height, width, ...style }}>
</span>
);
}
const { scrollPosition, ...props } = this.props;

render() {
return this.state.visible ?
this.props.children :
this.getPlaceholder();
return <LazyLoadComponentWithTracking {...props} />;
}
}

LazyLoadComponent.propTypes = {
scrollPosition: PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired
}).isRequired,
afterLoad: PropTypes.func,
beforeLoad: PropTypes.func,
className: PropTypes.string,
height: PropTypes.number,
placeholder: PropTypes.element,
threshold: PropTypes.number,
visibleByDefault: PropTypes.bool,
width: PropTypes.number
};

LazyLoadComponent.defaultProps = {
afterLoad: () => ({}),
beforeLoad: () => ({}),
className: '',
height: 0,
placeholder: null,
threshold: 100,
visibleByDefault: false,
width: 0
};

export default LazyLoadComponent;
215 changes: 21 additions & 194 deletions src/components/LazyLoadComponent.spec.js
Expand Up @@ -4,215 +4,42 @@ import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import LazyLoadComponent from './LazyLoadComponent.jsx';
import LazyLoadComponentWithTracking
from './LazyLoadComponentWithTracking.jsx';
import LazyLoadComponentWithoutTracking
from './LazyLoadComponentWithoutTracking.jsx';

configure({ adapter: new Adapter() });

const {
scryRenderedDOMComponentsWithTag
scryRenderedComponentsWithType
} = ReactTestUtils;

describe('LazyLoadComponent', function() {
function renderLazyLoadComponent({
afterLoad = () => null,
beforeLoad = () => null,
placeholder = null,
scrollPosition = {x: 0, y: 0},
style = {},
visibleByDefault = false
} = {}) {
return mount(
<LazyLoadComponent
afterLoad={afterLoad}
beforeLoad={beforeLoad}
placeholder={placeholder}
scrollPosition={scrollPosition}
style={style}
visibleByDefault={visibleByDefault}>
<p>Lorem ipsum</p>
it('renders a LazyLoadComponentWithTracking when scrollPosition is undefined', function() {
const lazyLoadComponent = mount(
<LazyLoadComponent>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);
}

function simulateScroll(lazyLoadComponent, offsetX = 0, offsetY = 0) {
const myMock = jest.fn();

myMock.mockReturnValue({
bottom: -offsetY,
height: 0,
left: -offsetX,
right: -offsetX,
top: -offsetY,
width: 0
});

lazyLoadComponent.instance().placeholder.getBoundingClientRect = myMock;

lazyLoadComponent.setProps({
scrollPosition: {x: offsetX, y: offsetY}
});
}

function expectParagraphs(wrapper, numberOfParagraphs) {
const p = scryRenderedDOMComponentsWithTag(wrapper.instance(), 'p');

expect(p.length).toEqual(numberOfParagraphs);
}

function expectPlaceholders(wrapper, numberOfPlaceholders, placeholderTag = 'span') {
const placeholder = scryRenderedDOMComponentsWithTag(wrapper.instance(), placeholderTag);
const lazyLoadComponentWithTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(), LazyLoadComponentWithTracking);

expect(placeholder.length).toEqual(numberOfPlaceholders);
}

it('renders the default placeholder when it\'s not in the viewport', function() {
const lazyLoadComponent = renderLazyLoadComponent({
style: {marginTop: 100000}
});

expectParagraphs(lazyLoadComponent, 0);
expectPlaceholders(lazyLoadComponent, 1);
expect(lazyLoadComponentWithTracking.length).toEqual(1);
});

it('renders the prop placeholder when it\'s not in the viewport', function() {
const style = {marginTop: 100000};
const placeholder = (
<strong style={style}></strong>
it('renders a LazyLoadComponentWithoutTracking when scrollPosition is defined', function() {
const lazyLoadComponent = mount(
<LazyLoadComponent
scrollPosition={{ x: 0, y: 0}}>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);
const lazyLoadComponent = renderLazyLoadComponent({
placeholder,
style
});

expectParagraphs(lazyLoadComponent, 0);
expectPlaceholders(lazyLoadComponent, 1, 'strong');
});

it('renders the image when it\'s in the viewport', function() {
const lazyLoadComponent = renderLazyLoadComponent();

expectParagraphs(lazyLoadComponent, 1);
expectPlaceholders(lazyLoadComponent, 0);
});

it('renders the image when it appears in the viewport', function() {
const offset = 100000;
const lazyLoadComponent = renderLazyLoadComponent({
style: {marginTop: offset}
});

simulateScroll(lazyLoadComponent, 0, offset);

expectParagraphs(lazyLoadComponent, 1);
expectPlaceholders(lazyLoadComponent, 0);
});

it('renders the image when it appears in the viewport horizontally', function() {
const offset = 100000;
const lazyLoadComponent = renderLazyLoadComponent({
style: {marginLeft: offset}
});

simulateScroll(lazyLoadComponent, offset, 0);

expectParagraphs(lazyLoadComponent, 1);
expectPlaceholders(lazyLoadComponent, 0);
});

it('renders the image when it\'s not in the viewport but visibleByDefault is true', function() {
const lazyLoadComponent = renderLazyLoadComponent({
style: {marginTop: 100000},
visibleByDefault: true
});

expectParagraphs(lazyLoadComponent, 1);
expectPlaceholders(lazyLoadComponent, 0);
});

it('doesn\'t trigger beforeLoad when the image is not the viewport', function() {
const beforeLoad = jest.fn();
const lazyLoadComponent = renderLazyLoadComponent({
beforeLoad,
style: {marginTop: 100000}
});

expect(beforeLoad).toHaveBeenCalledTimes(0);
});

it('triggers beforeLoad when the image is in the viewport', function() {
const beforeLoad = jest.fn();
const lazyLoadComponent = renderLazyLoadComponent({
beforeLoad
});

expect(beforeLoad).toHaveBeenCalledTimes(1);
});

it('triggers beforeLoad when the image appears in the viewport', function() {
const beforeLoad = jest.fn();
const offset = 100000;
const lazyLoadComponent = renderLazyLoadComponent({
beforeLoad,
style: {marginTop: offset}
});

simulateScroll(lazyLoadComponent, 0, offset);

expect(beforeLoad).toHaveBeenCalledTimes(1);
});

it('triggers beforeLoad when visibleByDefault is true', function() {
const beforeLoad = jest.fn();
const offset = 100000;
const lazyLoadComponent = renderLazyLoadComponent({
beforeLoad,
style: {marginTop: offset},
visibleByDefault: true
});

expect(beforeLoad).toHaveBeenCalledTimes(1);
});

it('doesn\'t trigger afterLoad when the image is not the viewport', function() {
const afterLoad = jest.fn();
const lazyLoadComponent = renderLazyLoadComponent({
afterLoad,
style: {marginTop: 100000}
});

expect(afterLoad).toHaveBeenCalledTimes(0);
});

it('triggers afterLoad when the image is in the viewport', function() {
const afterLoad = jest.fn();
const lazyLoadComponent = renderLazyLoadComponent({
afterLoad
});

expect(afterLoad).toHaveBeenCalledTimes(1);
});

it('triggers afterLoad when the image appears in the viewport', function() {
const afterLoad = jest.fn();
const offset = 100000;
const lazyLoadComponent = renderLazyLoadComponent({
afterLoad,
style: {marginTop: offset}
});

simulateScroll(lazyLoadComponent, 0, offset);

expect(afterLoad).toHaveBeenCalledTimes(1);
});

it('triggers afterLoad when visibleByDefault is true', function() {
const afterLoad = jest.fn();
const offset = 100000;
const lazyLoadComponent = renderLazyLoadComponent({
afterLoad,
style: {marginTop: offset},
visibleByDefault: true
});
const lazyLoadComponentWithoutTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(), LazyLoadComponentWithoutTracking);

expect(afterLoad).toHaveBeenCalledTimes(1);
expect(lazyLoadComponentWithoutTracking.length).toEqual(1);
});
});

0 comments on commit 9833d85

Please sign in to comment.