From 76388a9e69034dbd407ddc1d7dae67f8f56266f3 Mon Sep 17 00:00:00 2001 From: Simon Karman Date: Wed, 23 Oct 2024 21:19:43 +0200 Subject: [PATCH 1/9] Update the motion value of a child via its firstChild text node instead of innerText --- .../framer-motion/src/render/html/HTMLVisualElement.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/framer-motion/src/render/html/HTMLVisualElement.ts b/packages/framer-motion/src/render/html/HTMLVisualElement.ts index c538de2787..5a0ee5a7e8 100644 --- a/packages/framer-motion/src/render/html/HTMLVisualElement.ts +++ b/packages/framer-motion/src/render/html/HTMLVisualElement.ts @@ -79,7 +79,13 @@ export class HTMLVisualElement extends DOMVisualElement< const { children } = this.props if (isMotionValue(children)) { this.childSubscription = children.on("change", (latest) => { - if (this.current) this.current.textContent = `${latest}` + if ( + this.current + && this.current.firstChild + && this.current.firstChild.nodeType === Node.TEXT_NODE + ) { + this.current.firstChild.nodeValue = `${latest}` + } }) } } From 800bcaebb06132934304a7480eadd8c2782401f6 Mon Sep 17 00:00:00 2001 From: Simon Karman Date: Wed, 23 Oct 2024 21:22:44 +0200 Subject: [PATCH 2/9] Added changelog entry --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc95a1521b..f7b45eeb11 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.10] 2024-10-23 + +### Changed + +- Fixed `motion` components accept a `MotionValue` as `children` for elements that doen't support innerText (such as `motion.text` in SVGs). + ## [11.11.9] 2024-10-15 ### Changed From 19cc79676d320fadc32d4ffeda5f9536531e69e0 Mon Sep 17 00:00:00 2001 From: Simon Karman Date: Wed, 23 Oct 2024 21:29:55 +0200 Subject: [PATCH 3/9] Improved changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7b45eeb11..d3567d5564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Undocumented APIs should be considered internal and may change without warning. ### Changed -- Fixed `motion` components accept a `MotionValue` as `children` for elements that doen't support innerText (such as `motion.text` in SVGs). +- SVG `motion.text` and similar components now properly update when provided a `MotionValue` as children. ## [11.11.9] 2024-10-15 From 7d0a0b62eab93ba85f84b6e5c109f1c7352150b2 Mon Sep 17 00:00:00 2001 From: Simon Karman Date: Wed, 23 Oct 2024 21:33:53 +0200 Subject: [PATCH 4/9] Added a test for motion.text inside an svg --- .../src/motion/__tests__/child-motion-value.test.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/framer-motion/src/motion/__tests__/child-motion-value.test.tsx b/packages/framer-motion/src/motion/__tests__/child-motion-value.test.tsx index 0e0d63909a..3ef4b4d069 100644 --- a/packages/framer-motion/src/motion/__tests__/child-motion-value.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/child-motion-value.test.tsx @@ -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((resolve) => { + const child = motionValue(3) + const Component = () => {child} + const { container, rerender } = render() + rerender() + resolve(container.firstChild as HTMLDivElement) + }) + + return expect(promise).resolves.toHaveTextContent("3") + }) + test("updates textContent when motion value changes", async () => { const promise = new Promise((resolve) => { const child = motionValue(1) From a2eec07cb84a8bb6185449f170f3f1f484e71dd2 Mon Sep 17 00:00:00 2001 From: Simon Karman Date: Wed, 23 Oct 2024 21:44:02 +0200 Subject: [PATCH 5/9] Still use textContent where possible --- .../framer-motion/src/render/html/HTMLVisualElement.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/framer-motion/src/render/html/HTMLVisualElement.ts b/packages/framer-motion/src/render/html/HTMLVisualElement.ts index 5a0ee5a7e8..220c875d88 100644 --- a/packages/framer-motion/src/render/html/HTMLVisualElement.ts +++ b/packages/framer-motion/src/render/html/HTMLVisualElement.ts @@ -80,10 +80,17 @@ export class HTMLVisualElement extends DOMVisualElement< if (isMotionValue(children)) { this.childSubscription = children.on("change", (latest) => { if ( + this.current + && this.current.textContent + ) { + // In H1, H2, etc. the textContent can be set directly + this.current.textContent = `${latest}` + } else if ( this.current && this.current.firstChild && this.current.firstChild.nodeType === Node.TEXT_NODE ) { + // In SVG, the text can only be set through the value of the child text node this.current.firstChild.nodeValue = `${latest}` } }) From ed1bb9e340e73e73efd1a2d7977a60177b2cc82e Mon Sep 17 00:00:00 2001 From: Simon Karman Date: Wed, 23 Oct 2024 22:09:32 +0200 Subject: [PATCH 6/9] An example of providing a MotionValue to a component directly. Testing both a SVG text and HTML h1 element --- .../examples/SVG-Text-MotionValue-Child.tsx | 40 +++++++++++++++++++ .../src/render/html/HTMLVisualElement.ts | 13 +----- .../src/render/svg/SVGVisualElement.ts | 23 +++++++++++ 3 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 dev/react/src/examples/SVG-Text-MotionValue-Child.tsx diff --git a/dev/react/src/examples/SVG-Text-MotionValue-Child.tsx b/dev/react/src/examples/SVG-Text-MotionValue-Child.tsx new file mode 100644 index 0000000000..a6ec223d5d --- /dev/null +++ b/dev/react/src/examples/SVG-Text-MotionValue-Child.tsx @@ -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 (<> +

SVG

+ + + {rounded} + + +

HTML

+ {rounded} + {rounded} + ) +} diff --git a/packages/framer-motion/src/render/html/HTMLVisualElement.ts b/packages/framer-motion/src/render/html/HTMLVisualElement.ts index 220c875d88..f306d01129 100644 --- a/packages/framer-motion/src/render/html/HTMLVisualElement.ts +++ b/packages/framer-motion/src/render/html/HTMLVisualElement.ts @@ -79,19 +79,8 @@ export class HTMLVisualElement extends DOMVisualElement< const { children } = this.props if (isMotionValue(children)) { this.childSubscription = children.on("change", (latest) => { - if ( - this.current - && this.current.textContent - ) { - // In H1, H2, etc. the textContent can be set directly + if (this.current) { this.current.textContent = `${latest}` - } else if ( - this.current - && this.current.firstChild - && this.current.firstChild.nodeType === Node.TEXT_NODE - ) { - // In SVG, the text can only be set through the value of the child text node - this.current.firstChild.nodeValue = `${latest}` } }) } diff --git a/packages/framer-motion/src/render/svg/SVGVisualElement.ts b/packages/framer-motion/src/render/svg/SVGVisualElement.ts index 93681ebec8..dfcc4bb736 100644 --- a/packages/framer-motion/src/render/svg/SVGVisualElement.ts +++ b/packages/framer-motion/src/render/svg/SVGVisualElement.ts @@ -15,6 +15,7 @@ import { createBox } from "../../projection/geometry/models" import { IProjectionNode } from "../../projection/node/types" import { isSVGTag } from "./utils/is-svg-tag" import { VisualElement } from "../VisualElement" +import { isMotionValue } from "../../value/utils/is-motion-value" export class SVGVisualElement extends DOMVisualElement< SVGElement, @@ -77,4 +78,26 @@ export class SVGVisualElement extends DOMVisualElement< this.isSVGTag = isSVGTag(instance.tagName) super.mount(instance) } + + 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.firstChild + && this.current.firstChild.nodeType === Node.TEXT_NODE + ) { + // In SVG, the text can only be set through the value of the child text node + this.current.firstChild.nodeValue = `${latest}` + } + }) + } + } } From 33e6056d7265528d0890547cbee39849f0f2c3ae Mon Sep 17 00:00:00 2001 From: Simon Karman Date: Wed, 23 Oct 2024 22:11:10 +0200 Subject: [PATCH 7/9] SVG also supports updating through textContent --- .../framer-motion/src/render/svg/SVGVisualElement.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/framer-motion/src/render/svg/SVGVisualElement.ts b/packages/framer-motion/src/render/svg/SVGVisualElement.ts index dfcc4bb736..a9b50b6756 100644 --- a/packages/framer-motion/src/render/svg/SVGVisualElement.ts +++ b/packages/framer-motion/src/render/svg/SVGVisualElement.ts @@ -89,13 +89,8 @@ export class SVGVisualElement extends DOMVisualElement< const { children } = this.props if (isMotionValue(children)) { this.childSubscription = children.on("change", (latest) => { - if ( - this.current - && this.current.firstChild - && this.current.firstChild.nodeType === Node.TEXT_NODE - ) { - // In SVG, the text can only be set through the value of the child text node - this.current.firstChild.nodeValue = `${latest}` + if (this.current) { + this.current.textContent = `${latest}`; } }) } From 7cf844f01e82a74c4605ed2d8859eda0c12872a1 Mon Sep 17 00:00:00 2001 From: Simon Karman Date: Wed, 23 Oct 2024 22:13:30 +0200 Subject: [PATCH 8/9] Since the implementation in HTMLVisualElement and SVGVisualElement is now identical, move logic to DOMVisualElement --- .../src/render/dom/DOMVisualElement.ts | 18 ++++++++++++++++++ .../src/render/html/HTMLVisualElement.ts | 18 ------------------ .../src/render/svg/SVGVisualElement.ts | 18 ------------------ 3 files changed, 18 insertions(+), 36 deletions(-) diff --git a/packages/framer-motion/src/render/dom/DOMVisualElement.ts b/packages/framer-motion/src/render/dom/DOMVisualElement.ts index 67a168f696..28b4b211af 100644 --- a/packages/framer-motion/src/render/dom/DOMVisualElement.ts +++ b/packages/framer-motion/src/render/dom/DOMVisualElement.ts @@ -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, @@ -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}`; + } + }) + } + } } diff --git a/packages/framer-motion/src/render/html/HTMLVisualElement.ts b/packages/framer-motion/src/render/html/HTMLVisualElement.ts index f306d01129..8c43cd10f5 100644 --- a/packages/framer-motion/src/render/html/HTMLVisualElement.ts +++ b/packages/framer-motion/src/render/html/HTMLVisualElement.ts @@ -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" @@ -69,22 +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 } diff --git a/packages/framer-motion/src/render/svg/SVGVisualElement.ts b/packages/framer-motion/src/render/svg/SVGVisualElement.ts index a9b50b6756..93681ebec8 100644 --- a/packages/framer-motion/src/render/svg/SVGVisualElement.ts +++ b/packages/framer-motion/src/render/svg/SVGVisualElement.ts @@ -15,7 +15,6 @@ import { createBox } from "../../projection/geometry/models" import { IProjectionNode } from "../../projection/node/types" import { isSVGTag } from "./utils/is-svg-tag" import { VisualElement } from "../VisualElement" -import { isMotionValue } from "../../value/utils/is-motion-value" export class SVGVisualElement extends DOMVisualElement< SVGElement, @@ -78,21 +77,4 @@ export class SVGVisualElement extends DOMVisualElement< this.isSVGTag = isSVGTag(instance.tagName) super.mount(instance) } - - 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}`; - } - }) - } - } } From 21567c1ed41466b752e18affcf0a21f3cddcf3ba Mon Sep 17 00:00:00 2001 From: Simon Karman Date: Wed, 23 Oct 2024 22:20:44 +0200 Subject: [PATCH 9/9] Updated tests and changelog entry to reflect latest changes --- CHANGELOG.md | 2 +- .../__tests__/child-motion-value.test.tsx | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3567d5564..331157ca55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Undocumented APIs should be considered internal and may change without warning. ### Changed -- SVG `motion.text` and similar components now properly update when provided a `MotionValue` as children. +- SVG elements (like `motion.text`) now update when given a `MotionValue` as children, matching HTML element behavior. ## [11.11.9] 2024-10-15 diff --git a/packages/framer-motion/src/motion/__tests__/child-motion-value.test.tsx b/packages/framer-motion/src/motion/__tests__/child-motion-value.test.tsx index 3ef4b4d069..bb76b4c21a 100644 --- a/packages/framer-motion/src/motion/__tests__/child-motion-value.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/child-motion-value.test.tsx @@ -17,12 +17,12 @@ describe("child as motion value", () => { }) test("accepts motion values as children for motion.text inside an svg", async () => { - const promise = new Promise((resolve) => { + const promise = new Promise((resolve) => { const child = motionValue(3) const Component = () => {child} const { container, rerender } = render() rerender() - resolve(container.firstChild as HTMLDivElement) + resolve(container.firstChild?.firstChild as SVGTextElement) }) return expect(promise).resolves.toHaveTextContent("3") @@ -46,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((resolve) => { + const child = motionValue(3) + const Component = () => {child} + const { container, rerender } = render() + rerender() + + frame.postRender(() => { + child.set(4) + + frame.postRender(() => { + resolve(container.firstChild?.firstChild as SVGTextElement) + }) + }) + }) + + return expect(promise).resolves.toHaveTextContent("4") + }) })