Skip to content

Commit

Permalink
Add custom DOM element prop for injecting styles when using AnimatePr…
Browse files Browse the repository at this point in the history
…esence 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
  • Loading branch information
tehbelinda committed Oct 26, 2024
1 parent c02ec5a commit c1f485c
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 4 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface Size {
interface Props {
children: React.ReactElement
isPresent: boolean
parentDom?: HTMLElement | ShadowRoot
}

interface MeasureProps extends Props {
Expand Down Expand Up @@ -50,7 +51,7 @@ class PopChildMeasure extends React.Component<MeasureProps> {
}
}

export function PopChild({ children, isPresent }: Props) {
export function PopChild({ children, isPresent, parentDom = document.head }: Props) {
const id = useId()
const ref = useRef<HTMLElement>(null)
const size = useRef<Size>({
Expand Down Expand Up @@ -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}"] {
Expand All @@ -92,7 +93,7 @@ export function PopChild({ children, isPresent }: Props) {
}

return () => {
document.head.removeChild(style)
parentDom.removeChild(style)
}
}, [isPresent])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface PresenceChildProps {
custom?: any
presenceAffectsLayout: boolean
mode: "sync" | "popLayout" | "wait"
parentDom?: HTMLElement | ShadowRoot
}

export const PresenceChild = ({
Expand All @@ -28,6 +29,7 @@ export const PresenceChild = ({
custom,
presenceAffectsLayout,
mode,
parentDom
}: PresenceChildProps) => {
const presenceChildren = useConstant(newChildrenMap)
const id = useId()
Expand Down Expand Up @@ -83,7 +85,7 @@ export const PresenceChild = ({
}, [isPresent])

if (mode === "popLayout") {
children = <PopChild isPresent={isPresent}>{children}</PopChild>
children = <PopChild isPresent={isPresent} parentDom={parentDom}>{children}</PopChild>
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>((resolve) => {
const Component = ({ i }: { i: number }) => {
const testDom = document.createElement('div')
document.body.appendChild(testDom)
return (
<AnimatePresence mode="popLayout" parentDom={testDom}>
<motion.div
key={i}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
/>
</AnimatePresence>
)
}

const { container, rerender } = render(<Component i={0} />)
rerender(<Component i={0} />)
setTimeout(() => {
rerender(<Component i={1} />)
rerender(<Component i={1} />)
}, 50)
setTimeout(() => {
rerender(<Component i={2} />)
rerender(<Component i={2} />)
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<number>((resolve) => {
const Component = ({ i }: { i: number }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const AnimatePresence: React.FunctionComponent<
onExitComplete,
presenceAffectsLayout = true,
mode = "sync",
parentDom,
}) => {
invariant(!exitBeforeEnter, "Replace exitBeforeEnter with mode='wait'")

Expand Down Expand Up @@ -207,6 +208,7 @@ export const AnimatePresence: React.FunctionComponent<
custom={isPresent ? undefined : custom}
presenceAffectsLayout={presenceAffectsLayout}
mode={mode}
parentDom={parentDom}
onExitComplete={isPresent ? undefined : onExit}
>
{child}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit c1f485c

Please sign in to comment.