-
-
Notifications
You must be signed in to change notification settings - Fork 898
/
Copy pathindex.tsx
223 lines (195 loc) · 7.31 KB
/
index.tsx
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
"use client"
import * as React from "react"
import { useContext, useMemo, useRef, useState } from "react"
import { LayoutGroupContext } from "../../context/LayoutGroupContext"
import { useIsomorphicLayoutEffect } from "../../three-entry"
import { useConstant } from "../../utils/use-constant"
import { PresenceChild } from "./PresenceChild"
import { AnimatePresenceProps } from "./types"
import { usePresence } from "./use-presence"
import { ComponentKey, getChildKey, onlyElements } from "./utils"
/**
* `AnimatePresence` enables the animation of components that have been removed from the tree.
*
* When adding/removing more than a single child, every child **must** be given a unique `key` prop.
*
* Any `motion` components that have an `exit` property defined will animate out when removed from
* the tree.
*
* ```jsx
* import { motion, AnimatePresence } from 'framer-motion'
*
* export const Items = ({ items }) => (
* <AnimatePresence>
* {items.map(item => (
* <motion.div
* key={item.id}
* initial={{ opacity: 0 }}
* animate={{ opacity: 1 }}
* exit={{ opacity: 0 }}
* />
* ))}
* </AnimatePresence>
* )
* ```
*
* You can sequence exit animations throughout a tree using variants.
*
* If a child contains multiple `motion` components with `exit` props, it will only unmount the child
* once all `motion` components have finished animating out. Likewise, any components using
* `usePresence` all need to call `safeToRemove`.
*
* @public
*/
export const AnimatePresence = ({
children,
custom,
initial = true,
onExitComplete,
presenceAffectsLayout = true,
mode = "sync",
propagate = false,
anchorX = "left",
}: React.PropsWithChildren<AnimatePresenceProps>) => {
const [isParentPresent, safeToRemove] = usePresence(propagate)
/**
* Filter any children that aren't ReactElements. We can only track components
* between renders with a props.key.
*/
const presentChildren = useMemo(() => onlyElements(children), [children])
/**
* Track the keys of the currently rendered children. This is used to
* determine which children are exiting.
*/
const presentKeys =
propagate && !isParentPresent ? [] : presentChildren.map(getChildKey)
/**
* If `initial={false}` we only want to pass this to components in the first render.
*/
const isInitialRender = useRef(true)
/**
* A ref containing the currently present children. When all exit animations
* are complete, we use this to re-render the component with the latest children
* *committed* rather than the latest children *rendered*.
*/
const pendingPresentChildren = useRef(presentChildren)
/**
* Track which exiting children have finished animating out.
*/
const exitComplete = useConstant(() => new Map<ComponentKey, boolean>())
/**
* Save children to render as React state. To ensure this component is concurrent-safe,
* we check for exiting children via an effect.
*/
const [diffedChildren, setDiffedChildren] = useState(presentChildren)
const [renderedChildren, setRenderedChildren] = useState(presentChildren)
useIsomorphicLayoutEffect(() => {
isInitialRender.current = false
pendingPresentChildren.current = presentChildren
/**
* Update complete status of exiting children.
*/
for (let i = 0; i < renderedChildren.length; i++) {
const key = getChildKey(renderedChildren[i])
if (!presentKeys.includes(key)) {
if (exitComplete.get(key) !== true) {
exitComplete.set(key, false)
}
} else {
exitComplete.delete(key)
}
}
}, [renderedChildren, presentKeys.length, presentKeys.join("-")])
const exitingChildren: any[] = []
if (presentChildren !== diffedChildren) {
let nextChildren = [...presentChildren]
/**
* Loop through all the currently rendered components and decide which
* are exiting.
*/
for (let i = 0; i < renderedChildren.length; i++) {
const child = renderedChildren[i]
const key = getChildKey(child)
if (!presentKeys.includes(key)) {
nextChildren.splice(i, 0, child)
exitingChildren.push(child)
}
}
/**
* If we're in "wait" mode, and we have exiting children, we want to
* only render these until they've all exited.
*/
if (mode === "wait" && exitingChildren.length) {
nextChildren = exitingChildren
}
setRenderedChildren(onlyElements(nextChildren))
setDiffedChildren(presentChildren)
/**
* Early return to ensure once we've set state with the latest diffed
* children, we can immediately re-render.
*/
return null
}
if (
process.env.NODE_ENV !== "production" &&
mode === "wait" &&
renderedChildren.length > 1
) {
console.warn(
`You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour.`
)
}
/**
* If we've been provided a forceRender function by the LayoutGroupContext,
* we can use it to force a re-render amongst all surrounding components once
* all components have finished animating out.
*/
const { forceRender } = useContext(LayoutGroupContext)
return (
<>
{renderedChildren.map((child) => {
const key = getChildKey(child)
const isPresent =
propagate && !isParentPresent
? false
: presentChildren === renderedChildren ||
presentKeys.includes(key)
const onExit = () => {
if (exitComplete.has(key)) {
exitComplete.set(key, true)
} else {
return
}
let isEveryExitComplete = true
exitComplete.forEach((isExitComplete) => {
if (!isExitComplete) isEveryExitComplete = false
})
if (isEveryExitComplete) {
forceRender?.()
setRenderedChildren(pendingPresentChildren.current)
propagate && safeToRemove?.()
onExitComplete && onExitComplete()
}
}
return (
<PresenceChild
key={key}
isPresent={isPresent}
initial={
!isInitialRender.current || initial
? undefined
: false
}
custom={custom}
presenceAffectsLayout={presenceAffectsLayout}
mode={mode}
onExitComplete={isPresent ? undefined : onExit}
anchorX={anchorX}
>
{child}
</PresenceChild>
)
})}
</>
)
}