Last active
July 1, 2021 10:24
-
-
Save audunolsen/be8b9e5472c1db3d8d608610e5d6ecb7 to your computer and use it in GitHub Desktop.
useIntersectionObserver hook with additional extended data regarding element visibility
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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