Skip to content

Commit 4b0d1a7

Browse files
committed
feat: rewrite internals and tests
1 parent dbe8f56 commit 4b0d1a7

File tree

12 files changed

+675
-1043
lines changed

12 files changed

+675
-1043
lines changed

package.json

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,9 @@
126126
]
127127
},
128128
"dependencies": {
129-
"tiny-invariant": "^1.1.0"
130129
},
131130
"peerDependencies": {
132-
"react": "^15.0.0 || ^16.0.0 || ^17.0.0"
131+
"react": "^15.0.0 || ^16.0.0 || ^17.0.0|| ^17.0.0"
133132
},
134133
"devDependencies": {
135134
"@babel/cli": "^7.8.4",
@@ -172,9 +171,9 @@
172171
"lint-staged": "^10.1.3",
173172
"npm-run-all": "^4.1.5",
174173
"prettier": "^2.0.4",
175-
"react": "^17.0.0-rc.0",
176-
"react-dom": "^17.0.0-rc.0",
177-
"react-test-renderer": "^17.0.0-rc.0",
174+
"react": "^17.0.0-rc.1",
175+
"react-dom": "^17.0.0-rc.1",
176+
"react-test-renderer": "^17.0.0-rc.1",
178177
"rollup": "^2.6.0",
179178
"rollup-plugin-babel": "^4.4.0",
180179
"rollup-plugin-commonjs": "^10.0.1",
@@ -184,7 +183,7 @@
184183
"typescript": "^3.8.3"
185184
},
186185
"resolutions": {
187-
"react": "17.0.0-rc.0",
188-
"react-dom": "17.0.0-rc.0"
186+
"react": "17.0.0-rc.1",
187+
"react-dom": "17.0.0-rc.1"
189188
}
190189
}

src/InView.tsx

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as React from 'react'
2-
import invariant from 'tiny-invariant'
3-
import { observe, unobserve } from './intersection'
42
import { IntersectionObserverProps, PlainChildrenProps } from './index'
3+
import { newObserve } from './observers'
54

65
type State = {
76
inView: boolean
@@ -16,12 +15,6 @@ function isPlainChildren(
1615

1716
/**
1817
* Monitors scroll, and triggers the children function with updated props
19-
*
20-
<InView>
21-
{({inView, ref}) => (
22-
<h1 ref={ref}>{`${inView}`}</h1>
23-
)}
24-
</InView>
2518
*/
2619
export class InView extends React.Component<
2720
IntersectionObserverProps | PlainChildrenProps,
@@ -38,13 +31,6 @@ export class InView extends React.Component<
3831
entry: undefined,
3932
}
4033

41-
componentDidMount() {
42-
invariant(
43-
this.node,
44-
`react-intersection-observer: No DOM node found. Make sure you forward "ref" to the root DOM element you want to observe.`,
45-
)
46-
}
47-
4834
componentDidUpdate(prevProps: IntersectionObserverProps, prevState: State) {
4935
// If a IntersectionObserver option changed, reinit the observer
5036
if (
@@ -53,40 +39,48 @@ export class InView extends React.Component<
5339
prevProps.threshold !== this.props.threshold ||
5440
prevProps.skip !== this.props.skip
5541
) {
56-
unobserve(this.node)
42+
this.unobserve()
5743
this.observeNode()
5844
}
5945

6046
if (prevState.inView !== this.state.inView) {
6147
if (this.state.inView && this.props.triggerOnce) {
62-
unobserve(this.node)
48+
this.unobserve()
6349
this.node = null
6450
}
6551
}
6652
}
6753

6854
componentWillUnmount() {
6955
if (this.node) {
70-
unobserve(this.node)
56+
this.unobserve()
7157
this.node = null
7258
}
7359
}
7460

7561
node: Element | null = null
62+
_unobserveCb: (() => void) | null = null
7663

7764
observeNode() {
7865
if (!this.node || this.props.skip) return
7966
const { threshold, root, rootMargin } = this.props
80-
observe(this.node, this.handleChange, {
67+
this._unobserveCb = newObserve(this.node, this.handleChange, {
8168
threshold,
8269
root,
8370
rootMargin,
8471
})
8572
}
8673

74+
unobserve() {
75+
if (this._unobserveCb) {
76+
this._unobserveCb()
77+
this._unobserveCb = null
78+
}
79+
}
80+
8781
handleNode = (node?: Element | null) => {
8882
if (this.node) {
89-
unobserve(this.node)
83+
this.unobserve()
9084
if (!node && !this.props.triggerOnce && !this.props.skip) {
9185
this.setState({ inView: false, entry: undefined })
9286
}
@@ -95,7 +89,8 @@ export class InView extends React.Component<
9589
this.observeNode()
9690
}
9791

98-
handleChange = (inView: boolean, entry: IntersectionObserverEntry) => {
92+
handleChange = (entry: IntersectionObserverEntry) => {
93+
const inView = entry.isIntersecting || false
9994
// Only trigger a state update if inView has changed.
10095
// This prevents an unnecessary extra state update during mount, when the element stats outside the viewport
10196
if (inView !== this.state.inView || inView) {
@@ -122,6 +117,7 @@ export class InView extends React.Component<
122117
root,
123118
rootMargin,
124119
onChange,
120+
skip,
125121
...props
126122
} = this.props
127123

src/__tests__/InView.test.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import React from 'react'
2+
import { screen, fireEvent, render } from '@testing-library/react'
3+
import { intersectionMockInstance, mockAllIsIntersecting } from '../test-utils'
4+
import { InView } from '../InView'
5+
6+
it('Should render <InView /> intersecting', () => {
7+
const callback = jest.fn()
8+
render(
9+
<InView onChange={callback}>
10+
{({ inView, ref }) => <div ref={ref}>{inView.toString()}</div>}
11+
</InView>,
12+
)
13+
14+
mockAllIsIntersecting(false)
15+
expect(callback).toHaveBeenLastCalledWith(
16+
false,
17+
expect.objectContaining({ isIntersecting: false }),
18+
)
19+
20+
mockAllIsIntersecting(true)
21+
expect(callback).toHaveBeenLastCalledWith(
22+
true,
23+
expect.objectContaining({ isIntersecting: true }),
24+
)
25+
})
26+
27+
it('should render plain children', () => {
28+
render(<InView>inner</InView>)
29+
screen.getByText('inner')
30+
})
31+
32+
it('should render with tag', () => {
33+
const { container } = render(<InView tag="span">inner</InView>)
34+
const tagName = container.firstChild.tagName.toLowerCase()
35+
expect(tagName).toBe('span')
36+
})
37+
38+
it('should render with className', () => {
39+
const { container } = render(<InView className="inner-class">inner</InView>)
40+
expect(container.firstChild).toHaveClass('inner-class')
41+
})
42+
43+
it('Should respect skip', () => {
44+
const cb = jest.fn()
45+
render(<InView skip onChange={cb}></InView>)
46+
mockAllIsIntersecting()
47+
48+
expect(cb).not.toHaveBeenCalled()
49+
})
50+
it('Should unobserve old node', () => {
51+
const { rerender, container } = render(
52+
<InView>
53+
{({ inView, ref }) => (
54+
<div key="1" ref={ref}>
55+
Inview: {inView.toString()}
56+
</div>
57+
)}
58+
</InView>,
59+
)
60+
rerender(
61+
<InView>
62+
{({ inView, ref }) => (
63+
<div key="2" ref={ref}>
64+
Inview: {inView.toString()}
65+
</div>
66+
)}
67+
</InView>,
68+
)
69+
mockAllIsIntersecting(true)
70+
})
71+
72+
it('Should ensure node exists before observing and unobserving', () => {
73+
const { unmount } = render(<InView>{() => null}</InView>)
74+
unmount()
75+
})
76+
77+
it('Should recreate observer when threshold change', () => {
78+
const { container, rerender } = render(<InView>Inner</InView>)
79+
mockAllIsIntersecting(true)
80+
const instance = intersectionMockInstance(container.firstChild)
81+
jest.spyOn(instance, 'unobserve')
82+
83+
rerender(<InView threshold={0.5}>Inner</InView>)
84+
expect(instance.unobserve).toHaveBeenCalled()
85+
})
86+
87+
it('Should recreate observer when root change', () => {
88+
const { container, rerender } = render(<InView>Inner</InView>)
89+
mockAllIsIntersecting(true)
90+
const instance = intersectionMockInstance(container.firstChild)
91+
jest.spyOn(instance, 'unobserve')
92+
93+
rerender(<InView root={{}}>Inner</InView>)
94+
expect(instance.unobserve).toHaveBeenCalled()
95+
})
96+
97+
it('Should recreate observer when rootMargin change', () => {
98+
const { container, rerender } = render(<InView>Inner</InView>)
99+
mockAllIsIntersecting(true)
100+
const instance = intersectionMockInstance(container.firstChild)
101+
jest.spyOn(instance, 'unobserve')
102+
103+
rerender(<InView rootMargin="10px">Inner</InView>)
104+
expect(instance.unobserve).toHaveBeenCalled()
105+
})
106+
107+
it('Should unobserve when triggerOnce comes into view', () => {
108+
const { container, rerender } = render(<InView triggerOnce>Inner</InView>)
109+
mockAllIsIntersecting(false)
110+
const instance = intersectionMockInstance(container.firstChild)
111+
jest.spyOn(instance, 'unobserve')
112+
mockAllIsIntersecting(true)
113+
114+
expect(instance.unobserve).toHaveBeenCalled()
115+
})
116+
117+
it('Should unobserve when unmounted', () => {
118+
const { container, unmount } = render(<InView triggerOnce>Inner</InView>)
119+
const instance = intersectionMockInstance(container.firstChild)
120+
jest.spyOn(instance, 'unobserve')
121+
122+
unmount()
123+
124+
expect(instance.unobserve).toHaveBeenCalled()
125+
})
126+
127+
it('plain children should not catch bubbling onChange event', () => {
128+
const onChange = jest.fn()
129+
const { getByLabelText } = render(
130+
<InView onChange={onChange}>
131+
<label>
132+
<input name="field" />
133+
input
134+
</label>
135+
</InView>,
136+
)
137+
const input = getByLabelText('input')
138+
fireEvent.change(input, { target: { value: 'changed value' } })
139+
expect(onChange).not.toHaveBeenCalled()
140+
})

0 commit comments

Comments
 (0)