Skip to content

Commit a158754

Browse files
committed
Fix: Compose child refs in AnimatePresence popLayout so external refs are preserved
1 parent e0f7e07 commit a158754

File tree

2 files changed

+66
-2
lines changed

2 files changed

+66
-2
lines changed

packages/framer-motion/src/components/AnimatePresence/PopChild.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as React from "react"
55
import { useContext, useId, useInsertionEffect, useRef } from "react"
66

77
import { MotionConfigContext } from "../../context/MotionConfigContext"
8+
import { useComposedRefs } from "./use-composed-ref"
89

910
interface Size {
1011
width: number
@@ -71,6 +72,10 @@ export function PopChild({ children, isPresent, anchorX, root }: Props) {
7172
right: 0,
7273
})
7374
const { nonce } = useContext(MotionConfigContext)
75+
const composedRef = useComposedRefs(
76+
ref,
77+
(children as { ref?: React.Ref<HTMLElement> })?.ref
78+
)
7479

7580
/**
7681
* We create and inject a style block so we can apply this explicit
@@ -92,7 +97,7 @@ export function PopChild({ children, isPresent, anchorX, root }: Props) {
9297
const style = document.createElement("style")
9398
if (nonce) style.nonce = nonce
9499

95-
const parent = root ?? document.head;
100+
const parent = root ?? document.head
96101
parent.appendChild(style)
97102

98103
if (style.sheet) {
@@ -116,7 +121,7 @@ export function PopChild({ children, isPresent, anchorX, root }: Props) {
116121

117122
return (
118123
<PopChildMeasure isPresent={isPresent} childRef={ref} sizeRef={size}>
119-
{React.cloneElement(children as any, { ref })}
124+
{React.cloneElement(children as any, { ref: composedRef })}
120125
</PopChildMeasure>
121126
)
122127
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as React from "react"
2+
3+
type PossibleRef<T> = React.Ref<T> | undefined
4+
5+
/**
6+
* Set a given ref to a given value
7+
* This utility takes care of different types of refs: callback refs and RefObject(s)
8+
*/
9+
function setRef<T>(ref: PossibleRef<T>, value: T): void | (() => void) {
10+
if (typeof ref === "function") {
11+
return ref(value)
12+
} else if (ref !== null && ref !== undefined) {
13+
;(ref as React.MutableRefObject<T>).current = value
14+
}
15+
}
16+
17+
/**
18+
* A utility to compose multiple refs together
19+
* Accepts callback refs and RefObject(s)
20+
*/
21+
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
22+
return (node) => {
23+
let hasCleanup = false
24+
const cleanups = refs.map((ref) => {
25+
const cleanup = setRef(ref, node)
26+
if (!hasCleanup && typeof cleanup === "function") {
27+
hasCleanup = true
28+
}
29+
return cleanup
30+
})
31+
// React <19 will log an error to the console if a callback ref returns a
32+
// value. We don't use ref cleanups internally so this will only happen if a
33+
// user's ref callback returns a value, which we only expect if they are
34+
// using the cleanup functionality added in React 19.
35+
if (hasCleanup) {
36+
return () => {
37+
for (let i = 0; i < cleanups.length; i++) {
38+
const cleanup = cleanups[i]
39+
if (typeof cleanup === "function") {
40+
cleanup()
41+
} else {
42+
setRef(refs[i], null)
43+
}
44+
}
45+
}
46+
}
47+
}
48+
}
49+
50+
/**
51+
* A custom hook that composes multiple refs
52+
* Accepts callback refs and RefObject(s)
53+
*/
54+
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
55+
// eslint-disable-next-line react-hooks/exhaustive-deps
56+
return React.useCallback(composeRefs(...refs), refs)
57+
}
58+
59+
export { useComposedRefs }

0 commit comments

Comments
 (0)