Skip to content

Commit 97f0039

Browse files
authored
Add useIntersectionObserver prop (#45)
* Add useIntersectionObserver prop * Update tests
1 parent e1e3eb3 commit 97f0039

7 files changed

+185
-104
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export default MyImage;
7373
| placeholder | `ReactClass` | `<span>` | React element to use as a placeholder. |
7474
| placeholderSrc | `String` | | Image src to display while the image is not visible or loaded. |
7575
| threshold | `Number` | 100 | Threshold in pixels. So the image starts loading before it appears in the viewport. |
76+
| useIntersectionObserver | `Boolean` | true | Whether to use browser's IntersectionObserver when available. |
7677
| visibleByDefault | `Boolean` | false | Whether the image must be visible from the beginning. |
7778
| 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. |
7879
| ... | | | Any other image attribute |
@@ -141,6 +142,7 @@ export default Article;
141142
| delayTime | `Number` | 300 | Time in ms sent to the delayMethod from lodash. |
142143
| placeholder | `ReactClass` | `<span>` | React element to use as a placeholder. |
143144
| threshold | `Number` | 100 | Threshold in pixels. So the component starts loading before it appears in the viewport. |
145+
| useIntersectionObserver | `Boolean` | true | Whether to use browser's IntersectionObserver when available. |
144146
| visibleByDefault | `Boolean` | false | Whether the component must be visible from the beginning. |
145147

146148

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

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

src/components/LazyLoadComponent.jsx

+11-4
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class LazyLoadComponent extends React.Component {
2222

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

25-
this.isScrollTracked = (scrollPosition &&
25+
this.isScrollTracked = Boolean(scrollPosition &&
2626
Number.isFinite(scrollPosition.x) && scrollPosition.x >= 0 &&
2727
Number.isFinite(scrollPosition.y) && scrollPosition.y >= 0);
2828
}
@@ -45,10 +45,14 @@ class LazyLoadComponent extends React.Component {
4545
return this.props.children;
4646
}
4747

48-
const { className, delayMethod, delayTime, height, placeholder,
49-
scrollPosition, style, threshold, width } = this.props;
48+
const { className, delayMethod, delayTime, height,
49+
placeholder, scrollPosition, style, threshold,
50+
useIntersectionObserver, width } = this.props;
5051

51-
if (this.isScrollTracked || isIntersectionObserverAvailable()) {
52+
if (
53+
this.isScrollTracked ||
54+
(useIntersectionObserver && isIntersectionObserverAvailable())
55+
) {
5256
return (
5357
<PlaceholderWithoutTracking
5458
className={className}
@@ -58,6 +62,7 @@ class LazyLoadComponent extends React.Component {
5862
scrollPosition={scrollPosition}
5963
style={style}
6064
threshold={threshold}
65+
useIntersectionObserver={useIntersectionObserver}
6166
width={width} />
6267
);
6368
}
@@ -80,12 +85,14 @@ class LazyLoadComponent extends React.Component {
8085
LazyLoadComponent.propTypes = {
8186
afterLoad: PropTypes.func,
8287
beforeLoad: PropTypes.func,
88+
useIntersectionObserver: PropTypes.bool,
8389
visibleByDefault: PropTypes.bool,
8490
};
8591

8692
LazyLoadComponent.defaultProps = {
8793
afterLoad: () => ({}),
8894
beforeLoad: () => ({}),
95+
useIntersectionObserver: true,
8996
visibleByDefault: false,
9097
};
9198

src/components/LazyLoadComponent.spec.js

+135-91
Original file line numberDiff line numberDiff line change
@@ -29,60 +29,6 @@ describe('LazyLoadComponent', function() {
2929
window.IntersectionObserver = windowIntersectionObserver;
3030
});
3131

32-
it('renders a PlaceholderWithTracking when scrollPosition is undefined', function() {
33-
const lazyLoadComponent = mount(
34-
<LazyLoadComponent
35-
style={{ marginTop: 100000 }}>
36-
<p>Lorem Ipsum</p>
37-
</LazyLoadComponent>
38-
);
39-
40-
const placeholderWithTracking = scryRenderedComponentsWithType(
41-
lazyLoadComponent.instance(), PlaceholderWithTracking);
42-
43-
expect(placeholderWithTracking.length).toEqual(1);
44-
});
45-
46-
it('renders a PlaceholderWithoutTracking when scrollPosition is undefined but IntersectionObserver is available', function() {
47-
isIntersectionObserverAvailable.mockImplementation(() => true);
48-
window.IntersectionObserver = jest.fn(function() {
49-
this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this
50-
});
51-
52-
const lazyLoadComponent = mount(
53-
<LazyLoadComponent
54-
style={{ marginTop: 100000 }}>
55-
<p>Lorem Ipsum</p>
56-
</LazyLoadComponent>
57-
);
58-
59-
const placeholderWithTracking = scryRenderedComponentsWithType(
60-
lazyLoadComponent.instance(), PlaceholderWithTracking);
61-
const placeholderWithoutTracking = scryRenderedComponentsWithType(
62-
lazyLoadComponent.instance(), PlaceholderWithoutTracking);
63-
64-
expect(placeholderWithTracking.length).toEqual(0);
65-
expect(placeholderWithoutTracking.length).toEqual(1);
66-
});
67-
68-
it('renders a PlaceholderWithoutTracking when scrollPosition is defined', function() {
69-
const lazyLoadComponent = mount(
70-
<LazyLoadComponent
71-
scrollPosition={{ x: 0, y: 0 }}
72-
style={{ marginTop: 100000 }}>
73-
<p>Lorem Ipsum</p>
74-
</LazyLoadComponent>
75-
);
76-
77-
const placeholderWithTracking = scryRenderedComponentsWithType(
78-
lazyLoadComponent.instance(), PlaceholderWithTracking);
79-
const placeholderWithoutTracking = scryRenderedComponentsWithType(
80-
lazyLoadComponent.instance(), PlaceholderWithoutTracking);
81-
82-
expect(placeholderWithTracking.length).toEqual(0);
83-
expect(placeholderWithoutTracking.length).toEqual(1);
84-
});
85-
8632
it('renders children when visible', function() {
8733
const lazyLoadComponent = mount(
8834
<LazyLoadComponent>
@@ -98,51 +44,149 @@ describe('LazyLoadComponent', function() {
9844
expect(paragraphs.length).toEqual(1);
9945
});
10046

101-
it('triggers beforeLoad when onVisible is triggered', function() {
102-
const beforeLoad = jest.fn();
103-
const lazyLoadComponent = mount(
104-
<LazyLoadComponent
105-
beforeLoad={beforeLoad}
106-
style={{ marginTop: 100000 }}>
107-
<p>Lorem Ipsum</p>
108-
</LazyLoadComponent>
109-
);
47+
describe('placeholders', function() {
48+
it('renders a PlaceholderWithTracking when scrollPosition is undefined', function() {
49+
const lazyLoadComponent = mount(
50+
<LazyLoadComponent
51+
style={{ marginTop: 100000 }}
52+
>
53+
<p>Lorem Ipsum</p>
54+
</LazyLoadComponent>
55+
);
56+
57+
const placeholderWithTracking = scryRenderedComponentsWithType(
58+
lazyLoadComponent.instance(),
59+
PlaceholderWithTracking
60+
);
61+
62+
expect(placeholderWithTracking.length).toEqual(1);
63+
});
11064

111-
lazyLoadComponent.instance().onVisible();
65+
it('renders a PlaceholderWithTracking when when IntersectionObserver is available but useIntersectionObserver is set to false', function() {
66+
isIntersectionObserverAvailable.mockImplementation(() => true);
67+
window.IntersectionObserver = jest.fn(function() {
68+
this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this
69+
});
70+
71+
const lazyLoadComponent = mount(
72+
<LazyLoadComponent
73+
useIntersectionObserver={false}
74+
style={{ marginTop: 100000 }}
75+
>
76+
<p>Lorem Ipsum</p>
77+
</LazyLoadComponent>
78+
);
79+
80+
const placeholderWithTracking = scryRenderedComponentsWithType(
81+
lazyLoadComponent.instance(),
82+
PlaceholderWithTracking
83+
);
84+
const placeholderWithoutTracking = scryRenderedComponentsWithType(
85+
lazyLoadComponent.instance(),
86+
PlaceholderWithoutTracking
87+
);
88+
89+
expect(placeholderWithTracking.length).toEqual(1);
90+
});
11291

113-
expect(beforeLoad).toHaveBeenCalledTimes(1);
92+
it('renders a PlaceholderWithoutTracking when scrollPosition is undefined but IntersectionObserver is available', function() {
93+
isIntersectionObserverAvailable.mockImplementation(() => true);
94+
window.IntersectionObserver = jest.fn(function() {
95+
this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this
96+
});
97+
98+
const lazyLoadComponent = mount(
99+
<LazyLoadComponent
100+
style={{ marginTop: 100000 }}
101+
>
102+
<p>Lorem Ipsum</p>
103+
</LazyLoadComponent>
104+
);
105+
106+
const placeholderWithTracking = scryRenderedComponentsWithType(
107+
lazyLoadComponent.instance(),
108+
PlaceholderWithTracking
109+
);
110+
const placeholderWithoutTracking = scryRenderedComponentsWithType(
111+
lazyLoadComponent.instance(),
112+
PlaceholderWithoutTracking
113+
);
114+
115+
expect(placeholderWithTracking.length).toEqual(0);
116+
expect(placeholderWithoutTracking.length).toEqual(1);
117+
});
118+
119+
it('renders a PlaceholderWithoutTracking when scrollPosition is defined', function() {
120+
const lazyLoadComponent = mount(
121+
<LazyLoadComponent
122+
scrollPosition={{ x: 0, y: 0 }}
123+
style={{ marginTop: 100000 }}
124+
>
125+
<p>Lorem Ipsum</p>
126+
</LazyLoadComponent>
127+
);
128+
129+
const placeholderWithTracking = scryRenderedComponentsWithType(
130+
lazyLoadComponent.instance(),
131+
PlaceholderWithTracking
132+
);
133+
const placeholderWithoutTracking = scryRenderedComponentsWithType(
134+
lazyLoadComponent.instance(),
135+
PlaceholderWithoutTracking
136+
);
137+
138+
expect(placeholderWithTracking.length).toEqual(0);
139+
expect(placeholderWithoutTracking.length).toEqual(1);
140+
});
114141
});
115142

116-
it('triggers afterLoad when onVisible is triggered', function() {
117-
const afterLoad = jest.fn();
118-
const lazyLoadComponent = mount(
119-
<LazyLoadComponent
120-
afterLoad={afterLoad}
121-
style={{ marginTop: 100000 }}>
122-
<p>Lorem Ipsum</p>
123-
</LazyLoadComponent>
124-
);
143+
describe('beforeLoad/afterLoad', function() {
144+
it('triggers beforeLoad when onVisible is triggered', function() {
145+
const beforeLoad = jest.fn();
146+
const lazyLoadComponent = mount(
147+
<LazyLoadComponent
148+
beforeLoad={beforeLoad}
149+
style={{ marginTop: 100000 }}>
150+
<p>Lorem Ipsum</p>
151+
</LazyLoadComponent>
152+
);
125153

126-
lazyLoadComponent.instance().onVisible();
154+
lazyLoadComponent.instance().onVisible();
127155

128-
expect(afterLoad).toHaveBeenCalledTimes(1);
129-
});
156+
expect(beforeLoad).toHaveBeenCalledTimes(1);
157+
});
130158

131-
it('triggers beforeLoad and afterLoad when visibleByDefault is true', function() {
132-
const afterLoad = jest.fn();
133-
const beforeLoad = jest.fn();
134-
const lazyLoadComponent = mount(
135-
<LazyLoadComponent
136-
afterLoad={afterLoad}
137-
beforeLoad={beforeLoad}
138-
style={{ marginTop: 100000 }}>
139-
<p>Lorem Ipsum</p>
140-
</LazyLoadComponent>
141-
);
159+
it('triggers afterLoad when onVisible is triggered', function() {
160+
const afterLoad = jest.fn();
161+
const lazyLoadComponent = mount(
162+
<LazyLoadComponent
163+
afterLoad={afterLoad}
164+
style={{ marginTop: 100000 }}>
165+
<p>Lorem Ipsum</p>
166+
</LazyLoadComponent>
167+
);
142168

143-
lazyLoadComponent.instance().onVisible();
169+
lazyLoadComponent.instance().onVisible();
144170

145-
expect(afterLoad).toHaveBeenCalledTimes(1);
146-
expect(beforeLoad).toHaveBeenCalledTimes(1);
171+
expect(afterLoad).toHaveBeenCalledTimes(1);
172+
});
173+
174+
it('triggers beforeLoad and afterLoad when visibleByDefault is true', function() {
175+
const afterLoad = jest.fn();
176+
const beforeLoad = jest.fn();
177+
const lazyLoadComponent = mount(
178+
<LazyLoadComponent
179+
afterLoad={afterLoad}
180+
beforeLoad={beforeLoad}
181+
style={{ marginTop: 100000 }}>
182+
<p>Lorem Ipsum</p>
183+
</LazyLoadComponent>
184+
);
185+
186+
lazyLoadComponent.instance().onVisible();
187+
188+
expect(afterLoad).toHaveBeenCalledTimes(1);
189+
expect(beforeLoad).toHaveBeenCalledTimes(1);
190+
});
147191
});
148192
});

src/components/LazyLoadImage.jsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,16 @@ class LazyLoadImage extends React.Component {
2929
getImg() {
3030
const { afterLoad, beforeLoad, delayMethod, delayTime, effect,
3131
placeholder, placeholderSrc, scrollPosition, threshold,
32-
visibleByDefault, wrapperClassName, ...imgProps } = this.props;
32+
useIntersectionObserver, visibleByDefault, wrapperClassName,
33+
...imgProps } = this.props;
3334

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

3738
getLazyLoadImage(image) {
3839
const { beforeLoad, className, delayMethod, delayTime,
3940
height, placeholder, scrollPosition, style, threshold,
40-
visibleByDefault, width } = this.props;
41+
useIntersectionObserver, visibleByDefault, width } = this.props;
4142

4243
return (
4344
<LazyLoadComponent
@@ -50,6 +51,7 @@ class LazyLoadImage extends React.Component {
5051
scrollPosition={scrollPosition}
5152
style={style}
5253
threshold={threshold}
54+
useIntersectionObserver={useIntersectionObserver}
5355
visibleByDefault={visibleByDefault}
5456
width={width}>
5557
{image}
@@ -107,6 +109,7 @@ LazyLoadImage.propTypes = {
107109
effect: PropTypes.string,
108110
placeholderSrc: PropTypes.string,
109111
threshold: PropTypes.number,
112+
useIntersectionObserver: PropTypes.bool,
110113
visibleByDefault: PropTypes.bool,
111114
wrapperClassName: PropTypes.string,
112115
};
@@ -119,6 +122,7 @@ LazyLoadImage.defaultProps = {
119122
effect: '',
120123
placeholderSrc: '',
121124
threshold: 100,
125+
useIntersectionObserver: true,
122126
visibleByDefault: false,
123127
wrapperClassName: '',
124128
};

src/components/PlaceholderWithoutTracking.jsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ class PlaceholderWithoutTracking extends React.Component {
77
constructor(props) {
88
super(props);
99

10-
const supportsObserver = isIntersectionObserverAvailable();
10+
const supportsObserver = !props.scrollPosition &&
11+
props.useIntersectionObserver && isIntersectionObserverAvailable();
1112

1213
this.LAZY_LOAD_OBSERVER = { supportsObserver };
1314

@@ -120,6 +121,7 @@ PlaceholderWithoutTracking.propTypes = {
120121
height: PropTypes.number,
121122
placeholder: PropTypes.element,
122123
threshold: PropTypes.number,
124+
useIntersectionObserver: PropTypes.bool,
123125
scrollPosition: PropTypes.shape({
124126
x: PropTypes.number.isRequired,
125127
y: PropTypes.number.isRequired,
@@ -132,6 +134,7 @@ PlaceholderWithoutTracking.defaultProps = {
132134
height: 0,
133135
placeholder: null,
134136
threshold: 100,
137+
useIntersectionObserver: true,
135138
width: 0,
136139
};
137140

0 commit comments

Comments
 (0)