-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathuseInView.ts
126 lines (103 loc) · 3.25 KB
/
useInView.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
import { RefObject, useRef, useState } from 'react'
import { is, useIsomorphicLayoutEffect } from '@react-spring/shared'
import { Lookup } from '@react-spring/types'
import { PickAnimated, SpringValues } from '../types'
import { useSpring, UseSpringProps } from './useSpring'
import { Valid } from '../types/common'
export interface IntersectionArgs
extends Omit<IntersectionObserverInit, 'root' | 'threshold'> {
root?: React.MutableRefObject<HTMLElement>
once?: boolean
amount?: 'any' | 'all' | number | number[]
}
const defaultThresholdOptions = {
any: 0,
all: 1,
}
export function useInView(args?: IntersectionArgs): [RefObject<any>, boolean]
export function useInView<Props extends object>(
/**
* TODO: make this narrower to only accept reserved props.
*/
props: () => Props & Valid<Props, UseSpringProps<Props>>,
args?: IntersectionArgs
): PickAnimated<Props> extends infer State
? State extends Lookup
? [RefObject<any>, SpringValues<State>]
: never
: never
export function useInView<TElement extends HTMLElement>(
props?: (() => UseSpringProps<any>) | IntersectionArgs,
args?: IntersectionArgs
) {
const [isInView, setIsInView] = useState(false)
const ref = useRef<TElement>(null)
const propsFn = is.fun(props) && props
const springsProps = propsFn ? propsFn() : {}
const { to = {}, from = {}, ...restSpringProps } = springsProps
const intersectionArguments = propsFn ? args : props
const [springs, api] = useSpring(() => ({ from, ...restSpringProps }), [])
useIsomorphicLayoutEffect(() => {
const element = ref.current
const {
root,
once,
amount = 'any',
...restArgs
} = intersectionArguments ?? {}
if (
!element ||
(once && isInView) ||
typeof IntersectionObserver === 'undefined'
)
return
const activeIntersections = new WeakMap<Element, VoidFunction>()
const onEnter = () => {
if (to) {
// @ts-expect-error – TODO: fix this type error
api.start(to)
}
setIsInView(true)
const cleanup = () => {
if (from) {
api.start(from)
}
setIsInView(false)
}
return once ? undefined : cleanup
}
const handleIntersection: IntersectionObserverCallback = entries => {
entries.forEach(entry => {
const onLeave = activeIntersections.get(entry.target)
if (entry.isIntersecting === Boolean(onLeave)) {
return
}
if (entry.isIntersecting) {
const newOnLeave = onEnter()
if (is.fun(newOnLeave)) {
activeIntersections.set(entry.target, newOnLeave)
} else {
observer.unobserve(entry.target)
}
} else if (onLeave) {
onLeave()
activeIntersections.delete(entry.target)
}
})
}
const observer = new IntersectionObserver(handleIntersection, {
root: (root && root.current) || undefined,
threshold:
typeof amount === 'number' || Array.isArray(amount)
? amount
: defaultThresholdOptions[amount],
...restArgs,
})
observer.observe(element)
return () => observer.unobserve(element)
}, [intersectionArguments])
if (propsFn) {
return [ref, springs]
}
return [ref, isInView]
}