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