-
Notifications
You must be signed in to change notification settings - Fork 102
/
Copy pathindex.ts
127 lines (104 loc) · 3.43 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import {type RefObject, useEffect} from 'react';
import {useSyncedRef} from '../useSyncedRef/index.js';
import {isBrowser} from '../util/const.js';
export type UseResizeObserverCallback = (entry: ResizeObserverEntry) => void;
type ResizeObserverSingleton = {
observer: ResizeObserver;
subscribe: (target: Element, callback: UseResizeObserverCallback) => void;
unsubscribe: (target: Element, callback: UseResizeObserverCallback) => void;
};
let observerSingleton: ResizeObserverSingleton;
function getResizeObserver(): ResizeObserverSingleton | undefined {
if (!isBrowser) {
return undefined;
}
if (observerSingleton) {
return observerSingleton;
}
const callbacks = new Map<Element, Set<UseResizeObserverCallback>>();
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const cbs = callbacks.get(entry.target);
if (cbs === undefined || cbs.size === 0) {
continue;
}
for (const cb of cbs) {
setTimeout(() => {
cb(entry);
}, 0);
}
}
});
observerSingleton = {
observer,
subscribe(target, callback) {
let cbs = callbacks.get(target);
if (!cbs) {
// If target has no observers yet - register it
cbs = new Set<UseResizeObserverCallback>();
callbacks.set(target, cbs);
observer.observe(target);
}
// As Set is duplicate-safe - simply add callback on each call
cbs.add(callback);
},
unsubscribe(target, callback) {
const cbs = callbacks.get(target);
// Else branch should never occur in case of normal execution
// because callbacks map is hidden in closure - it is impossible to
// simulate situation with non-existent `cbs` Set
if (cbs) {
// Remove current observer
cbs.delete(callback);
if (cbs.size === 0) {
// If no observers left unregister target completely
callbacks.delete(target);
observer.unobserve(target);
}
}
},
};
return observerSingleton;
}
/**
* Invokes a callback whenever ResizeObserver detects a change to target's size.
*
* @param target React reference or Element to track.
* @param callback Callback that will be invoked on resize.
* @param enabled Whether resize observer is enabled or not.
*/
export function useResizeObserver<T extends Element>(
target: RefObject<T | null> | T | null,
callback: UseResizeObserverCallback,
enabled = true,
): void {
const ro = enabled && getResizeObserver();
const cb = useSyncedRef(callback);
const tgt = target && 'current' in target ? target.current : target;
useEffect(() => {
// This secondary target resolve required for case when we receive ref object, which, most
// likely, contains null during render stage, but already populated with element during
// effect stage.
const tgt = target && 'current' in target ? target.current : target;
if (!ro || !tgt) {
return;
}
// As unsubscription in internals of our ResizeObserver abstraction can
// happen a bit later than effect cleanup invocation - we need a marker,
// that this handler should not be invoked anymore
let subscribed = true;
const handler: UseResizeObserverCallback = (...args) => {
// It is reinsurance for the highly asynchronous invocations, almost
// impossible to achieve in tests, thus excluding from LOC
if (subscribed) {
cb.current(...args);
}
};
ro.subscribe(tgt, handler);
return () => {
subscribed = false;
ro.unsubscribe(tgt, handler);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tgt, ro]);
}