@elv1n/react-use-measure
Version:
measure view bounds
167 lines (138 loc) • 5.42 kB
JavaScript
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { debounce } from 'debounce';
function useMeasure({
debounce: debounce$1,
scroll,
polyfill
} = {
debounce: 0,
scroll: false
}) {
const ResizeObserver = polyfill || (typeof window === 'undefined' ? class ResizeObserver {} : window.ResizeObserver);
if (!ResizeObserver) {
throw new Error('This browser does not support ResizeObserver out of the box. See: https://github.com/react-spring/react-use-measure/#resize-observer-polyfills');
}
const [bounds, set] = useState({
left: 0,
top: 0,
width: 0,
height: 0,
bottom: 0,
right: 0,
x: 0,
y: 0
}); // keep all state in a ref
const state = useRef({
element: null,
scrollContainers: null,
resizeObserver: null,
lastBounds: bounds
}); // set actual debounce values early, so effects know if they should react accordingly
const scrollDebounce = debounce$1 ? typeof debounce$1 === 'number' ? debounce$1 : debounce$1.scroll : null;
const resizeDebounce = debounce$1 ? typeof debounce$1 === 'number' ? debounce$1 : debounce$1.resize : null; // make sure to update state only as long as the component is truly mounted
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => void (mounted.current = false);
}); // memoize handlers, so event-listeners know when they should update
const [forceRefresh, resizeChange, scrollChange] = useMemo(() => {
const callback = () => {
if (!state.current.element) return;
const {
left,
top,
width,
height,
bottom,
right,
x,
y
} = state.current.element.getBoundingClientRect();
const size = {
left,
top,
width,
height,
bottom,
right,
x,
y
};
Object.freeze(size);
if (mounted.current && !areBoundsEqual(state.current.lastBounds, size)) set(state.current.lastBounds = size);
};
return [callback, resizeDebounce ? debounce(callback, resizeDebounce) : callback, scrollDebounce ? debounce(callback, scrollDebounce) : callback];
}, [set, scrollDebounce, resizeDebounce]); // the ref we expose to the user
const ref = useCallback(node => {
if (!node || node === state.current.element) return;
removeListeners(state, scrollChange);
state.current.element = node;
state.current.scrollContainers = findScrollContainers(node);
addListeners(state, scrollChange, scroll);
}, [scroll, scrollChange]); // add general event listeners
useOnWindowScroll(scrollChange, Boolean(scroll));
useOnWindowResize(resizeChange); // respond to changes that are relevant for the listeners
useEffect(() => {
removeListeners(state, scrollChange);
addListeners(state, scrollChange, scroll);
}, [scroll, scrollChange, resizeChange]); // remove all listeners when the components unmounts
useEffect(() => () => removeListeners(state, scrollChange), []);
return [ref, bounds, forceRefresh];
} // Adds native resize listener to window
function useOnWindowResize(onWindowResize) {
useEffect(() => {
const cb = onWindowResize;
window.addEventListener('resize', cb);
return () => void window.removeEventListener('resize', cb);
}, [onWindowResize]);
}
function useOnWindowScroll(onScroll, enabled) {
useEffect(() => {
if (enabled) {
const cb = onScroll;
window.addEventListener('scroll', cb, {
capture: true,
passive: true
});
return () => void window.removeEventListener('scroll', cb, true);
}
}, [onScroll, enabled]);
} // Returns a list of scroll offsets
function findScrollContainers(element) {
const result = [];
if (!element || element === document.body) return result;
const {
overflow,
overflowX,
overflowY
} = window.getComputedStyle(element);
if ([overflow, overflowX, overflowY].some(prop => prop === 'auto' || prop === 'scroll')) result.push(element);
return [...result, ...findScrollContainers(element.parentElement)];
} // cleanup current scroll-listeners / observers
function removeListeners(state, scrollChange) {
if (state.current.scrollContainers) {
state.current.scrollContainers.forEach(element => element.removeEventListener('scroll', scrollChange, true));
state.current.scrollContainers = null;
}
if (state.current.resizeObserver) {
state.current.resizeObserver.disconnect();
state.current.resizeObserver = null;
}
} // add scroll-listeners / observers
function addListeners(state, scrollChange, scroll) {
if (!state.current.element) return;
state.current.resizeObserver = new ResizeObserver(scrollChange);
state.current.resizeObserver.observe(state.current.element);
if (scroll && state.current.scrollContainers) {
state.current.scrollContainers.forEach(scrollContainer => scrollContainer.addEventListener('scroll', scrollChange, {
capture: true,
passive: true
}));
}
} // Checks if element boundaries are equal
const keys = ['x', 'y', 'top', 'bottom', 'left', 'right', 'width', 'height'];
const areBoundsEqual = (a, b) => keys.every(key => a[key] === b[key]);
if (typeof module !== 'undefined' && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(module, 'exports').writable) {
module.exports = useMeasure;
}
export default useMeasure;