Skip to content

Commit 6644e8a

Browse files
authored
feat: view port observer on lost visibility (#17417)
* refactor: improve types * feat: add optional isIntersecting param * feat: add not visible handler * refactor: improve naming * runfix: visibility lost on unmount * docs: add unmount comment
1 parent 6878886 commit 6644e8a

File tree

2 files changed

+39
-12
lines changed

2 files changed

+39
-12
lines changed

src/script/components/utils/InViewport.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {viewportObserver} from 'Util/DOM/viewportObserver';
2424

2525
interface InViewportParams {
2626
onVisible: () => void;
27+
onVisibilityLost?: () => void;
2728
requireFullyInView?: boolean;
2829
allowBiggerThanViewport?: boolean;
2930
/** Will check if the element is overlayed by something else. Can be used to be sure the user could actually see the element. Should not be used to do lazy loading as the overlayObserver has quite a long debounce time */
@@ -33,6 +34,7 @@ interface InViewportParams {
3334
const InViewport: React.FC<InViewportParams & React.HTMLProps<HTMLDivElement>> = ({
3435
children,
3536
onVisible,
37+
onVisibilityLost,
3638
requireFullyInView = false,
3739
checkOverlay = false,
3840
allowBiggerThanViewport = false,
@@ -58,15 +60,23 @@ const InViewport: React.FC<InViewportParams & React.HTMLProps<HTMLDivElement>> =
5860
const triggerCallbackIfVisible = () => {
5961
if (inViewport && visible) {
6062
onVisible();
61-
releaseTrackers();
63+
64+
if (!onVisibilityLost) {
65+
releaseTrackers();
66+
}
6267
}
6368
};
6469

6570
viewportObserver.trackElement(
6671
element,
67-
(isInViewport: boolean) => {
72+
(isInViewport: boolean, isPartiallyVisible: boolean) => {
6873
inViewport = isInViewport;
6974
triggerCallbackIfVisible();
75+
76+
// If the element is not intersecting at all, we can trigger the onVisibilityLost callback
77+
if (!isPartiallyVisible) {
78+
onVisibilityLost?.();
79+
}
7080
},
7181
requireFullyInView,
7282
allowBiggerThanViewport,
@@ -77,8 +87,12 @@ const InViewport: React.FC<InViewportParams & React.HTMLProps<HTMLDivElement>> =
7787
triggerCallbackIfVisible();
7888
});
7989
}
80-
return () => releaseTrackers();
81-
}, [allowBiggerThanViewport, requireFullyInView, checkOverlay, onVisible]);
90+
return () => {
91+
// If the element is unmounted, we can trigger the onVisibilityLost callback and release the trackers
92+
onVisibilityLost?.();
93+
releaseTrackers();
94+
};
95+
}, [allowBiggerThanViewport, requireFullyInView, checkOverlay, onVisible, onVisibilityLost]);
8296

8397
return (
8498
<div ref={domNode} {...props} css={{minHeight: '1px'}}>

src/script/util/DOM/viewportObserver.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,21 @@
1717
*
1818
*/
1919

20-
const observedElements = new Map();
20+
const observedElements = new Map<
21+
Element,
22+
{
23+
allowBiggerThanViewport?: boolean;
24+
requireFullyInView?: boolean;
25+
onVisible?: Function;
26+
onVisibilityChange?: (isVisible: boolean, isPartiallyVisible: boolean) => void;
27+
}
28+
>();
2129
const tolerance = 0.8;
2230

2331
const onIntersect: IntersectionObserverCallback = entries => {
2432
entries.forEach(({intersectionRatio, isIntersecting, target: element, rootBounds}) => {
25-
const {onVisible, onChange, requireFullyInView, allowBiggerThanViewport} = observedElements.get(element) || {};
33+
const {onVisible, onVisibilityChange, requireFullyInView, allowBiggerThanViewport} =
34+
observedElements.get(element) || {};
2635
const isFullyInView = intersectionRatio >= tolerance;
2736

2837
const isBiggerThanRoot = () => {
@@ -35,11 +44,11 @@ const onIntersect: IntersectionObserverCallback = entries => {
3544

3645
const isVisible = isIntersecting && (!requireFullyInView || isFullyInView || isBiggerThanRoot());
3746

38-
if (onChange) {
39-
onChange(isVisible);
47+
if (onVisibilityChange) {
48+
onVisibilityChange(!!isVisible, isIntersecting);
4049
} else if (isVisible) {
4150
removeElement(element);
42-
return onVisible && onVisible();
51+
return onVisible?.();
4352
}
4453
});
4554
};
@@ -72,18 +81,22 @@ const onElementInViewport = (
7281
* Will track an element and trigger the callback whenever the intersecting state changes
7382
*
7483
* @param element the element to observe
75-
* @param onChange the callback to call when the element intersects or not
84+
* @param onVisibilityChange the callback to call when the element intersects or not
7685
* @param requireFullyInView should the element be fully in view
7786
* @param allowBiggerThanViewport should fire when element is bigger than viewport
7887
*/
7988
const trackElement = (
8089
element: HTMLElement,
81-
onChange: Function,
90+
onVisibilityChange: (isVisible: boolean, isPartiallyVisible: boolean) => void,
8291
requireFullyInView = false,
8392
allowBiggerThanViewport = false,
8493
): void => {
8594
if (element) {
86-
observedElements.set(element, {allowBiggerThanViewport, onChange, requireFullyInView});
95+
observedElements.set(element, {
96+
allowBiggerThanViewport,
97+
onVisibilityChange,
98+
requireFullyInView,
99+
});
87100
return observer.observe(element);
88101
}
89102
};

0 commit comments

Comments
 (0)