-
Notifications
You must be signed in to change notification settings - Fork 30
/
index.ts
201 lines (174 loc) Β· 6.41 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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import { useEffect, useState, useRef, useMemo } from 'react'
import createDebounce from 'debounce'
declare type ResizeObserverCallback = (entries: any[], observer: ResizeObserver) => void
declare class ResizeObserver {
constructor(callback: ResizeObserverCallback)
observe(target: Element, options?: any): void
unobserve(target: Element): void
disconnect(): void
static toString(): string
}
export interface RectReadOnly {
readonly x: number
readonly y: number
readonly width: number
readonly height: number
readonly top: number
readonly right: number
readonly bottom: number
readonly left: number
[key: string]: number
}
type HTMLOrSVGElement = HTMLElement | SVGElement
type Result = [(element: HTMLOrSVGElement | null) => void, RectReadOnly, () => void]
type State = {
element: HTMLOrSVGElement | null
scrollContainers: HTMLOrSVGElement[] | null
resizeObserver: ResizeObserver | null
lastBounds: RectReadOnly
}
export type Options = {
debounce?: number | { scroll: number; resize: number }
scroll?: boolean
polyfill?: { new (cb: ResizeObserverCallback): ResizeObserver }
offsetSize?: boolean
}
function useMeasure(
{ debounce, scroll, polyfill, offsetSize }: Options = { debounce: 0, scroll: false, offsetSize: false }
): Result {
const ResizeObserver =
polyfill || (typeof window === 'undefined' ? class ResizeObserver {} : (window as any).ResizeObserver)
if (!ResizeObserver) {
throw new Error(
'This browser does not support ResizeObserver out of the box. See: https://github.com/react-spring/react-use-measure/#resize-observer-polyfills'
)
}
const [bounds, set] = useState<RectReadOnly>({
left: 0,
top: 0,
width: 0,
height: 0,
bottom: 0,
right: 0,
x: 0,
y: 0,
})
// keep all state in a ref
const state = useRef<State>({ element: null, scrollContainers: null, resizeObserver: null, lastBounds: bounds })
// set actual debounce values early, so effects know if they should react accordingly
const scrollDebounce = debounce ? (typeof debounce === 'number' ? debounce : debounce.scroll) : null
const resizeDebounce = debounce ? (typeof debounce === 'number' ? debounce : debounce.resize) : null
// make sure to update state only as long as the component is truly mounted
const mounted = useRef(false)
useEffect(() => {
mounted.current = true
return () => void (mounted.current = false)
})
// memoize handlers, so event-listeners know when they should update
const [forceRefresh, resizeChange, scrollChange] = useMemo(() => {
const callback = () => {
if (!state.current.element) return
const {
left,
top,
width,
height,
bottom,
right,
x,
y,
} = (state.current.element.getBoundingClientRect() as unknown) as RectReadOnly
const size = {
left,
top,
width,
height,
bottom,
right,
x,
y,
}
if (state.current.element instanceof HTMLElement && offsetSize) {
size.height = state.current.element.offsetHeight
size.width = state.current.element.offsetWidth
}
Object.freeze(size)
if (mounted.current && !areBoundsEqual(state.current.lastBounds, size)) set((state.current.lastBounds = size))
}
return [
callback,
resizeDebounce ? createDebounce(callback, resizeDebounce) : callback,
scrollDebounce ? createDebounce(callback, scrollDebounce) : callback,
]
}, [set, offsetSize, scrollDebounce, resizeDebounce])
// cleanup current scroll-listeners / observers
function removeListeners() {
if (state.current.scrollContainers) {
state.current.scrollContainers.forEach((element) => element.removeEventListener('scroll', scrollChange, true))
state.current.scrollContainers = null
}
if (state.current.resizeObserver) {
state.current.resizeObserver.disconnect()
state.current.resizeObserver = null
}
}
// add scroll-listeners / observers
function addListeners() {
if (!state.current.element) return
state.current.resizeObserver = new ResizeObserver(scrollChange)
state.current.resizeObserver!.observe(state.current.element)
if (scroll && state.current.scrollContainers) {
state.current.scrollContainers.forEach((scrollContainer) =>
scrollContainer.addEventListener('scroll', scrollChange, { capture: true, passive: true })
)
}
}
// the ref we expose to the user
const ref = (node: HTMLOrSVGElement | null) => {
if (!node || node === state.current.element) return
removeListeners()
state.current.element = node
state.current.scrollContainers = findScrollContainers(node)
addListeners()
}
// add general event listeners
useOnWindowScroll(scrollChange, Boolean(scroll))
useOnWindowResize(resizeChange)
// respond to changes that are relevant for the listeners
useEffect(() => {
removeListeners()
addListeners()
}, [scroll, scrollChange, resizeChange])
// remove all listeners when the components unmounts
useEffect(() => removeListeners, [])
return [ref, bounds, forceRefresh]
}
// Adds native resize listener to window
function useOnWindowResize(onWindowResize: (event: Event) => void) {
useEffect(() => {
const cb = onWindowResize
window.addEventListener('resize', cb)
return () => void window.removeEventListener('resize', cb)
}, [onWindowResize])
}
function useOnWindowScroll(onScroll: () => void, enabled: boolean) {
useEffect(() => {
if (enabled) {
const cb = onScroll
window.addEventListener('scroll', cb, { capture: true, passive: true })
return () => void window.removeEventListener('scroll', cb, true)
}
}, [onScroll, enabled])
}
// Returns a list of scroll offsets
function findScrollContainers(element: HTMLOrSVGElement | null): HTMLOrSVGElement[] {
const result: HTMLOrSVGElement[] = []
if (!element || element === document.body) return result
const { overflow, overflowX, overflowY } = window.getComputedStyle(element)
if ([overflow, overflowX, overflowY].some((prop) => prop === 'auto' || prop === 'scroll')) result.push(element)
return [...result, ...findScrollContainers(element.parentElement)]
}
// Checks if element boundaries are equal
const keys: (keyof RectReadOnly)[] = ['x', 'y', 'top', 'bottom', 'left', 'right', 'width', 'height']
const areBoundsEqual = (a: RectReadOnly, b: RectReadOnly): boolean => keys.every((key) => a[key] === b[key])
export default useMeasure