Skip to content

Commit 88dd5de

Browse files
tehbelindaGradial
authored and
Gradial
committed
Add custom DOM element prop for injecting styles when using AnimatePresence with mode === popLayout
By default this was using document.head, but that is not always available to where it's rendered, e.g. when using the shadow DOM. Instead, pass the shadow root to the new `parentDOM` prop
1 parent d905a99 commit 88dd5de

File tree

6 files changed

+48
-9
lines changed

6 files changed

+48
-9
lines changed

CHANGELOG.md

-5
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@ Framer Motion adheres to [Semantic Versioning](http://semver.org/).
44

55
Undocumented APIs should be considered internal and may change without warning.
66

7-
## [11.11.9] 2024-10-15
8-
9-
### Changed
10-
11-
- `will-change` is now no longer automatically managed without `useWillChange`.
127

138
## [11.11.8] 2024-10-11
149

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface Size {
1515
interface Props {
1616
children: React.ReactElement
1717
isPresent: boolean
18+
parentDom?: HTMLElement | ShadowRoot
1819
}
1920

2021
interface MeasureProps extends Props {
@@ -50,7 +51,7 @@ class PopChildMeasure extends React.Component<MeasureProps> {
5051
}
5152
}
5253

53-
export function PopChild({ children, isPresent }: Props) {
54+
export function PopChild({ children, isPresent, parentDom = document.head }: Props) {
5455
const id = useId()
5556
const ref = useRef<HTMLElement>(null)
5657
const size = useRef<Size>({
@@ -78,7 +79,7 @@ export function PopChild({ children, isPresent }: Props) {
7879

7980
const style = document.createElement("style")
8081
if (nonce) style.nonce = nonce
81-
document.head.appendChild(style)
82+
parentDom.appendChild(style)
8283
if (style.sheet) {
8384
style.sheet.insertRule(`
8485
[data-motion-pop-id="${id}"] {
@@ -92,7 +93,7 @@ export function PopChild({ children, isPresent }: Props) {
9293
}
9394

9495
return () => {
95-
document.head.removeChild(style)
96+
parentDom.removeChild(style)
9697
}
9798
}, [isPresent])
9899

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface PresenceChildProps {
1818
custom?: any
1919
presenceAffectsLayout: boolean
2020
mode: "sync" | "popLayout" | "wait"
21+
parentDom?: HTMLElement | ShadowRoot
2122
}
2223

2324
export const PresenceChild = ({
@@ -28,6 +29,7 @@ export const PresenceChild = ({
2829
custom,
2930
presenceAffectsLayout,
3031
mode,
32+
parentDom
3133
}: PresenceChildProps) => {
3234
const presenceChildren = useConstant(newChildrenMap)
3335
const id = useId()
@@ -83,7 +85,7 @@ export const PresenceChild = ({
8385
}, [isPresent])
8486

8587
if (mode === "popLayout") {
86-
children = <PopChild isPresent={isPresent}>{children}</PopChild>
88+
children = <PopChild isPresent={isPresent} parentDom={parentDom}>{children}</PopChild>
8789
}
8890

8991
return (

packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx

+33
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,39 @@ describe("AnimatePresence", () => {
346346
return await expect(promise).resolves.toBe(3)
347347
})
348348

349+
test("Can cycle through multiple components with mode === 'popLayout' and dom", async () => {
350+
const promise = new Promise<number>((resolve) => {
351+
const Component = ({ i }: { i: number }) => {
352+
const testDom = document.createElement('div')
353+
document.body.appendChild(testDom)
354+
return (
355+
<AnimatePresence mode="popLayout" parentDom={testDom}>
356+
<motion.div
357+
key={i}
358+
animate={{ opacity: 1 }}
359+
exit={{ opacity: 0 }}
360+
transition={{ duration: 0.5 }}
361+
/>
362+
</AnimatePresence>
363+
)
364+
}
365+
366+
const { container, rerender } = render(<Component i={0} />)
367+
rerender(<Component i={0} />)
368+
setTimeout(() => {
369+
rerender(<Component i={1} />)
370+
rerender(<Component i={1} />)
371+
}, 50)
372+
setTimeout(() => {
373+
rerender(<Component i={2} />)
374+
rerender(<Component i={2} />)
375+
resolve(container.childElementCount)
376+
}, 400)
377+
})
378+
379+
return await expect(promise).resolves.toBe(3)
380+
})
381+
349382
test("Only renders one child at a time if mode === 'wait'", async () => {
350383
const promise = new Promise<number>((resolve) => {
351384
const Component = ({ i }: { i: number }) => {

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

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const AnimatePresence: React.FunctionComponent<
5353
onExitComplete,
5454
presenceAffectsLayout = true,
5555
mode = "sync",
56+
parentDom,
5657
}) => {
5758
invariant(!exitBeforeEnter, "Replace exitBeforeEnter with mode='wait'")
5859

@@ -207,6 +208,7 @@ export const AnimatePresence: React.FunctionComponent<
207208
custom={isPresent ? undefined : custom}
208209
presenceAffectsLayout={presenceAffectsLayout}
209210
mode={mode}
211+
parentDom={parentDom}
210212
onExitComplete={isPresent ? undefined : onExit}
211213
>
212214
{child}

packages/framer-motion/src/components/AnimatePresence/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ export interface AnimatePresenceProps {
6161
*/
6262
mode?: "sync" | "popLayout" | "wait"
6363

64+
/**
65+
* Parent DOM element used when injecting styles, used when mode === `"popLayout"`.
66+
* This defaults to document.head but can be overridden e.g. for use in shadow DOM.
67+
*/
68+
parentDom?: HTMLElement | ShadowRoot;
69+
6470
/**
6571
* Internal. Used in Framer to flag that sibling children *shouldn't* re-render as a result of a
6672
* child being removed.

0 commit comments

Comments
 (0)