Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SVG elements (like motion.text) now update when given a MotionValue as children, matching HTML element behavior #2841

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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.10] 2024-10-23

### Changed

- SVG elements (like `motion.text`) now update when given a `MotionValue` as children, matching HTML element behavior.

## [11.11.9] 2024-10-15

### Changed
Expand Down
40 changes: 40 additions & 0 deletions dev/react/src/examples/SVG-Text-MotionValue-Child.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { animate, motion, useMotionValue, useTransform } from "framer-motion"
import { useEffect } from "react"

/**
* An example of providing a MotionValue to a component directly. Testing both
* a SVG text and HTML h1 element.
*/
export const App = () => {
const count = useMotionValue(0);
const rounded = useTransform(count, Math.round);
useEffect(() => {
const animation = animate(count, 100, { duration: 10 });
return animation.stop;
}, [])

return (<>
<p>SVG</p>
<svg
width="250"
height="250"
viewBox="0 0 250 250"
xmlns="http://www.w3.org/2000/svg"
style={{ border: '1px solid white' }}
>
<motion.text
x={125}
y={125}
fontSize={40}
dominantBaseline="middle"
textAnchor="middle"
fill="currentColor"
>
{rounded}
</motion.text>
</svg>
<p>HTML</p>
<motion.h1>{rounded}</motion.h1>
<motion.p>{rounded}</motion.p>
</>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ describe("child as motion value", () => {
return expect(promise).resolves.toHaveTextContent("1")
})

test("accepts motion values as children for motion.text inside an svg", async () => {
const promise = new Promise<SVGTextElement>((resolve) => {
const child = motionValue(3)
const Component = () => <svg><motion.text>{child}</motion.text></svg>
const { container, rerender } = render(<Component />)
rerender(<Component />)
resolve(container.firstChild?.firstChild as SVGTextElement)
})

return expect(promise).resolves.toHaveTextContent("3")
})

test("updates textContent when motion value changes", async () => {
const promise = new Promise<HTMLDivElement>((resolve) => {
const child = motionValue(1)
Expand All @@ -34,4 +46,23 @@ describe("child as motion value", () => {

return expect(promise).resolves.toHaveTextContent("2")
})

test("updates svg text when motion value changes", async () => {
const promise = new Promise<SVGTextElement>((resolve) => {
const child = motionValue(3)
const Component = () => <svg><motion.text>{child}</motion.text></svg>
const { container, rerender } = render(<Component />)
rerender(<Component />)

frame.postRender(() => {
child.set(4)

frame.postRender(() => {
resolve(container.firstChild?.firstChild as SVGTextElement)
})
})
})

return expect(promise).resolves.toHaveTextContent("4")
})
})
18 changes: 18 additions & 0 deletions packages/framer-motion/src/render/dom/DOMVisualElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MotionProps, MotionStyle } from "../../motion/types"
import { MotionValue } from "../../value"
import { HTMLRenderState } from "../html/types"
import { DOMKeyframesResolver } from "./DOMKeyframesResolver"
import { isMotionValue } from "../../value/utils/is-motion-value"

export abstract class DOMVisualElement<
Instance extends HTMLElement | SVGElement = HTMLElement,
Expand Down Expand Up @@ -37,4 +38,21 @@ export abstract class DOMVisualElement<
}

KeyframeResolver = DOMKeyframesResolver

childSubscription?: VoidFunction
handleChildMotionValue() {
if (this.childSubscription) {
this.childSubscription()
delete this.childSubscription
}

const { children } = this.props
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might still be worth exploring why a children prop is set on the SVG elements.

image

Screenshot from example in Chrome macOS: http://localhost:9990/?example=SVG-Text-MotionValue-Child

if (isMotionValue(children)) {
this.childSubscription = children.on("change", (latest) => {
if (this.current) {
this.current.textContent = `${latest}`;
}
})
}
}
}
16 changes: 0 additions & 16 deletions packages/framer-motion/src/render/html/HTMLVisualElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { MotionProps } from "../../motion/types"
import type { Box } from "../../projection/geometry/types"
import { DOMVisualElement } from "../dom/DOMVisualElement"
import { MotionConfigContext } from "../../context/MotionConfigContext"
import { isMotionValue } from "../../value/utils/is-motion-value"
import type { ResolvedValues } from "../types"
import { VisualElement } from "../VisualElement"

Expand Down Expand Up @@ -69,20 +68,5 @@ export class HTMLVisualElement extends DOMVisualElement<
return scrapeMotionValuesFromProps(props, prevProps, visualElement)
}

childSubscription?: VoidFunction
handleChildMotionValue() {
if (this.childSubscription) {
this.childSubscription()
delete this.childSubscription
}

const { children } = this.props
if (isMotionValue(children)) {
this.childSubscription = children.on("change", (latest) => {
if (this.current) this.current.textContent = `${latest}`
})
}
}

renderInstance = renderHTML
}