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