Skip to content

Commit 6fe5403

Browse files
committed
Add viewport calculation use cases
1 parent 487a796 commit 6fe5403

File tree

2 files changed

+186
-2
lines changed

2 files changed

+186
-2
lines changed

src/__tests__/within-viewport-test.js

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,109 @@ describe('WithinViewport()(Component)', () => {
2525
})
2626

2727
describe('when composing with a custom transform function', () => {
28+
const Decorated = WithinViewport({ transform: (v) => ({ insideViewport: v }) })(Header)
29+
30+
beforeEach(() => { subject = mount(<Decorated title={'My title'} />) })
31+
32+
it('injects property insideViewport', () => {
33+
expect(subject.find(Header).prop('insideViewport')).not.toBeUndefined()
34+
})
2835
})
2936

30-
describe('when composing with a different default value', () => {
37+
describe('when composing with a diferent default value', () => {
38+
const Decorated = WithinViewport({ defaultAnswer: false })(Header)
39+
40+
beforeEach(() => {
41+
subject = mount(<Decorated title={'My title'} />)
42+
subject.setState({ updated: false })
43+
})
44+
45+
it('changes the default answer', () => {
46+
expect(subject.find(Header).prop('inViewport')).toBe(false)
47+
})
48+
})
49+
50+
describe('when composed is smaller than viewport', () => {
51+
describe('and inside viewport', () => {
52+
const Decorated = WithinViewport({
53+
getHeight: () => 20,
54+
getWidth: () => 20,
55+
getTop: () => 0,
56+
getLeft: () => 0,
57+
getViewportHeight: () => 200,
58+
getViewportWidth: () => 200,
59+
})(Header)
60+
beforeEach(() => { subject = mount(<Decorated title={'My title'} />) })
61+
62+
it('flags inViewport true', () => {
63+
expect(subject.find(Header).prop('inViewport')).toBe(true)
64+
})
65+
})
66+
67+
describe('and outside (top left) of viewport', () => {
68+
const Decorated = WithinViewport({
69+
getHeight: () => 20,
70+
getWidth: () => 20,
71+
getTop: () => -30,
72+
getLeft: () => -30,
73+
getViewportHeight: () => 200,
74+
getViewportWidth: () => 200,
75+
})(Header)
76+
beforeEach(() => { subject = mount(<Decorated title={'My title'} />) })
77+
78+
it('flags inViewport false', () => {
79+
expect(subject.find(Header).prop('inViewport')).toBe(false)
80+
})
81+
})
82+
83+
describe('and outside (far right) of viewport', () => {
84+
const Decorated = WithinViewport({
85+
getHeight: () => 20,
86+
getWidth: () => 20,
87+
getTop: () => 0,
88+
getLeft: () => 300,
89+
getViewportHeight: () => 200,
90+
getViewportWidth: () => 200,
91+
})(Header)
92+
beforeEach(() => { subject = mount(<Decorated title={'My title'} />) })
93+
94+
it('flags inViewport false', () => {
95+
expect(subject.find(Header).prop('inViewport')).toBe(false)
96+
})
97+
})
98+
99+
describe('and outside (far bottom) of viewport', () => {
100+
const Decorated = WithinViewport({
101+
getHeight: () => 20,
102+
getWidth: () => 20,
103+
getTop: () => 300,
104+
getLeft: () => 0,
105+
getViewportHeight: () => 200,
106+
getViewportWidth: () => 200,
107+
})(Header)
108+
beforeEach(() => { subject = mount(<Decorated title={'My title'} />) })
109+
110+
it('flags inViewport false', () => {
111+
expect(subject.find(Header).prop('inViewport')).toBe(false)
112+
})
113+
})
114+
})
115+
116+
describe('when composed is bigger than viewport', () => {
117+
describe('and inside viewport', () => {
118+
const Decorated = WithinViewport({
119+
getHeight: () => 300,
120+
getWidth: () => 300,
121+
getTop: () => -50,
122+
getLeft: () => -50,
123+
getViewportHeight: () => 200,
124+
getViewportWidth: () => 200,
125+
})(Header)
126+
beforeEach(() => { subject = mount(<Decorated title={'My title'} />) })
127+
128+
it('flags inViewport true', () => {
129+
expect(subject.find(Header).prop('inViewport')).toBe(true)
130+
})
131+
})
31132
})
32133
})

src/within-viewport.js

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { Component } from 'react'
22
import wrapDisplayName from 'recompose/wrapDisplayName'
3+
import debounce from 'lodash.debounce'
34

45
const withinViewport = ({
56
transform = ((inViewport) => ({ inViewport })),
@@ -9,17 +10,99 @@ const withinViewport = ({
910
padding: 0,
1011
border: 0,
1112
}),
13+
getHeight = ((elem) => elem.getBoundingClientRect().height),
14+
getWidth = ((elem) => elem.getBoundingClientRect().width),
15+
getTop = ((elem) => elem.getBoundingClientRect().top),
16+
getLeft = ((elem) => elem.getBoundingClientRect().left),
17+
getViewportHeight = ((_window, _document) => _window.innerHeight || _document.clientHeight),
18+
getViewportWidth = ((_window, _document) => _window.innerWidth || _document.clientWidth),
19+
defaultAnswer = true,
20+
wait = 200,
1221
} = {}) => (BaseComponent) => class extends Component {
1322
static displayName = wrapDisplayName(BaseComponent, 'withinViewport')
1423

24+
state = {
25+
containerWidth: null,
26+
containerHeight: null,
27+
containerTopOffset: null,
28+
containerLeftOffset: null,
29+
windowWidth: null,
30+
windowHeight: null,
31+
updated: false,
32+
}
33+
34+
componentDidMount() {
35+
if (window && window.addEventListener) {
36+
this.debouncedUpdate = debounce(this.updateState, wait)
37+
window.addEventListener('resize', this.debouncedUpdate)
38+
window.addEventListener('scroll', this.debouncedUpdate)
39+
this.updateState()
40+
}
41+
}
42+
43+
componentWillUnmount() {
44+
if (window && window.removeEventListener) {
45+
window.removeEventListener('resize', this.debouncedUpdate)
46+
window.removeEventListener('scroll', this.debouncedUpdate)
47+
}
48+
}
49+
50+
updateState = () => {
51+
const update = () => {
52+
const elem = this.refs.withinViewportContainer
53+
54+
this.setState({
55+
containerWidth: getWidth(elem),
56+
containerHeight: getHeight(elem),
57+
containerTopOffset: getTop(elem),
58+
containerLeftOffset: getLeft(elem),
59+
windowWidth: getViewportWidth(window, document),
60+
windowHeight: getViewportHeight(window, document),
61+
updated: true,
62+
})
63+
}
64+
65+
if (window.requestAnimationFrame) {
66+
window.requestAnimationFrame(() => update())
67+
} else {
68+
update()
69+
}
70+
}
71+
72+
inViewport = () => {
73+
if (this.state.updated) {
74+
return (
75+
(
76+
this.state.containerTopOffset < 0 &&
77+
(this.state.containerHeight + this.state.containerTopOffset) > 0
78+
) ||
79+
(
80+
this.state.containerTopOffset >= 0 &&
81+
this.state.containerTopOffset < this.state.windowHeight
82+
)
83+
) &&
84+
(
85+
(
86+
this.state.containerLeftOffset < 0 &&
87+
(this.state.containerWidth + this.state.containerLeftOffset) > 0
88+
) ||
89+
(
90+
this.state.containerLeftOffset >= 0 &&
91+
this.state.containerLeftOffset < this.state.windowWidth
92+
)
93+
)
94+
}
95+
return defaultAnswer
96+
}
97+
1598
render() {
1699
return (
17100
<div
18101
ref={'withinViewportContainer'}
19102
style={containerStyle}
20103
>
21104
<BaseComponent
22-
{...transform(true)}
105+
{...transform(this.inViewport())}
23106
{...this.props}
24107
/>
25108
</div>

0 commit comments

Comments
 (0)