From c1f485cf3e5300720a6f41805eb1bb0cb4845382 Mon Sep 17 00:00:00 2001 From: Bel Teh Date: Sat, 26 Oct 2024 14:37:04 +0000 Subject: [PATCH] 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 --- CHANGELOG.md | 6 ++++ .../components/AnimatePresence/PopChild.tsx | 7 ++-- .../AnimatePresence/PresenceChild.tsx | 4 ++- .../__tests__/AnimatePresence.test.tsx | 33 +++++++++++++++++++ .../src/components/AnimatePresence/index.tsx | 2 ++ .../src/components/AnimatePresence/types.ts | 6 ++++ 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 521e0f61c2..fa40aa1364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Framer Motion adheres to [Semantic Versioning](http://semver.org/). Undocumented APIs should be considered internal and may change without warning. +## [11.11.11] 2024-10-25 + +### Fixed + +- Allowing custom DOM element for injecting styles when using AnimatePresence with mode === `popLayout`. Fixes shadow DOM issue [#2508](https://github.com/framer/motion/issues/2508) by passing the shadow root into the new `parentDOM` prop. + ## [11.11.10] 2024-10-25 ### Fixed diff --git a/packages/framer-motion/src/components/AnimatePresence/PopChild.tsx b/packages/framer-motion/src/components/AnimatePresence/PopChild.tsx index 17cf8867a4..21d8e1226c 100644 --- a/packages/framer-motion/src/components/AnimatePresence/PopChild.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/PopChild.tsx @@ -15,6 +15,7 @@ interface Size { interface Props { children: React.ReactElement isPresent: boolean + parentDom?: HTMLElement | ShadowRoot } interface MeasureProps extends Props { @@ -50,7 +51,7 @@ class PopChildMeasure extends React.Component { } } -export function PopChild({ children, isPresent }: Props) { +export function PopChild({ children, isPresent, parentDom = document.head }: Props) { const id = useId() const ref = useRef(null) const size = useRef({ @@ -78,7 +79,7 @@ export function PopChild({ children, isPresent }: Props) { const style = document.createElement("style") if (nonce) style.nonce = nonce - document.head.appendChild(style) + parentDom.appendChild(style) if (style.sheet) { style.sheet.insertRule(` [data-motion-pop-id="${id}"] { @@ -92,7 +93,7 @@ export function PopChild({ children, isPresent }: Props) { } return () => { - document.head.removeChild(style) + parentDom.removeChild(style) } }, [isPresent]) diff --git a/packages/framer-motion/src/components/AnimatePresence/PresenceChild.tsx b/packages/framer-motion/src/components/AnimatePresence/PresenceChild.tsx index b9fe4b6e5d..9d5325200d 100644 --- a/packages/framer-motion/src/components/AnimatePresence/PresenceChild.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/PresenceChild.tsx @@ -18,6 +18,7 @@ interface PresenceChildProps { custom?: any presenceAffectsLayout: boolean mode: "sync" | "popLayout" | "wait" + parentDom?: HTMLElement | ShadowRoot } export const PresenceChild = ({ @@ -28,6 +29,7 @@ export const PresenceChild = ({ custom, presenceAffectsLayout, mode, + parentDom }: PresenceChildProps) => { const presenceChildren = useConstant(newChildrenMap) const id = useId() @@ -83,7 +85,7 @@ export const PresenceChild = ({ }, [isPresent]) if (mode === "popLayout") { - children = {children} + children = {children} } return ( diff --git a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx index 685337b525..ce708572c6 100644 --- a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx @@ -346,6 +346,39 @@ describe("AnimatePresence", () => { return await expect(promise).resolves.toBe(3) }) + test("Can cycle through multiple components with mode === 'popLayout' and dom", async () => { + const promise = new Promise((resolve) => { + const Component = ({ i }: { i: number }) => { + const testDom = document.createElement('div') + document.body.appendChild(testDom) + return ( + + + + ) + } + + const { container, rerender } = render() + rerender() + setTimeout(() => { + rerender() + rerender() + }, 50) + setTimeout(() => { + rerender() + rerender() + resolve(container.childElementCount) + }, 400) + }) + + return await expect(promise).resolves.toBe(3) + }) + test("Only renders one child at a time if mode === 'wait'", async () => { const promise = new Promise((resolve) => { const Component = ({ i }: { i: number }) => { diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 84c6b28770..2743eb836d 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -53,6 +53,7 @@ export const AnimatePresence: React.FunctionComponent< onExitComplete, presenceAffectsLayout = true, mode = "sync", + parentDom, }) => { invariant(!exitBeforeEnter, "Replace exitBeforeEnter with mode='wait'") @@ -207,6 +208,7 @@ export const AnimatePresence: React.FunctionComponent< custom={isPresent ? undefined : custom} presenceAffectsLayout={presenceAffectsLayout} mode={mode} + parentDom={parentDom} onExitComplete={isPresent ? undefined : onExit} > {child} diff --git a/packages/framer-motion/src/components/AnimatePresence/types.ts b/packages/framer-motion/src/components/AnimatePresence/types.ts index 5f54c02aa1..c434e07487 100644 --- a/packages/framer-motion/src/components/AnimatePresence/types.ts +++ b/packages/framer-motion/src/components/AnimatePresence/types.ts @@ -61,6 +61,12 @@ export interface AnimatePresenceProps { */ mode?: "sync" | "popLayout" | "wait" + /** + * Parent DOM element used when injecting styles, used when mode === `"popLayout"`. + * This defaults to document.head but can be overridden e.g. for use in shadow DOM. + */ + parentDom?: HTMLElement | ShadowRoot; + /** * Internal. Used in Framer to flag that sibling children *shouldn't* re-render as a result of a * child being removed.