Skip to content

Commit 9833d85

Browse files
committed
Make components track scroll position if not set in the props
1 parent 49f180f commit 9833d85

7 files changed

+409
-355
lines changed

src/components/LazyLoadComponent.jsx

Lines changed: 14 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,30 @@
11
import React from 'react';
2-
import ReactDOM from 'react-dom';
3-
import { PropTypes } from 'prop-types';
2+
3+
import LazyLoadComponentWithoutTracking
4+
from './LazyLoadComponentWithoutTracking.jsx';
5+
import LazyLoadComponentWithTracking
6+
from './LazyLoadComponentWithTracking.jsx';
47

58
class LazyLoadComponent extends React.Component {
69
constructor(props) {
710
super(props);
811

9-
const { afterLoad, beforeLoad, visibleByDefault } = this.props;
10-
11-
this.state = {
12-
visible: visibleByDefault
13-
};
14-
15-
if (visibleByDefault) {
16-
beforeLoad();
17-
afterLoad();
18-
}
19-
}
20-
21-
componentDidMount() {
22-
this.updateVisibility();
23-
}
24-
25-
componentDidUpdate(prevProps, prevState) {
26-
if (prevState.visible) {
27-
return;
28-
}
29-
30-
if (this.state.visible) {
31-
this.props.afterLoad();
32-
}
33-
34-
this.updateVisibility();
35-
}
36-
37-
getPlaceholderBoundingBox(scrollPosition = this.props.scrollPosition) {
38-
const boundingRect = this.placeholder.getBoundingClientRect();
39-
const style = ReactDOM.findDOMNode(this.placeholder).style;
40-
const margin = {
41-
left: parseInt(style.getPropertyValue('margin-left'), 10) || 0,
42-
top: parseInt(style.getPropertyValue('margin-top'), 10) || 0
43-
};
44-
45-
return {
46-
bottom: scrollPosition.y + boundingRect.bottom + margin.top,
47-
left: scrollPosition.x + boundingRect.left + margin.left,
48-
right: scrollPosition.x + boundingRect.right + margin.left,
49-
top: scrollPosition.y + boundingRect.top + margin.top
50-
};
51-
}
52-
53-
isPlaceholderInViewport() {
54-
if (!this.placeholder) {
55-
return false;
56-
}
57-
58-
const { scrollPosition, threshold } = this.props;
59-
const boundingBox = this.getPlaceholderBoundingBox(scrollPosition);
60-
const viewport = {
61-
bottom: scrollPosition.y + window.innerHeight,
62-
left: scrollPosition.x,
63-
right: scrollPosition.x + window.innerWidth,
64-
top: scrollPosition.y
65-
};
12+
const { scrollPosition } = props;
6613

67-
return Boolean(viewport.top - threshold <= boundingBox.bottom &&
68-
viewport.bottom + threshold >= boundingBox.top &&
69-
viewport.left - threshold <= boundingBox.right &&
70-
viewport.right + threshold >= boundingBox.left);
14+
this.isScrollTracked = (scrollPosition &&
15+
Number.isFinite(scrollPosition.x) && scrollPosition.x >= 0 &&
16+
Number.isFinite(scrollPosition.y) && scrollPosition.y >= 0);
7117
}
7218

73-
updateVisibility() {
74-
if (this.state.visible || !this.isPlaceholderInViewport()) {
75-
return;
76-
}
77-
78-
this.props.beforeLoad();
79-
80-
this.setState({
81-
visible: true
82-
});
83-
}
84-
85-
getPlaceholder() {
86-
const { className, height, placeholder, style, width } = this.props;
87-
88-
if (placeholder) {
89-
return React.cloneElement(placeholder,
90-
{ ref: el => this.placeholder = el });
19+
render() {
20+
if (this.isScrollTracked) {
21+
return <LazyLoadComponentWithoutTracking {...this.props} />;
9122
}
9223

93-
return (
94-
<span className={className}
95-
ref={el => this.placeholder = el}
96-
style={{ height, width, ...style }}>
97-
</span>
98-
);
99-
}
24+
const { scrollPosition, ...props } = this.props;
10025

101-
render() {
102-
return this.state.visible ?
103-
this.props.children :
104-
this.getPlaceholder();
26+
return <LazyLoadComponentWithTracking {...props} />;
10527
}
10628
}
10729

108-
LazyLoadComponent.propTypes = {
109-
scrollPosition: PropTypes.shape({
110-
x: PropTypes.number.isRequired,
111-
y: PropTypes.number.isRequired
112-
}).isRequired,
113-
afterLoad: PropTypes.func,
114-
beforeLoad: PropTypes.func,
115-
className: PropTypes.string,
116-
height: PropTypes.number,
117-
placeholder: PropTypes.element,
118-
threshold: PropTypes.number,
119-
visibleByDefault: PropTypes.bool,
120-
width: PropTypes.number
121-
};
122-
123-
LazyLoadComponent.defaultProps = {
124-
afterLoad: () => ({}),
125-
beforeLoad: () => ({}),
126-
className: '',
127-
height: 0,
128-
placeholder: null,
129-
threshold: 100,
130-
visibleByDefault: false,
131-
width: 0
132-
};
133-
13430
export default LazyLoadComponent;

src/components/LazyLoadComponent.spec.js

Lines changed: 21 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -4,215 +4,42 @@ import { configure, mount } from 'enzyme';
44
import Adapter from 'enzyme-adapter-react-16';
55

66
import LazyLoadComponent from './LazyLoadComponent.jsx';
7+
import LazyLoadComponentWithTracking
8+
from './LazyLoadComponentWithTracking.jsx';
9+
import LazyLoadComponentWithoutTracking
10+
from './LazyLoadComponentWithoutTracking.jsx';
711

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

1014
const {
11-
scryRenderedDOMComponentsWithTag
15+
scryRenderedComponentsWithType
1216
} = ReactTestUtils;
1317

1418
describe('LazyLoadComponent', function() {
15-
function renderLazyLoadComponent({
16-
afterLoad = () => null,
17-
beforeLoad = () => null,
18-
placeholder = null,
19-
scrollPosition = {x: 0, y: 0},
20-
style = {},
21-
visibleByDefault = false
22-
} = {}) {
23-
return mount(
24-
<LazyLoadComponent
25-
afterLoad={afterLoad}
26-
beforeLoad={beforeLoad}
27-
placeholder={placeholder}
28-
scrollPosition={scrollPosition}
29-
style={style}
30-
visibleByDefault={visibleByDefault}>
31-
<p>Lorem ipsum</p>
19+
it('renders a LazyLoadComponentWithTracking when scrollPosition is undefined', function() {
20+
const lazyLoadComponent = mount(
21+
<LazyLoadComponent>
22+
<p>Lorem Ipsum</p>
3223
</LazyLoadComponent>
3324
);
34-
}
35-
36-
function simulateScroll(lazyLoadComponent, offsetX = 0, offsetY = 0) {
37-
const myMock = jest.fn();
38-
39-
myMock.mockReturnValue({
40-
bottom: -offsetY,
41-
height: 0,
42-
left: -offsetX,
43-
right: -offsetX,
44-
top: -offsetY,
45-
width: 0
46-
});
47-
48-
lazyLoadComponent.instance().placeholder.getBoundingClientRect = myMock;
49-
50-
lazyLoadComponent.setProps({
51-
scrollPosition: {x: offsetX, y: offsetY}
52-
});
53-
}
54-
55-
function expectParagraphs(wrapper, numberOfParagraphs) {
56-
const p = scryRenderedDOMComponentsWithTag(wrapper.instance(), 'p');
57-
58-
expect(p.length).toEqual(numberOfParagraphs);
59-
}
6025

61-
function expectPlaceholders(wrapper, numberOfPlaceholders, placeholderTag = 'span') {
62-
const placeholder = scryRenderedDOMComponentsWithTag(wrapper.instance(), placeholderTag);
26+
const lazyLoadComponentWithTracking = scryRenderedComponentsWithType(
27+
lazyLoadComponent.instance(), LazyLoadComponentWithTracking);
6328

64-
expect(placeholder.length).toEqual(numberOfPlaceholders);
65-
}
66-
67-
it('renders the default placeholder when it\'s not in the viewport', function() {
68-
const lazyLoadComponent = renderLazyLoadComponent({
69-
style: {marginTop: 100000}
70-
});
71-
72-
expectParagraphs(lazyLoadComponent, 0);
73-
expectPlaceholders(lazyLoadComponent, 1);
29+
expect(lazyLoadComponentWithTracking.length).toEqual(1);
7430
});
7531

76-
it('renders the prop placeholder when it\'s not in the viewport', function() {
77-
const style = {marginTop: 100000};
78-
const placeholder = (
79-
<strong style={style}></strong>
32+
it('renders a LazyLoadComponentWithoutTracking when scrollPosition is defined', function() {
33+
const lazyLoadComponent = mount(
34+
<LazyLoadComponent
35+
scrollPosition={{ x: 0, y: 0}}>
36+
<p>Lorem Ipsum</p>
37+
</LazyLoadComponent>
8038
);
81-
const lazyLoadComponent = renderLazyLoadComponent({
82-
placeholder,
83-
style
84-
});
85-
86-
expectParagraphs(lazyLoadComponent, 0);
87-
expectPlaceholders(lazyLoadComponent, 1, 'strong');
88-
});
89-
90-
it('renders the image when it\'s in the viewport', function() {
91-
const lazyLoadComponent = renderLazyLoadComponent();
92-
93-
expectParagraphs(lazyLoadComponent, 1);
94-
expectPlaceholders(lazyLoadComponent, 0);
95-
});
96-
97-
it('renders the image when it appears in the viewport', function() {
98-
const offset = 100000;
99-
const lazyLoadComponent = renderLazyLoadComponent({
100-
style: {marginTop: offset}
101-
});
102-
103-
simulateScroll(lazyLoadComponent, 0, offset);
104-
105-
expectParagraphs(lazyLoadComponent, 1);
106-
expectPlaceholders(lazyLoadComponent, 0);
107-
});
108-
109-
it('renders the image when it appears in the viewport horizontally', function() {
110-
const offset = 100000;
111-
const lazyLoadComponent = renderLazyLoadComponent({
112-
style: {marginLeft: offset}
113-
});
114-
115-
simulateScroll(lazyLoadComponent, offset, 0);
116-
117-
expectParagraphs(lazyLoadComponent, 1);
118-
expectPlaceholders(lazyLoadComponent, 0);
119-
});
120-
121-
it('renders the image when it\'s not in the viewport but visibleByDefault is true', function() {
122-
const lazyLoadComponent = renderLazyLoadComponent({
123-
style: {marginTop: 100000},
124-
visibleByDefault: true
125-
});
126-
127-
expectParagraphs(lazyLoadComponent, 1);
128-
expectPlaceholders(lazyLoadComponent, 0);
129-
});
130-
131-
it('doesn\'t trigger beforeLoad when the image is not the viewport', function() {
132-
const beforeLoad = jest.fn();
133-
const lazyLoadComponent = renderLazyLoadComponent({
134-
beforeLoad,
135-
style: {marginTop: 100000}
136-
});
137-
138-
expect(beforeLoad).toHaveBeenCalledTimes(0);
139-
});
140-
141-
it('triggers beforeLoad when the image is in the viewport', function() {
142-
const beforeLoad = jest.fn();
143-
const lazyLoadComponent = renderLazyLoadComponent({
144-
beforeLoad
145-
});
146-
147-
expect(beforeLoad).toHaveBeenCalledTimes(1);
148-
});
149-
150-
it('triggers beforeLoad when the image appears in the viewport', function() {
151-
const beforeLoad = jest.fn();
152-
const offset = 100000;
153-
const lazyLoadComponent = renderLazyLoadComponent({
154-
beforeLoad,
155-
style: {marginTop: offset}
156-
});
157-
158-
simulateScroll(lazyLoadComponent, 0, offset);
159-
160-
expect(beforeLoad).toHaveBeenCalledTimes(1);
161-
});
162-
163-
it('triggers beforeLoad when visibleByDefault is true', function() {
164-
const beforeLoad = jest.fn();
165-
const offset = 100000;
166-
const lazyLoadComponent = renderLazyLoadComponent({
167-
beforeLoad,
168-
style: {marginTop: offset},
169-
visibleByDefault: true
170-
});
171-
172-
expect(beforeLoad).toHaveBeenCalledTimes(1);
173-
});
174-
175-
it('doesn\'t trigger afterLoad when the image is not the viewport', function() {
176-
const afterLoad = jest.fn();
177-
const lazyLoadComponent = renderLazyLoadComponent({
178-
afterLoad,
179-
style: {marginTop: 100000}
180-
});
181-
182-
expect(afterLoad).toHaveBeenCalledTimes(0);
183-
});
184-
185-
it('triggers afterLoad when the image is in the viewport', function() {
186-
const afterLoad = jest.fn();
187-
const lazyLoadComponent = renderLazyLoadComponent({
188-
afterLoad
189-
});
190-
191-
expect(afterLoad).toHaveBeenCalledTimes(1);
192-
});
193-
194-
it('triggers afterLoad when the image appears in the viewport', function() {
195-
const afterLoad = jest.fn();
196-
const offset = 100000;
197-
const lazyLoadComponent = renderLazyLoadComponent({
198-
afterLoad,
199-
style: {marginTop: offset}
200-
});
201-
202-
simulateScroll(lazyLoadComponent, 0, offset);
203-
204-
expect(afterLoad).toHaveBeenCalledTimes(1);
205-
});
20639

207-
it('triggers afterLoad when visibleByDefault is true', function() {
208-
const afterLoad = jest.fn();
209-
const offset = 100000;
210-
const lazyLoadComponent = renderLazyLoadComponent({
211-
afterLoad,
212-
style: {marginTop: offset},
213-
visibleByDefault: true
214-
});
40+
const lazyLoadComponentWithoutTracking = scryRenderedComponentsWithType(
41+
lazyLoadComponent.instance(), LazyLoadComponentWithoutTracking);
21542

216-
expect(afterLoad).toHaveBeenCalledTimes(1);
43+
expect(lazyLoadComponentWithoutTracking.length).toEqual(1);
21744
});
21845
});

0 commit comments

Comments
 (0)