Skip to content

Instantly share code, notes, and snippets.

@audunolsen
Last active July 1, 2021 10:24
Show Gist options
  • Save audunolsen/be8b9e5472c1db3d8d608610e5d6ecb7 to your computer and use it in GitHub Desktop.
Save audunolsen/be8b9e5472c1db3d8d608610e5d6ecb7 to your computer and use it in GitHub Desktop.
useIntersectionObserver hook with additional extended data regarding element visibility
import { useCallback, useEffect, useMemo, useRef } from 'react';
import visibility from './visibility';
import debounceFn from 'lodash.debounce';
import throttleFn from 'lodash.throttle';
/*
Performance tip:
Rememeber to memoize the callback! (or any other dynamic config opts)
Or else every re-render will trigger a cb dependency
update deleting and reinstantiating the observers
*/
export default (cb = () => {}, {
debounce = 0,
throttle = 0,
...config
} = {}) => {
const ref = useRef(null);
const [ handler = fn => fn, delay = 0 ] = useMemo(() => [
throttle && [throttleFn, throttle],
debounce && [debounceFn, debounce]
].filter(Boolean).flat(), [debounce, throttle]);
const intersectionObserver = useMemo(() => (
new IntersectionObserver(handler((entries, observer) => {
for (const entry of entries) entry.visibility = visibility(
entry.boundingClientRect,
{ relativeTo: entry.rootBounds }
);
cb(entries, observer);
}, delay), config)
), [cb, handler, delay]);
const setRef = useCallback(el => {
if (ref.current) intersectionObserver?.disconnect();
intersectionObserver?.observe(el);
ref.current = el;
}, []);
useEffect(() => {
if (ref.current) intersectionObserver.observe(ref.current);
return intersectionObserver?.disconnect;
}, [intersectionObserver]);
return [ref, setRef];
};
/*
type Directions {
top: boolean;
left: boolean;
bottom: boolean;
right: boolean;
horizontal: boolean;
vertical: boolean;
all: boolean;
}
type Percents {
horizontal: number;
vertical: number;
total: number;
}
type Return {
fully?: Directions
hidden?: Directions
intersects?: Directions
percents {
visible: Percents
occupied: Percents
}
}
*/
const
or = (a, b) => a || b,
and = (a, b) => a && b,
directions = ['top', 'left', 'bottom', 'right'];
export default function visibility(target/* Element | DOMRectReadOnly */, {
relativeTo /* Element | DOMRectReadOnly */
} = {}) {
let rect,
bools = {},
percents = { occupied: {}, visible: {} },
relativeRect = { width: innerWidth, height: innerHeight };
if (target instanceof DOMRectReadOnly)
rect = JSON.parse(JSON.stringify(target));
if (target instanceof Element)
rect = JSON.parse(JSON.stringify(target.getBoundingClientRect()));
if (relativeTo) {
if (relativeTo instanceof DOMRectReadOnly)
relativeRect = relativeTo;
if (relativeTo instanceof Element)
relativeRect = relativeTo.getBoundingClientRect();
rect.left = rect.left - relativeRect.left;
rect.right = rect.right - relativeRect.left;
rect.top = rect.top - relativeRect.top;
rect.bottom = rect.bottom - relativeRect.top;
}
bools.fully = {
top: rect.top >= 0,
left: rect.left >= 0,
bottom: rect.bottom <= relativeRect.height,
right: rect.right <= relativeRect.width
};
bools.hidden = {
top: rect.top + rect.height <= 0,
left: rect.left + rect.width <= 0,
bottom: rect.bottom - rect.height >= relativeRect.height,
right: rect.right - rect.width >= relativeRect.width
};
bools.intersects = Object.fromEntries(directions.map(dir => [
dir, !bools.fully[dir] && !bools.hidden[dir]
]));
const types = [
[bools.fully, and],
[bools.hidden, or],
[bools.intersects, or]
];
for (const [type, op] of types) {
type.horizontal = op(type.left, type.right);
type.vertical = op(type.top, type.bottom);
type.all = type.horizontal && type.vertical;
}
const axes = {
horizontal: ['left', 'right', 'width'],
vertical: ['top', 'bottom', 'height']
};
for (const [axis, [start, end, length]] of Object.entries(axes)) {
if (bools.hidden[axis]) percents.visible[axis] = 0;
if (bools.fully[axis]) percents.visible[axis] = 1;
const percenTypes = [
['visible', rect],
['occupied', relativeRect]
];
if (bools.intersects[start] ^ bools.intersects[end])
for (const [type, square] of percenTypes)
percents[type][axis] = bools.intersects[start]
? (rect[start] + rect[length]) / square[length]
: (relativeRect[length] - rect[start]) / square[length];
if (bools.intersects[start] && bools.intersects[end]) {
percents.occupied[axis] = 1;
percents.visible[axis] = relativeRect[length] / rect[length];
}
if (bools.fully[start] && bools.fully[end])
percents.occupied[axis] = rect[length] / relativeRect[length];
}
const
visibleWidth = rect.width * percents.visible.horizontal,
visibleHeight = rect.height * percents.visible.vertical,
visibleArea = visibleWidth * visibleHeight;
relativeArea = relativeRect.width * relativeRect.height
percents.occupied.total = visibleArea / relativeArea;
percents.visible.total = visibleArea / (rect.height * rect.width);
for (const k of Object.keys(bools))
if (Object.values(bools[k]).every(e => !e)) bools[k] = false;
return { ...bools, ...percents };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment