Skip to content

Commit ccfbd87

Browse files
authored
Use IntersectionObserver when it's available (#6)
* Use IntersectionObserver when it's available * Avoid using 'window' to store the IntersectionObserver * Add unit tests * Remove unnecessary conditional * Fix tests in Travis * Fix wrong props order
1 parent 1a638ed commit ccfbd87

8 files changed

+155
-11
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@
133133
"padded-blocks": [1, "never"],
134134
"quote-props": [1, "as-needed"],
135135
"quotes": [1, 'single'],
136-
"require-jsdoc": 1,
136+
"require-jsdoc": 0,
137137
"semi-spacing": 1,
138138
"semi": 1,
139139
"sort-keys": 0,

src/components/LazyLoadComponent.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { PropTypes } from 'prop-types';
33

44
import PlaceholderWithoutTracking from './PlaceholderWithoutTracking.jsx';
55
import PlaceholderWithTracking from './PlaceholderWithTracking.jsx';
6+
import isIntersectionObserverAvailable from '../utils/intersection-observer';
67

78
class LazyLoadComponent extends React.Component {
89
constructor(props) {
@@ -47,7 +48,7 @@ class LazyLoadComponent extends React.Component {
4748
const { className, height, placeholder, scrollPosition, style,
4849
threshold, width } = this.props;
4950

50-
if (this.isScrollTracked) {
51+
if (this.isScrollTracked || isIntersectionObserverAvailable()) {
5152
return (
5253
<PlaceholderWithoutTracking
5354
className={className}

src/components/LazyLoadComponent.spec.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import Adapter from 'enzyme-adapter-react-16';
77
import LazyLoadComponent from './LazyLoadComponent.jsx';
88
import PlaceholderWithTracking from './PlaceholderWithTracking.jsx';
99
import PlaceholderWithoutTracking from './PlaceholderWithoutTracking.jsx';
10+
import isIntersectionObserverAvailable from '../utils/intersection-observer';
11+
12+
jest.mock('../utils/intersection-observer');
1013

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

@@ -16,6 +19,16 @@ const {
1619
} = ReactTestUtils;
1720

1821
describe('LazyLoadComponent', function() {
22+
const windowIntersectionObserver = window.IntersectionObserver;
23+
24+
beforeEach(() => {
25+
isIntersectionObserverAvailable.mockImplementation(() => false);
26+
});
27+
28+
afterEach(() => {
29+
window.IntersectionObserver = windowIntersectionObserver;
30+
});
31+
1932
it('renders a PlaceholderWithTracking when scrollPosition is undefined', function() {
2033
const lazyLoadComponent = mount(
2134
<LazyLoadComponent
@@ -30,6 +43,28 @@ describe('LazyLoadComponent', function() {
3043
expect(placeholderWithTracking.length).toEqual(1);
3144
});
3245

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+
3368
it('renders a PlaceholderWithoutTracking when scrollPosition is defined', function() {
3469
const lazyLoadComponent = mount(
3570
<LazyLoadComponent
@@ -39,9 +74,12 @@ describe('LazyLoadComponent', function() {
3974
</LazyLoadComponent>
4075
);
4176

77+
const placeholderWithTracking = scryRenderedComponentsWithType(
78+
lazyLoadComponent.instance(), PlaceholderWithTracking);
4279
const placeholderWithoutTracking = scryRenderedComponentsWithType(
4380
lazyLoadComponent.instance(), PlaceholderWithoutTracking);
4481

82+
expect(placeholderWithTracking.length).toEqual(0);
4583
expect(placeholderWithoutTracking.length).toEqual(1);
4684
});
4785

src/components/PlaceholderWithoutTracking.jsx

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,57 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom';
33
import { PropTypes } from 'prop-types';
4+
import isIntersectionObserverAvailable from '../utils/intersection-observer';
45

56
class PlaceholderWithoutTracking extends React.Component {
67
constructor(props) {
78
super(props);
9+
10+
const supportsObserver = isIntersectionObserverAvailable();
11+
12+
this.LAZY_LOAD_OBSERVER = { supportsObserver };
13+
14+
if (supportsObserver) {
15+
const { threshold } = props;
16+
17+
this.LAZY_LOAD_OBSERVER.observer = new IntersectionObserver(
18+
this.checkIntersections, { rootMargin: threshold + 'px' }
19+
);
20+
}
21+
}
22+
23+
checkIntersections(entries) {
24+
entries.forEach(entry => {
25+
if (entry.isIntersecting) {
26+
entry.target.onVisible();
27+
}
28+
});
829
}
930

1031
componentDidMount() {
11-
this.updateVisibility();
32+
if (this.placeholder &&
33+
this.LAZY_LOAD_OBSERVER && this.LAZY_LOAD_OBSERVER.observer) {
34+
this.placeholder.onVisible = this.props.onVisible;
35+
this.LAZY_LOAD_OBSERVER.observer.observe(this.placeholder);
36+
}
37+
38+
if (this.LAZY_LOAD_OBSERVER &&
39+
!this.LAZY_LOAD_OBSERVER.supportsObserver) {
40+
this.updateVisibility();
41+
}
42+
}
43+
44+
componentWillUnMount() {
45+
if (this.LAZY_LOAD_OBSERVER) {
46+
this.LAZY_LOAD_OBSERVER.observer.unobserve(this.placeholder);
47+
}
1248
}
1349

1450
componentDidUpdate() {
15-
this.updateVisibility();
51+
if (this.LAZY_LOAD_OBSERVER &&
52+
!this.LAZY_LOAD_OBSERVER.supportsObserver) {
53+
this.updateVisibility();
54+
}
1655
}
1756

1857
getPlaceholderBoundingBox(scrollPosition = this.props.scrollPosition) {
@@ -77,14 +116,14 @@ class PlaceholderWithoutTracking extends React.Component {
77116

78117
PlaceholderWithoutTracking.propTypes = {
79118
onVisible: PropTypes.func.isRequired,
80-
scrollPosition: PropTypes.shape({
81-
x: PropTypes.number.isRequired,
82-
y: PropTypes.number.isRequired,
83-
}).isRequired,
84119
className: PropTypes.string,
85120
height: PropTypes.number,
86121
placeholder: PropTypes.element,
87122
threshold: PropTypes.number,
123+
scrollPosition: PropTypes.shape({
124+
x: PropTypes.number.isRequired,
125+
y: PropTypes.number.isRequired,
126+
}),
88127
width: PropTypes.number,
89128
};
90129

src/components/PlaceholderWithoutTracking.spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { configure, mount } from 'enzyme';
88
import Adapter from 'enzyme-adapter-react-16';
99

1010
import PlaceholderWithoutTracking from './PlaceholderWithoutTracking.jsx';
11+
import isIntersectionObserverAvailable from '../utils/intersection-observer';
12+
13+
jest.mock('../utils/intersection-observer');
1114

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

@@ -73,6 +76,16 @@ describe('PlaceholderWithoutTracking', function() {
7376
expect(placeholderWrapper.length).toEqual(numberOfPlaceholderWrappers);
7477
}
7578

79+
const windowIntersectionObserver = window.IntersectionObserver;
80+
81+
beforeEach(() => {
82+
isIntersectionObserverAvailable.mockImplementation(() => false);
83+
});
84+
85+
afterEach(() => {
86+
window.IntersectionObserver = windowIntersectionObserver;
87+
});
88+
7689
it('renders the default placeholder when it\'s not in the viewport', function() {
7790
const className = 'placeholder-wrapper';
7891
const component = renderPlaceholderWithoutTracking({
@@ -168,4 +181,17 @@ describe('PlaceholderWithoutTracking', function() {
168181

169182
expect(onVisible).toHaveBeenCalledTimes(1);
170183
});
184+
185+
it('doesn\'t track placeholder visibility if IntersectionObserver is available', function() {
186+
isIntersectionObserverAvailable.mockImplementation(() => true);
187+
window.IntersectionObserver = jest.fn(function() {
188+
this.observe = jest.fn(); // eslint-disable-line babel/no-invalid-this
189+
});
190+
const onVisible = jest.fn();
191+
const component = renderPlaceholderWithoutTracking({
192+
onVisible,
193+
});
194+
195+
expect(onVisible).toHaveBeenCalledTimes(0);
196+
});
171197
});

src/hoc/trackWindowScroll.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import React from 'react';
22
import { PropTypes } from 'prop-types';
33
import debounce from 'lodash.debounce';
44
import throttle from 'lodash.throttle';
5+
import isIntersectionObserverAvailable from '../utils/intersection-observer';
56

67
const trackWindowScroll = (BaseComponent) => {
78
class ScrollAwareComponent extends React.Component {
89
constructor(props) {
910
super(props);
1011

12+
if (isIntersectionObserverAvailable()) {
13+
return;
14+
}
15+
1116
const onChangeScroll = this.onChangeScroll.bind(this);
1217

1318
if (props.delayMethod === 'debounce') {
@@ -31,22 +36,25 @@ const trackWindowScroll = (BaseComponent) => {
3136
}
3237

3338
componentDidMount() {
34-
if (typeof window == 'undefined') {
39+
if (typeof window == 'undefined' || isIntersectionObserverAvailable()) {
3540
return;
3641
}
3742
window.addEventListener('scroll', this.delayedScroll);
3843
window.addEventListener('resize', this.delayedScroll);
3944
}
4045

4146
componentWillUnmount() {
42-
if (typeof window === 'undefined') {
47+
if (typeof window == 'undefined' || isIntersectionObserverAvailable()) {
4348
return;
4449
}
4550
window.removeEventListener('scroll', this.delayedScroll);
4651
window.removeEventListener('resize', this.delayedScroll);
4752
}
4853

4954
onChangeScroll() {
55+
if (isIntersectionObserverAvailable()) {
56+
return;
57+
}
5058
this.setState({
5159
scrollPosition: {
5260
x: (typeof window == 'undefined' ?
@@ -63,10 +71,12 @@ const trackWindowScroll = (BaseComponent) => {
6371

6472
render() {
6573
const { delayMethod, delayTime, ...props } = this.props;
74+
const scrollPosition = isIntersectionObserverAvailable() ?
75+
null : this.state.scrollPosition;
6676

6777
return (
6878
<BaseComponent
69-
scrollPosition={this.state.scrollPosition}
79+
scrollPosition={scrollPosition}
7080
{...props} />
7181
);
7282
}

src/utils/intersection-observer.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default function() {
2+
return (
3+
'IntersectionObserver' in window &&
4+
'isIntersecting' in window.IntersectionObserverEntry.prototype
5+
);
6+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import isIntersectionObserverAvailable from './intersection-observer';
2+
3+
describe('isIntersectionObserverAvailable', function() {
4+
it('returns true if IntersectionObserver is available', function() {
5+
window.IntersectionObserver = {};
6+
window.IntersectionObserverEntry = {
7+
prototype: {
8+
isIntersecting: () => null,
9+
},
10+
};
11+
12+
expect(isIntersectionObserverAvailable()).toBe(true);
13+
});
14+
15+
it('returns false if IntersectionObserver is not available', function() {
16+
delete window.IntersectionObserver;
17+
window.IntersectionObserverEntry = {
18+
prototype: {},
19+
};
20+
delete window.IntersectionObserverEntry;
21+
22+
expect(isIntersectionObserverAvailable()).toBe(false);
23+
});
24+
});

0 commit comments

Comments
 (0)