Skip to content

Commit

Permalink
SVG elements (like motion.text) now update when given a MotionValue a…
Browse files Browse the repository at this point in the history
…s children, matching HTML element behavior (#2841)

* Update the motion value of a child via its firstChild text node instead of innerText

* Added changelog entry

* Improved changelog entry

* Added a test for motion.text inside an svg

* Still use textContent where possible

* An example of providing a MotionValue to a component directly. Testing both a SVG text and HTML h1 element

* SVG also supports updating through textContent

* Since the implementation in HTMLVisualElement and SVGVisualElement is now identical, move logic to DOMVisualElement

* Updated tests and changelog entry to reflect latest changes

---------

Co-authored-by: Matt Perry <[email protected]>
  • Loading branch information
simonkarman and mattgperry authored Dec 3, 2024
1 parent 7c6b174 commit 7c66534
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 16 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 @@ Motion adheres to [Semantic Versioning](http://semver.org/).

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

## [11.13.1] 2024-12-03

### Fixed

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

## [11.13.0] 2024-12-03

### Added
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
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 @@ -67,20 +66,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
}

0 comments on commit 7c66534

Please sign in to comment.