Skip to content

Commit

Permalink
Merge pull request #2321 from framer/fix/svg-scroll
Browse files Browse the repository at this point in the history
Fixing SVG as scroll target
  • Loading branch information
mergetron[bot] authored Aug 30, 2023
2 parents be6ef73 + 6bc5175 commit 5aa49a6
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 20 deletions.
47 changes: 47 additions & 0 deletions dev/tests/scroll-svg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from "react"
import { useRef } from "react"
import { motion, useScroll } from "framer-motion"

export const App = () => {
const rect = useRef(null)
const svg = useRef(null)

const rectValues = useScroll({
target: rect,
offset: ["start end", "end start"],
})

const svgValues = useScroll({
target: svg,
offset: ["start end", "end start"],
})

return (
<>
<div style={{ paddingTop: 400, paddingBottom: 400 }}>
<svg ref={svg} viewBox="0 0 200 200" width="200" height="200">
<rect
ref={rect}
width="100"
height="100"
x="50"
y="50"
fill="red"
/>
</svg>
</div>
<motion.div style={{ ...fixed }} id="rect-progress">
{rectValues.scrollYProgress}
</motion.div>
<motion.div style={{ ...fixed, top: 50 }} id="svg-progress">
{svgValues.scrollYProgress}
</motion.div>
</>
)
}

const fixed: React.CSSProperties = {
position: "fixed",
top: 10,
left: 10,
}
40 changes: 40 additions & 0 deletions packages/framer-motion/cypress/integration/scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,43 @@ describe("scroll() animation", () => {
})
})
})

describe("SVG", () => {
it("tracks SVG elements as target", () => {
cy.visit("?test=scroll-svg").wait(100).viewport(100, 400)
cy.get("#rect-progress").should(([$element]: any) => {
expect($element.innerText).to.be("0")
})
cy.get("#svg-progress").should(([$element]: any) => {
expect($element.innerText).to.be("0")
})
cy.scrollTo(0, 25)
cy.get("#rect-progress").should(([$element]: any) => {
expect($element.innerText).not.to.be("0")
})
cy.get("#svg-progress").should(([$element]: any) => {
expect($element.innerText).to.be("0")
})
cy.scrollTo(0, 75)
cy.get("#rect-progress").should(([$element]: any) => {
expect($element.innerText).not.to.be("0")
})
cy.get("#svg-progress").should(([$element]: any) => {
expect($element.innerText).not.to.be("0")
})
cy.scrollTo(0, 500)
cy.get("#rect-progress").should(([$element]: any) => {
expect($element.innerText).not.to.be("1")
})
cy.get("#svg-progress").should(([$element]: any) => {
expect($element.innerText).not.to.be("1")
})
cy.scrollTo(0, 600)
cy.get("#rect-progress").should(([$element]: any) => {
expect($element.innerText).to.be("1")
})
cy.get("#svg-progress").should(([$element]: any) => {
expect($element.innerText).to.be("1")
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import { defaultOffset } from "../../../../utils/offsets/default"

const point = { x: 0, y: 0 }

function getTargetSize(target: Element) {
return "getBBox" in target && target.tagName !== "svg"
? (target as SVGGraphicsElement).getBBox()
: { width: target.clientWidth, height: target.clientHeight }
}

export function resolveOffsets(
container: HTMLElement,
info: ScrollInfo,
Expand All @@ -27,7 +33,7 @@ export function resolveOffsets(
const targetSize =
target === container
? { width: container.scrollWidth, height: container.scrollHeight }
: { width: target.clientWidth, height: target.clientHeight }
: getTargetSize(target)

const containerSize = {
width: container.clientWidth,
Expand Down
56 changes: 37 additions & 19 deletions packages/framer-motion/src/render/dom/scroll/offsets/inset.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
export function calcInset(element: Element, container: HTMLElement) {
let inset = { x: 0, y: 0 }
const inset = { x: 0, y: 0 }

let current: Element | null = element
while (current && current !== container) {
if (current instanceof HTMLElement) {
inset.x += current.offsetLeft
inset.y += current.offsetTop
current = current.offsetParent
} else if (current instanceof SVGGraphicsElement && "getBBox" in current) {
const { top, left } = current.getBBox()
inset.x += left
inset.y += top
let current: Element | null = element
while (current && current !== container) {
if (current instanceof HTMLElement) {
inset.x += current.offsetLeft
inset.y += current.offsetTop
current = current.offsetParent
} else if (current.tagName === "svg") {
/**
* This isn't an ideal approach to measuring the offset of <svg /> tags.
* It would be preferable, given they behave like HTMLElements in most ways
* to use offsetLeft/Top. But these don't exist on <svg />. Likewise we
* can't use .getBBox() like most SVG elements as these provide the offset
* relative to the SVG itself, which for <svg /> is usually 0x0.
*/
const svgBoundingBox = current.getBoundingClientRect()
current = current.parentElement!
const parentBoundingBox = current.getBoundingClientRect()
inset.x += svgBoundingBox.left - parentBoundingBox.left
inset.y += svgBoundingBox.top - parentBoundingBox.top
} else if (current instanceof SVGGraphicsElement) {
const { x, y } = current.getBBox()
inset.x += x
inset.y += y

/**
* Assign the next parent element as the <svg /> tag.
*/
while (current && current.tagName !== "svg") {
current = current.parentNode as SVGElement
}
let svg: SVGElement | null = null
let parent: SVGElement = current.parentNode as SVGElement
while (!svg) {
if (parent.tagName === "svg") {
svg = parent
}
parent = current.parentNode as SVGElement
}
current = svg
} else {
break
}
}
}

return inset
return inset
}

0 comments on commit 5aa49a6

Please sign in to comment.