Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/framer-motion/src/motion/__tests__/ssr.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ function runTests(render: (components: any) => string) {
)

expect(circle).toBe(
'<circle cx="100" stroke-width="10" pathLength="1" stroke-dashoffset="0px" stroke-dasharray="0.5px 1px" style="background:#fff;transform:translateX(100px);transform-origin:50% 50%;transform-box:fill-box"></circle>'
'<circle cx="100" stroke-width="10" pathLength="1" stroke-dashoffset="0" stroke-dasharray="0.5 1" style="background:#fff;transform:translateX(100px);transform-origin:50% 50%;transform-box:fill-box"></circle>'
)
const rect = render(
<AnimatePresence>
Expand Down
4 changes: 2 additions & 2 deletions packages/framer-motion/src/motion/__tests__/svg-path.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ describe("SVG path", () => {
rerender(<Component />)
})

expect(element).toHaveAttribute("stroke-dashoffset", "0px")
expect(element).toHaveAttribute("stroke-dasharray", "1px 1px")
expect(element).toHaveAttribute("stroke-dashoffset", "0")
expect(element).toHaveAttribute("stroke-dasharray", "1 1")
expect(element).toHaveAttribute("pathLength", "1")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ describe("SVG useProps", () => {
)
)

// Uses unitless values to avoid Safari zoom bug
expect(result.current).toStrictEqual({
pathLength: 1,
strokeDasharray: "0.5px 1px",
strokeDashoffset: "0px",
strokeDasharray: "0.5 1",
strokeDashoffset: "0",
style: {},
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import "../../../../jest.setup"
describe("buildSVGPath", () => {
it("correctly generates SVG path props", () => {
const attrs: {
["stroke-dashoffset"]?: number
["stroke-dasharray"]?: number
["stroke-dashoffset"]?: string
["stroke-dasharray"]?: string
} = {}

buildSVGPath(attrs, 0.5, 0.25, 0.25)

expect(attrs["stroke-dashoffset"]).toBe("-0.25px")
expect(attrs["stroke-dasharray"]).toBe("0.5px 0.25px")
// Uses unitless values to avoid Safari zoom bug
expect(attrs["stroke-dashoffset"]).toBe("-0.25")
expect(attrs["stroke-dasharray"]).toBe("0.5 0.25")
})
})
24 changes: 12 additions & 12 deletions packages/motion-dom/src/effects/__tests__/svg-effect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,10 @@ describe("svgEffect", () => {

await nextFrame()

// Verify initial path properties
// Verify initial path properties (uses unitless values to avoid Safari zoom bug)
expect(element.getAttribute("pathLength")).toBe("1")
expect(element.getAttribute("stroke-dashoffset")).toBe("-0.5px")
expect(element.getAttribute("stroke-dasharray")).toBe("2px 1px")
expect(element.getAttribute("stroke-dashoffset")).toBe("-0.5")
expect(element.getAttribute("stroke-dasharray")).toBe("2 1")

// Update values
pathOffset.set("0.25")
Expand All @@ -225,8 +225,8 @@ describe("svgEffect", () => {

// Verify updated path properties
expect(element.getAttribute("pathLength")).toBe("1")
expect(element.getAttribute("stroke-dashoffset")).toBe("-0.25px")
expect(element.getAttribute("stroke-dasharray")).toBe("3px 2px")
expect(element.getAttribute("stroke-dashoffset")).toBe("-0.25")
expect(element.getAttribute("stroke-dasharray")).toBe("3 2")
})

it("handles path properties with cleanup", async () => {
Expand All @@ -247,9 +247,9 @@ describe("svgEffect", () => {

await nextFrame()

// Verify initial values
expect(element.getAttribute("stroke-dashoffset")).toBe("-0.5px")
expect(element.getAttribute("stroke-dasharray")).toBe("2px 1px")
// Verify initial values (uses unitless values to avoid Safari zoom bug)
expect(element.getAttribute("stroke-dashoffset")).toBe("-0.5")
expect(element.getAttribute("stroke-dasharray")).toBe("2 1")

// Update values
pathOffset.set("0.25")
Expand All @@ -259,8 +259,8 @@ describe("svgEffect", () => {
await nextFrame()

// Verify updates
expect(element.getAttribute("stroke-dashoffset")).toBe("-0.25px")
expect(element.getAttribute("stroke-dasharray")).toBe("3px 2px")
expect(element.getAttribute("stroke-dashoffset")).toBe("-0.25")
expect(element.getAttribute("stroke-dasharray")).toBe("3 2")

// Cleanup
cleanup()
Expand All @@ -273,7 +273,7 @@ describe("svgEffect", () => {
await nextFrame()

// Verify values didn't change after cleanup
expect(element.getAttribute("stroke-dashoffset")).toBe("-0.25px")
expect(element.getAttribute("stroke-dasharray")).toBe("3px 2px")
expect(element.getAttribute("stroke-dashoffset")).toBe("-0.25")
expect(element.getAttribute("stroke-dasharray")).toBe("3 2")
})
})
16 changes: 7 additions & 9 deletions packages/motion-dom/src/effects/svg/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { frame } from "../../frameloop"
import { MotionValue } from "../../value"
import { px } from "../../value/types/numbers/units"
import { addAttrValue } from "../attr"
import { MotionValueState } from "../MotionValueState"
import { addStyleValue } from "../style"
import { createSelectorEffect } from "../utils/create-dom-effect"
import { createEffect } from "../utils/create-effect"

const toPx = px.transform!

function addSVGPathValue(
element: SVGElement,
state: MotionValueState,
Expand All @@ -18,19 +15,20 @@ function addSVGPathValue(
frame.render(() => element.setAttribute("pathLength", "1"))

if (key === "pathOffset") {
return state.set(key, value, () =>
element.setAttribute("stroke-dashoffset", toPx(-state.latest[key]))
)
return state.set(key, value, () => {
// Use unitless value to avoid Safari zoom bug
const offset = state.latest[key]
element.setAttribute("stroke-dashoffset", `${-offset}`)
})
} else {
if (!state.get("stroke-dasharray")) {
state.set("stroke-dasharray", new MotionValue("1 1"), () => {
const { pathLength = 1, pathSpacing } = state.latest

// Use unitless values to avoid Safari zoom bug
element.setAttribute(
"stroke-dasharray",
`${toPx(pathLength)} ${toPx(
pathSpacing ?? 1 - Number(pathLength)
)}`
`${pathLength} ${pathSpacing ?? 1 - Number(pathLength)}`
)
})
}
Expand Down
14 changes: 7 additions & 7 deletions packages/motion-dom/src/render/svg/utils/path.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { px } from "../../../value/types/numbers/units"
import { ResolvedValues } from "../../types"

const dashKeys = {
Expand All @@ -17,6 +16,9 @@ const camelKeys = {
* and stroke-dasharray attributes.
*
* This function is mutative to reduce per-frame GC.
*
* Note: We use unitless values for stroke-dasharray and stroke-dashoffset
* because Safari incorrectly scales px values when the page is zoomed.
*/
export function buildSVGPath(
attrs: ResolvedValues,
Expand All @@ -32,11 +34,9 @@ export function buildSVGPath(
// when defining props on a React component.
const keys = useDashCase ? dashKeys : camelKeys

// Build the dash offset
attrs[keys.offset] = px.transform!(-offset)
// Build the dash offset (unitless to avoid Safari zoom bug)
attrs[keys.offset] = `${-offset}`

// Build the dash array
const pathLength = px.transform!(length)
const pathSpacing = px.transform!(spacing)
attrs[keys.array] = `${pathLength} ${pathSpacing}`
// Build the dash array (unitless to avoid Safari zoom bug)
attrs[keys.array] = `${length} ${spacing}`
}
10 changes: 6 additions & 4 deletions tests/effects/svg.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { expect, test } from "@playwright/test"

test.describe("svgEffect", () => {
// Uses unitless values for stroke-dasharray and stroke-dashoffset
// to avoid Safari zoom bug where px values are incorrectly scaled
test("draws pathLength as stroke-dasharray", async ({ page }) => {
await page.goto("effects/path-length.html")
const path = page.locator("#tick")
const strokeDasharray = await path.getAttribute("stroke-dasharray")
expect(strokeDasharray).toBe("0.25px 0.75px")
expect(strokeDasharray).toBe("0.25 0.75")
const pathLength = await path.getAttribute("pathLength")
expect(pathLength).toBe("1")
})
Expand All @@ -14,7 +16,7 @@ test.describe("svgEffect", () => {
await page.goto("effects/path-offset.html")
const path = page.locator("#tick")
const strokeDashoffset = await path.getAttribute("stroke-dashoffset")
expect(strokeDashoffset).toBe("-0.5px")
expect(strokeDashoffset).toBe("-0.5")
})

test("ensures default pathSpacing correctly creates looping effect by calculating remaining amount", async ({
Expand All @@ -23,9 +25,9 @@ test.describe("svgEffect", () => {
await page.goto("effects/path-offset-loop.html")
const circle = page.locator("#circle")
const strokeDasharray = await circle.getAttribute("stroke-dasharray")
expect(strokeDasharray).toBe("0.5px 0.5px")
expect(strokeDasharray).toBe("0.5 0.5")
const strokeDashoffset = await circle.getAttribute("stroke-dashoffset")
expect(strokeDashoffset).toBe("-0.75px")
expect(strokeDashoffset).toBe("-0.75")
})

test("draws attrX as x", async ({ page }) => {
Expand Down