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

Allow SVG attributes to animate to CSS variables #1366

Merged
merged 2 commits into from Nov 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,12 @@

Framer Motion adheres to [Semantic Versioning](http://semver.org/).

## [5.3.3] 2021-11-Unreleased

### Fixed

- Fixing animating to CSS variables with `SVGElement`. [Issue](https://github.com/framer/motion/issues/1334)

## [5.3.2] 2021-11-23

### Fixed
Expand Down
62 changes: 26 additions & 36 deletions cypress/integration/svg.ts
Expand Up @@ -5,12 +5,8 @@ describe("SVG", () => {
.get("[data-testid='rotate']")
.should(($rotate: any) => {
const rotate = $rotate[0] as SVGRectElement
const {
top,
left,
right,
bottom,
} = rotate.getBoundingClientRect()
const { top, left, right, bottom } =
rotate.getBoundingClientRect()
expect(Math.round(top)).to.equal(29)
expect(Math.round(left)).to.equal(29)
expect(Math.round(right)).to.equal(171)
Expand All @@ -19,12 +15,8 @@ describe("SVG", () => {
.get("[data-testid='scale']")
.should(($scale: any) => {
const scale = $scale[0] as SVGRectElement
const {
top,
left,
right,
bottom,
} = scale.getBoundingClientRect()
const { top, left, right, bottom } =
scale.getBoundingClientRect()
expect(top).to.equal(150)
expect(left).to.equal(0)
expect(right).to.equal(200)
Expand All @@ -33,12 +25,8 @@ describe("SVG", () => {
.get("[data-testid='translate']")
.should(($translate: any) => {
const translate = $translate[0] as SVGRectElement
const {
top,
left,
right,
bottom,
} = translate.getBoundingClientRect()
const { top, left, right, bottom } =
translate.getBoundingClientRect()
expect(top).to.equal(350)
expect(left).to.equal(150)
expect(right).to.equal(250)
Expand All @@ -51,12 +39,8 @@ describe("SVG", () => {
.get("[data-testid='rotate']")
.should(($rotate: any) => {
const rotate = $rotate[0] as SVGRectElement
const {
top,
left,
right,
bottom,
} = rotate.getBoundingClientRect()
const { top, left, right, bottom } =
rotate.getBoundingClientRect()
expect(Math.round(top)).to.equal(29)
expect(Math.round(left)).to.equal(29)
expect(Math.round(right)).to.equal(171)
Expand All @@ -65,12 +49,8 @@ describe("SVG", () => {
.get("[data-testid='scale']")
.should(($scale: any) => {
const scale = $scale[0] as SVGRectElement
const {
top,
left,
right,
bottom,
} = scale.getBoundingClientRect()
const { top, left, right, bottom } =
scale.getBoundingClientRect()
expect(top).to.equal(150)
expect(left).to.equal(0)
expect(right).to.equal(200)
Expand All @@ -79,16 +59,26 @@ describe("SVG", () => {
.get("[data-testid='translate']")
.should(($translate: any) => {
const translate = $translate[0] as SVGRectElement
const {
top,
left,
right,
bottom,
} = translate.getBoundingClientRect()
const { top, left, right, bottom } =
translate.getBoundingClientRect()
expect(top).to.equal(350)
expect(left).to.equal(150)
expect(right).to.equal(250)
expect(bottom).to.equal(450)
})
})

it("Correctly animates to CSS variables", () => {
cy.visit("?test=svg-css-vars")
.wait(50)
.get("svg")
.click()
.wait(50)
.get("circle")
.should(([$circle]: any) => {
expect($circle.getAttribute("fill")).to.equal(
"rgba(180, 0, 180, 1)"
)
})
})
})
29 changes: 29 additions & 0 deletions dev/tests/svg-css-vars.tsx
@@ -0,0 +1,29 @@
import * as React from "react"
import { motion } from "@framer"

/**
* An example of providing a MotionValue to an SVG component via its props
*/

export const App = () => {
const [state, setState] = React.useState(false)
return (
<svg
width="250"
height="250"
viewBox="0 0 250 250"
xmlns="http://www.w3.org/2000/svg"
onClick={() => setState(!state)}
style={{ "--color": "#f00" } as any}
>
<motion.circle
initial={false}
cx={125}
cy={125}
r="100"
animate={{ fill: state ? "var(--color)" : "#00f" }}
transition={{ duration: 3, ease: () => 0.5 }}
/>
</svg>
)
}
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "framer-motion",
"version": "5.3.1",
"version": "5.3.2",
"description": "A simple and powerful React animation library",
"main": "dist/framer-motion.cjs.js",
"module": "dist/es/index.mjs",
Expand Down
7 changes: 4 additions & 3 deletions src/render/dom/utils/css-variables-conversion.ts
Expand Up @@ -15,7 +15,8 @@ function isCSSVariable(value: any): value is string {
*
* @param current
*/
export const cssVariableRegex = /var\((--[a-zA-Z0-9-_]+),? ?([a-zA-Z0-9 ()%#.,-]+)?\)/
export const cssVariableRegex =
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also cover some unit tests for the regex?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The function that uses it, parseCSSVariable, is already covered by unit tests.

/var\((--[a-zA-Z0-9-_]+),? ?([a-zA-Z0-9 ()%#.,-]+)?\)/
export function parseCSSVariable(current: string) {
const match = cssVariableRegex.exec(current)
if (!match) return [,]
Expand All @@ -27,7 +28,7 @@ export function parseCSSVariable(current: string) {
const maxDepth = 4
function getVariableValue(
current: string,
element: HTMLElement,
element: Element,
depth = 1
): string | undefined {
invariant(
Expand Down Expand Up @@ -64,7 +65,7 @@ export function resolveCSSVariables(
transitionEnd: Target | undefined
): { target: TargetWithKeyframes; transitionEnd?: Target } {
const element = visualElement.getInstance()
if (!(element instanceof HTMLElement)) return { target, transitionEnd }
if (!(element instanceof Element)) return { target, transitionEnd }

// If `transitionEnd` isn't `undefined`, clone it. We could clone `target` and `transitionEnd`
// only if they change but I think this reads clearer and this isn't a performance-critical path.
Expand Down