Skip to content

Add useIntersectionObserver prop #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export default MyImage;
| placeholder | `ReactClass` | `<span>` | React element to use as a placeholder. |
| placeholderSrc | `String` | | Image src to display while the image is not visible or loaded. |
| threshold | `Number` | 100 | Threshold in pixels. So the image starts loading before it appears in the viewport. |
| useIntersectionObserver | `Boolean` | true | Whether to use browser's IntersectionObserver when available. |
| visibleByDefault | `Boolean` | false | Whether the image must be visible from the beginning. |
| wrapperClassName | `String` | | In some occasions (for example, when using a placeholderSrc) a wrapper span tag is rendered. This prop allows setting a class to that element. |
| ... | | | Any other image attribute |
Expand Down Expand Up @@ -141,6 +142,7 @@ export default Article;
| delayTime | `Number` | 300 | Time in ms sent to the delayMethod from lodash. |
| placeholder | `ReactClass` | `<span>` | React element to use as a placeholder. |
| threshold | `Number` | 100 | Threshold in pixels. So the component starts loading before it appears in the viewport. |
| useIntersectionObserver | `Boolean` | true | Whether to use browser's IntersectionObserver when available. |
| visibleByDefault | `Boolean` | false | Whether the component must be visible from the beginning. |


Expand Down Expand Up @@ -198,6 +200,7 @@ Component wrapped with `trackWindowScroll` (in the example, `Gallery`)
|:---|:---|:---|:---|
| delayMethod | `String` | `throttle` | Method from lodash to use to delay the scroll/resize events. It can be `throttle` or `debounce`. |
| delayTime | `Number` | 300 | Time in ms sent to the delayMethod from lodash. |
| useIntersectionObserver | `Boolean` | true | Whether to use browser's IntersectionObserver when available. |

Notice you can do the same replacing `LazyLoadImage` with `LazyLoadComponent`.

Expand Down
15 changes: 11 additions & 4 deletions src/components/LazyLoadComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class LazyLoadComponent extends React.Component {

this.onVisible = this.onVisible.bind(this);

this.isScrollTracked = (scrollPosition &&
this.isScrollTracked = Boolean(scrollPosition &&
Number.isFinite(scrollPosition.x) && scrollPosition.x >= 0 &&
Number.isFinite(scrollPosition.y) && scrollPosition.y >= 0);
}
Expand All @@ -45,10 +45,14 @@ class LazyLoadComponent extends React.Component {
return this.props.children;
}

const { className, delayMethod, delayTime, height, placeholder,
scrollPosition, style, threshold, width } = this.props;
const { className, delayMethod, delayTime, height,
placeholder, scrollPosition, style, threshold,
useIntersectionObserver, width } = this.props;

if (this.isScrollTracked || isIntersectionObserverAvailable()) {
if (
this.isScrollTracked ||
(useIntersectionObserver && isIntersectionObserverAvailable())
) {
return (
<PlaceholderWithoutTracking
className={className}
Expand All @@ -58,6 +62,7 @@ class LazyLoadComponent extends React.Component {
scrollPosition={scrollPosition}
style={style}
threshold={threshold}
useIntersectionObserver={useIntersectionObserver}
width={width} />
);
}
Expand All @@ -80,12 +85,14 @@ class LazyLoadComponent extends React.Component {
LazyLoadComponent.propTypes = {
afterLoad: PropTypes.func,
beforeLoad: PropTypes.func,
useIntersectionObserver: PropTypes.bool,
visibleByDefault: PropTypes.bool,
};

LazyLoadComponent.defaultProps = {
afterLoad: () => ({}),
beforeLoad: () => ({}),
useIntersectionObserver: true,
visibleByDefault: false,
};

Expand Down
226 changes: 135 additions & 91 deletions src/components/LazyLoadComponent.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,60 +29,6 @@ describe('LazyLoadComponent', function() {
window.IntersectionObserver = windowIntersectionObserver;
});

it('renders a PlaceholderWithTracking when scrollPosition is undefined', function() {
const lazyLoadComponent = mount(
<LazyLoadComponent
style={{ marginTop: 100000 }}>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);

const placeholderWithTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(), PlaceholderWithTracking);

expect(placeholderWithTracking.length).toEqual(1);
});

it('renders a PlaceholderWithoutTracking when scrollPosition is undefined but IntersectionObserver is available', function() {
isIntersectionObserverAvailable.mockImplementation(() => true);
window.IntersectionObserver = jest.fn(function() {
this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this
});

const lazyLoadComponent = mount(
<LazyLoadComponent
style={{ marginTop: 100000 }}>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);

const placeholderWithTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(), PlaceholderWithTracking);
const placeholderWithoutTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(), PlaceholderWithoutTracking);

expect(placeholderWithTracking.length).toEqual(0);
expect(placeholderWithoutTracking.length).toEqual(1);
});

it('renders a PlaceholderWithoutTracking when scrollPosition is defined', function() {
const lazyLoadComponent = mount(
<LazyLoadComponent
scrollPosition={{ x: 0, y: 0 }}
style={{ marginTop: 100000 }}>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);

const placeholderWithTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(), PlaceholderWithTracking);
const placeholderWithoutTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(), PlaceholderWithoutTracking);

expect(placeholderWithTracking.length).toEqual(0);
expect(placeholderWithoutTracking.length).toEqual(1);
});

it('renders children when visible', function() {
const lazyLoadComponent = mount(
<LazyLoadComponent>
Expand All @@ -98,51 +44,149 @@ describe('LazyLoadComponent', function() {
expect(paragraphs.length).toEqual(1);
});

it('triggers beforeLoad when onVisible is triggered', function() {
const beforeLoad = jest.fn();
const lazyLoadComponent = mount(
<LazyLoadComponent
beforeLoad={beforeLoad}
style={{ marginTop: 100000 }}>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);
describe('placeholders', function() {
it('renders a PlaceholderWithTracking when scrollPosition is undefined', function() {
const lazyLoadComponent = mount(
<LazyLoadComponent
style={{ marginTop: 100000 }}
>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);

const placeholderWithTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(),
PlaceholderWithTracking
);

expect(placeholderWithTracking.length).toEqual(1);
});

lazyLoadComponent.instance().onVisible();
it('renders a PlaceholderWithTracking when when IntersectionObserver is available but useIntersectionObserver is set to false', function() {
isIntersectionObserverAvailable.mockImplementation(() => true);
window.IntersectionObserver = jest.fn(function() {
this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this
});

const lazyLoadComponent = mount(
<LazyLoadComponent
useIntersectionObserver={false}
style={{ marginTop: 100000 }}
>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);

const placeholderWithTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(),
PlaceholderWithTracking
);
const placeholderWithoutTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(),
PlaceholderWithoutTracking
);

expect(placeholderWithTracking.length).toEqual(1);
});

expect(beforeLoad).toHaveBeenCalledTimes(1);
it('renders a PlaceholderWithoutTracking when scrollPosition is undefined but IntersectionObserver is available', function() {
isIntersectionObserverAvailable.mockImplementation(() => true);
window.IntersectionObserver = jest.fn(function() {
this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this
});

const lazyLoadComponent = mount(
<LazyLoadComponent
style={{ marginTop: 100000 }}
>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);

const placeholderWithTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(),
PlaceholderWithTracking
);
const placeholderWithoutTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(),
PlaceholderWithoutTracking
);

expect(placeholderWithTracking.length).toEqual(0);
expect(placeholderWithoutTracking.length).toEqual(1);
});

it('renders a PlaceholderWithoutTracking when scrollPosition is defined', function() {
const lazyLoadComponent = mount(
<LazyLoadComponent
scrollPosition={{ x: 0, y: 0 }}
style={{ marginTop: 100000 }}
>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);

const placeholderWithTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(),
PlaceholderWithTracking
);
const placeholderWithoutTracking = scryRenderedComponentsWithType(
lazyLoadComponent.instance(),
PlaceholderWithoutTracking
);

expect(placeholderWithTracking.length).toEqual(0);
expect(placeholderWithoutTracking.length).toEqual(1);
});
});

it('triggers afterLoad when onVisible is triggered', function() {
const afterLoad = jest.fn();
const lazyLoadComponent = mount(
<LazyLoadComponent
afterLoad={afterLoad}
style={{ marginTop: 100000 }}>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);
describe('beforeLoad/afterLoad', function() {
it('triggers beforeLoad when onVisible is triggered', function() {
const beforeLoad = jest.fn();
const lazyLoadComponent = mount(
<LazyLoadComponent
beforeLoad={beforeLoad}
style={{ marginTop: 100000 }}>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);

lazyLoadComponent.instance().onVisible();
lazyLoadComponent.instance().onVisible();

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

it('triggers beforeLoad and afterLoad when visibleByDefault is true', function() {
const afterLoad = jest.fn();
const beforeLoad = jest.fn();
const lazyLoadComponent = mount(
<LazyLoadComponent
afterLoad={afterLoad}
beforeLoad={beforeLoad}
style={{ marginTop: 100000 }}>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);
it('triggers afterLoad when onVisible is triggered', function() {
const afterLoad = jest.fn();
const lazyLoadComponent = mount(
<LazyLoadComponent
afterLoad={afterLoad}
style={{ marginTop: 100000 }}>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);

lazyLoadComponent.instance().onVisible();
lazyLoadComponent.instance().onVisible();

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

it('triggers beforeLoad and afterLoad when visibleByDefault is true', function() {
const afterLoad = jest.fn();
const beforeLoad = jest.fn();
const lazyLoadComponent = mount(
<LazyLoadComponent
afterLoad={afterLoad}
beforeLoad={beforeLoad}
style={{ marginTop: 100000 }}>
<p>Lorem Ipsum</p>
</LazyLoadComponent>
);

lazyLoadComponent.instance().onVisible();

expect(afterLoad).toHaveBeenCalledTimes(1);
expect(beforeLoad).toHaveBeenCalledTimes(1);
});
});
});
8 changes: 6 additions & 2 deletions src/components/LazyLoadImage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ class LazyLoadImage extends React.Component {
getImg() {
const { afterLoad, beforeLoad, delayMethod, delayTime, effect,
placeholder, placeholderSrc, scrollPosition, threshold,
visibleByDefault, wrapperClassName, ...imgProps } = this.props;
useIntersectionObserver, visibleByDefault, wrapperClassName,
...imgProps } = this.props;

return <img onLoad={this.onImageLoad()} {...imgProps} />;
}

getLazyLoadImage(image) {
const { beforeLoad, className, delayMethod, delayTime,
height, placeholder, scrollPosition, style, threshold,
visibleByDefault, width } = this.props;
useIntersectionObserver, visibleByDefault, width } = this.props;

return (
<LazyLoadComponent
Expand All @@ -50,6 +51,7 @@ class LazyLoadImage extends React.Component {
scrollPosition={scrollPosition}
style={style}
threshold={threshold}
useIntersectionObserver={useIntersectionObserver}
visibleByDefault={visibleByDefault}
width={width}>
{image}
Expand Down Expand Up @@ -107,6 +109,7 @@ LazyLoadImage.propTypes = {
effect: PropTypes.string,
placeholderSrc: PropTypes.string,
threshold: PropTypes.number,
useIntersectionObserver: PropTypes.bool,
visibleByDefault: PropTypes.bool,
wrapperClassName: PropTypes.string,
};
Expand All @@ -119,6 +122,7 @@ LazyLoadImage.defaultProps = {
effect: '',
placeholderSrc: '',
threshold: 100,
useIntersectionObserver: true,
visibleByDefault: false,
wrapperClassName: '',
};
Expand Down
5 changes: 4 additions & 1 deletion src/components/PlaceholderWithoutTracking.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ class PlaceholderWithoutTracking extends React.Component {
constructor(props) {
super(props);

const supportsObserver = isIntersectionObserverAvailable();
const supportsObserver = !props.scrollPosition &&
props.useIntersectionObserver && isIntersectionObserverAvailable();

this.LAZY_LOAD_OBSERVER = { supportsObserver };

Expand Down Expand Up @@ -120,6 +121,7 @@ PlaceholderWithoutTracking.propTypes = {
height: PropTypes.number,
placeholder: PropTypes.element,
threshold: PropTypes.number,
useIntersectionObserver: PropTypes.bool,
scrollPosition: PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
Expand All @@ -132,6 +134,7 @@ PlaceholderWithoutTracking.defaultProps = {
height: 0,
placeholder: null,
threshold: 100,
useIntersectionObserver: true,
width: 0,
};

Expand Down
Loading