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}