-
Notifications
You must be signed in to change notification settings - Fork 104
Expand file tree
/
Copy pathindex.ts
More file actions
128 lines (105 loc) · 3.45 KB
/
index.ts
File metadata and controls
128 lines (105 loc) · 3.45 KB
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
128
import type {RefObject} from 'react';
import {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]);
}