Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1const resizeOptions: ResizeObserverOptions = { box: 'border-box' }; 2const mutationObservers: Map<HTMLElement, MutationObserver> = new Map(); 3const resizeListeners: Map<HTMLElement, Array<() => void>> = new Map(); 4 5const resizeObserver = new ResizeObserver(entries => { 6 const parents = new Set<Element>(); 7 for (let i = 0; i < entries.length; i++) { 8 const parent = entries[i].target.parentElement; 9 if (parent && !parents.has(parent)) { 10 parents.add(parent); 11 const listeners = resizeListeners.get(parent) || []; 12 for (let i = 0; i < listeners.length; i++) listeners[i](); 13 } 14 } 15}); 16 17export function observeScrollArea( 18 element: HTMLElement, 19 onAreaChange: (width: number, height: number) => void 20): () => void { 21 const listeners = resizeListeners.get(element) || []; 22 const isFirstListener = !listeners.length; 23 resizeListeners.set(element, listeners); 24 25 let previousScrollHeight: null | number = null; 26 let hasUnmounted = false; 27 const onResize = () => { 28 const scrollHeight = element.scrollHeight || 0; 29 if (!hasUnmounted && scrollHeight !== previousScrollHeight) { 30 onAreaChange(element.scrollWidth, element.scrollHeight); 31 previousScrollHeight = scrollHeight; 32 } 33 }; 34 35 listeners.push(onResize); 36 37 if (isFirstListener) { 38 const mutationObserver = new MutationObserver(entries => { 39 for (let i = 0; i < entries.length; i++) { 40 const entry = entries[i]; 41 for (let j = 0; j < entry.addedNodes.length; j++) { 42 const node = entry.addedNodes[j]; 43 if (node.nodeType === Node.ELEMENT_NODE) { 44 resizeObserver.observe(node as Element, resizeOptions); 45 } 46 } 47 48 for (let j = 0; j < entry.removedNodes.length; j++) { 49 const node = entry.removedNodes[j]; 50 if (node.nodeType === Node.ELEMENT_NODE) { 51 resizeObserver.unobserve(node as Element); 52 } 53 } 54 } 55 }); 56 57 requestAnimationFrame(() => { 58 const childNodes = element.childNodes; 59 for (let i = 0; i < childNodes.length; i++) 60 if (childNodes[i].nodeType === Node.ELEMENT_NODE) 61 resizeObserver.observe(childNodes[i] as Element, resizeOptions); 62 mutationObserver.observe(element, { childList: true }); 63 }); 64 65 mutationObservers.set(element, mutationObserver); 66 } 67 68 return () => { 69 const listeners = resizeListeners.get(element) || []; 70 listeners.splice(listeners.indexOf(onResize), 1); 71 hasUnmounted = true; 72 73 if (!listeners.length) { 74 const mutationObserver = mutationObservers.get(element); 75 if (mutationObserver) mutationObserver.disconnect(); 76 77 const childNodes = element.childNodes; 78 for (let i = 0; i < childNodes.length; i++) 79 if (childNodes[i].nodeType === Node.ELEMENT_NODE) 80 resizeObserver.unobserve(childNodes[i] as Element); 81 82 resizeListeners.delete(element); 83 mutationObservers.delete(element); 84 } 85 }; 86}