Skip to content

Commit 06870ac

Browse files
committed
rewrite code into hooks
1 parent b60a440 commit 06870ac

File tree

3 files changed

+350
-381
lines changed

3 files changed

+350
-381
lines changed

lib/is-visible-with-offset.js

Lines changed: 0 additions & 35 deletions
This file was deleted.

lib/use-visibility-sensor.js

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import { useEffect, useState, useRef, useCallback } from "react";
2+
3+
function normalizeRect(rect) {
4+
if (rect.width === undefined) {
5+
rect.width = rect.right - rect.left;
6+
}
7+
8+
if (rect.height === undefined) {
9+
rect.height = rect.bottom - rect.top;
10+
}
11+
12+
return rect;
13+
}
14+
15+
function roundRectDown(rect) {
16+
return {
17+
top: Math.floor(rect.top),
18+
left: Math.floor(rect.left),
19+
bottom: Math.floor(rect.bottom),
20+
right: Math.floor(rect.right)
21+
};
22+
}
23+
24+
export default function useVisibilitySensor(
25+
nodeRef,
26+
{
27+
active = true,
28+
onChange,
29+
partialVisibility = false,
30+
minTopValue = 0,
31+
scrollCheck = false,
32+
scrollDelay = 250,
33+
scrollThrottle = -1,
34+
resizeCheck = false,
35+
resizeDelay = 250,
36+
resizeThrottle = -1,
37+
intervalCheck = true,
38+
intervalDelay = 100,
39+
delayedCall = false,
40+
offset = {},
41+
containment = null
42+
}
43+
) {
44+
const debounceCheckRef = useRef();
45+
const intervalRef = useRef();
46+
const [isVisible, setIsVisible] = useState(false);
47+
const [visibilityRect, setVisibilityRect] = useState({});
48+
49+
const getContainer = useCallback(() => containment || window, [containment]);
50+
51+
// Check if the element is within the visible viewport
52+
const visibilityCheck = useCallback(
53+
() => {
54+
const el = nodeRef && nodeRef.current;
55+
let rect;
56+
let containmentRect;
57+
58+
// if the component has rendered to null, dont update visibility
59+
if (!el) {
60+
return;
61+
}
62+
63+
rect = normalizeRect(roundRectDown(el.getBoundingClientRect()));
64+
65+
if (containment) {
66+
const containmentDOMRect = containment.getBoundingClientRect();
67+
containmentRect = {
68+
top: containmentDOMRect.top,
69+
left: containmentDOMRect.left,
70+
bottom: containmentDOMRect.bottom,
71+
right: containmentDOMRect.right
72+
};
73+
} else {
74+
containmentRect = {
75+
top: 0,
76+
left: 0,
77+
bottom: window.innerHeight || document.documentElement.clientHeight,
78+
right: window.innerWidth || document.documentElement.clientWidth
79+
};
80+
}
81+
82+
// Check if visibility is wanted via offset?
83+
const hasValidOffset = typeof offset === "object";
84+
if (hasValidOffset) {
85+
containmentRect.top += offset.top || 0;
86+
containmentRect.left += offset.left || 0;
87+
containmentRect.bottom -= offset.bottom || 0;
88+
containmentRect.right -= offset.right || 0;
89+
}
90+
91+
const nextVisibilityRect = {
92+
top: rect.top >= containmentRect.top,
93+
left: rect.left >= containmentRect.left,
94+
bottom: rect.bottom <= containmentRect.bottom,
95+
right: rect.right <= containmentRect.right
96+
};
97+
98+
// https://github.com/joshwnj/react-visibility-sensor/pull/114
99+
const hasSize = rect.height > 0 && rect.width > 0;
100+
101+
let nextIsVisible =
102+
hasSize &&
103+
nextVisibilityRect.top &&
104+
nextVisibilityRect.left &&
105+
nextVisibilityRect.bottom &&
106+
nextVisibilityRect.right;
107+
108+
// check for partial visibility
109+
if (hasSize && partialVisibility) {
110+
let partialVisible =
111+
rect.top <= containmentRect.bottom &&
112+
rect.bottom >= containmentRect.top &&
113+
rect.left <= containmentRect.right &&
114+
rect.right >= containmentRect.left;
115+
116+
// account for partial visibility on a single edge
117+
if (typeof partialVisibility === "string") {
118+
partialVisible = nextVisibilityRect[partialVisibility];
119+
}
120+
121+
// if we have minimum top visibility set by props, lets check, if it meets the passed value
122+
// so if for instance element is at least 200px in viewport, then show it.
123+
nextIsVisible = minTopValue
124+
? partialVisible && rect.top <= containmentRect.bottom - minTopValue
125+
: partialVisible;
126+
}
127+
128+
// notify the parent when the value changes
129+
if (isVisible !== nextIsVisible) {
130+
setIsVisible(nextIsVisible);
131+
setVisibilityRect(nextVisibilityRect);
132+
if (onChange) onChange(nextIsVisible);
133+
}
134+
},
135+
[
136+
isVisible,
137+
offset,
138+
containment,
139+
partialVisibility,
140+
minTopValue,
141+
onChange,
142+
setIsVisible,
143+
setVisibilityRect
144+
]
145+
);
146+
147+
const addEventListener = useCallback(
148+
(target, event, delay, throttle) => {
149+
if (!debounceCheckRef.current) {
150+
debounceCheckRef.current = {};
151+
}
152+
const debounceCheck = debounceCheckRef.current;
153+
let timeout;
154+
let func;
155+
156+
const later = () => {
157+
timeout = null;
158+
visibilityCheck();
159+
};
160+
161+
if (throttle > -1) {
162+
func = () => {
163+
if (!timeout) {
164+
timeout = setTimeout(later, throttle || 0);
165+
}
166+
};
167+
} else {
168+
func = () => {
169+
clearTimeout(timeout);
170+
timeout = setTimeout(later, delay || 0);
171+
};
172+
}
173+
174+
const info = {
175+
target: target,
176+
fn: func,
177+
getLastTimeout: () => {
178+
return timeout;
179+
}
180+
};
181+
182+
target.addEventListener(event, info.fn);
183+
debounceCheck[event] = info;
184+
185+
return () => {
186+
clearTimeout(timeout);
187+
};
188+
},
189+
[visibilityCheck]
190+
);
191+
192+
useEffect(
193+
() => {
194+
function watch() {
195+
if (debounceCheckRef.current || intervalRef.current) {
196+
return;
197+
}
198+
199+
if (intervalCheck) {
200+
intervalRef.current = setInterval(visibilityCheck, intervalDelay);
201+
}
202+
203+
if (scrollCheck) {
204+
addEventListener(
205+
getContainer(),
206+
"scroll",
207+
scrollDelay,
208+
scrollThrottle
209+
);
210+
}
211+
212+
if (resizeCheck) {
213+
addEventListener(window, "resize", resizeDelay, resizeThrottle);
214+
}
215+
216+
// if dont need delayed call, check on load ( before the first interval fires )
217+
!delayedCall && visibilityCheck();
218+
}
219+
220+
if (active) {
221+
watch();
222+
}
223+
224+
// stop any listeners and intervals on props change and re-registers
225+
return () => {
226+
if (debounceCheckRef.current) {
227+
const debounceCheck = debounceCheckRef.current;
228+
// clean up event listeners and their debounce callers
229+
for (let debounceEvent in debounceCheck) {
230+
if (debounceCheck.hasOwnProperty(debounceEvent)) {
231+
const debounceInfo = debounceCheck[debounceEvent];
232+
233+
clearTimeout(debounceInfo.getLastTimeout());
234+
debounceInfo.target.removeEventListener(
235+
debounceEvent,
236+
debounceInfo.fn
237+
);
238+
239+
debounceCheck[debounceEvent] = null;
240+
}
241+
}
242+
}
243+
debounceCheckRef.current = null;
244+
245+
if (intervalRef.current) {
246+
intervalRef.current = clearInterval(intervalRef.current);
247+
}
248+
};
249+
},
250+
[
251+
active,
252+
scrollCheck,
253+
scrollDelay,
254+
scrollThrottle,
255+
resizeCheck,
256+
resizeDelay,
257+
resizeThrottle,
258+
intervalCheck,
259+
intervalDelay,
260+
visibilityCheck
261+
]
262+
);
263+
264+
return { isVisible, visibilityRect };
265+
}

0 commit comments

Comments
 (0)