-
Notifications
You must be signed in to change notification settings - Fork 104
Expand file tree
/
Copy pathindex.ts
More file actions
137 lines (112 loc) · 4.09 KB
/
Copy pathindex.ts
File metadata and controls
137 lines (112 loc) · 4.09 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
129
130
131
132
133
134
135
136
137
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;
};
// One observer per box model: a ResizeObserver only notifies on changes of the
// box it was asked to observe, so boxes cannot share an instance.
const observerSingletons = new Map<ResizeObserverBoxOptions, ResizeObserverSingleton>();
function getResizeObserver(box: ResizeObserverBoxOptions): ResizeObserverSingleton | undefined {
if (!isBrowser) {
return undefined;
}
const observerSingleton = observerSingletons.get(box);
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);
}
}
});
const singleton: ResizeObserverSingleton = {
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, {box});
}
// 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);
}
}
},
};
observerSingletons.set(box, singleton);
return singleton;
}
/**
* 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.
* @param box Box model whose changes trigger the callback. A `content-box` observer stays silent
* when padding or border grow around an unchanged content box, and vice versa.
*/
export function useResizeObserver<T extends Element>(
target: RefObject<T | null> | T | null,
callback: UseResizeObserverCallback,
enabled = true,
box: ResizeObserverBoxOptions = 'content-box',
): void {
const ro = enabled && getResizeObserver(box);
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 element = target && 'current' in target ? target.current : target;
if (ro === false || ro === undefined || element === null || element === undefined) {
return undefined;
}
// 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(element, handler);
return () => {
subscribed = false;
ro.unsubscribe(element, handler);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tgt, ro]);
}