Skip to content

Commit 5aa49a6

Browse files
Merge pull request #2321 from framer/fix/svg-scroll
Fixing SVG as scroll target
2 parents be6ef73 + 6bc5175 commit 5aa49a6

File tree

4 files changed

+131
-20
lines changed

4 files changed

+131
-20
lines changed

dev/tests/scroll-svg.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as React from "react"
2+
import { useRef } from "react"
3+
import { motion, useScroll } from "framer-motion"
4+
5+
export const App = () => {
6+
const rect = useRef(null)
7+
const svg = useRef(null)
8+
9+
const rectValues = useScroll({
10+
target: rect,
11+
offset: ["start end", "end start"],
12+
})
13+
14+
const svgValues = useScroll({
15+
target: svg,
16+
offset: ["start end", "end start"],
17+
})
18+
19+
return (
20+
<>
21+
<div style={{ paddingTop: 400, paddingBottom: 400 }}>
22+
<svg ref={svg} viewBox="0 0 200 200" width="200" height="200">
23+
<rect
24+
ref={rect}
25+
width="100"
26+
height="100"
27+
x="50"
28+
y="50"
29+
fill="red"
30+
/>
31+
</svg>
32+
</div>
33+
<motion.div style={{ ...fixed }} id="rect-progress">
34+
{rectValues.scrollYProgress}
35+
</motion.div>
36+
<motion.div style={{ ...fixed, top: 50 }} id="svg-progress">
37+
{svgValues.scrollYProgress}
38+
</motion.div>
39+
</>
40+
)
41+
}
42+
43+
const fixed: React.CSSProperties = {
44+
position: "fixed",
45+
top: 10,
46+
left: 10,
47+
}

packages/framer-motion/cypress/integration/scroll.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,43 @@ describe("scroll() animation", () => {
114114
})
115115
})
116116
})
117+
118+
describe("SVG", () => {
119+
it("tracks SVG elements as target", () => {
120+
cy.visit("?test=scroll-svg").wait(100).viewport(100, 400)
121+
cy.get("#rect-progress").should(([$element]: any) => {
122+
expect($element.innerText).to.be("0")
123+
})
124+
cy.get("#svg-progress").should(([$element]: any) => {
125+
expect($element.innerText).to.be("0")
126+
})
127+
cy.scrollTo(0, 25)
128+
cy.get("#rect-progress").should(([$element]: any) => {
129+
expect($element.innerText).not.to.be("0")
130+
})
131+
cy.get("#svg-progress").should(([$element]: any) => {
132+
expect($element.innerText).to.be("0")
133+
})
134+
cy.scrollTo(0, 75)
135+
cy.get("#rect-progress").should(([$element]: any) => {
136+
expect($element.innerText).not.to.be("0")
137+
})
138+
cy.get("#svg-progress").should(([$element]: any) => {
139+
expect($element.innerText).not.to.be("0")
140+
})
141+
cy.scrollTo(0, 500)
142+
cy.get("#rect-progress").should(([$element]: any) => {
143+
expect($element.innerText).not.to.be("1")
144+
})
145+
cy.get("#svg-progress").should(([$element]: any) => {
146+
expect($element.innerText).not.to.be("1")
147+
})
148+
cy.scrollTo(0, 600)
149+
cy.get("#rect-progress").should(([$element]: any) => {
150+
expect($element.innerText).to.be("1")
151+
})
152+
cy.get("#svg-progress").should(([$element]: any) => {
153+
expect($element.innerText).to.be("1")
154+
})
155+
})
156+
})

packages/framer-motion/src/render/dom/scroll/offsets/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import { defaultOffset } from "../../../../utils/offsets/default"
88

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

11+
function getTargetSize(target: Element) {
12+
return "getBBox" in target && target.tagName !== "svg"
13+
? (target as SVGGraphicsElement).getBBox()
14+
: { width: target.clientWidth, height: target.clientHeight }
15+
}
16+
1117
export function resolveOffsets(
1218
container: HTMLElement,
1319
info: ScrollInfo,
@@ -27,7 +33,7 @@ export function resolveOffsets(
2733
const targetSize =
2834
target === container
2935
? { width: container.scrollWidth, height: container.scrollHeight }
30-
: { width: target.clientWidth, height: target.clientHeight }
36+
: getTargetSize(target)
3137

3238
const containerSize = {
3339
width: container.clientWidth,
Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,43 @@
11
export function calcInset(element: Element, container: HTMLElement) {
2-
let inset = { x: 0, y: 0 }
2+
const inset = { x: 0, y: 0 }
33

4-
let current: Element | null = element
5-
while (current && current !== container) {
6-
if (current instanceof HTMLElement) {
7-
inset.x += current.offsetLeft
8-
inset.y += current.offsetTop
9-
current = current.offsetParent
10-
} else if (current instanceof SVGGraphicsElement && "getBBox" in current) {
11-
const { top, left } = current.getBBox()
12-
inset.x += left
13-
inset.y += top
4+
let current: Element | null = element
5+
while (current && current !== container) {
6+
if (current instanceof HTMLElement) {
7+
inset.x += current.offsetLeft
8+
inset.y += current.offsetTop
9+
current = current.offsetParent
10+
} else if (current.tagName === "svg") {
11+
/**
12+
* This isn't an ideal approach to measuring the offset of <svg /> tags.
13+
* It would be preferable, given they behave like HTMLElements in most ways
14+
* to use offsetLeft/Top. But these don't exist on <svg />. Likewise we
15+
* can't use .getBBox() like most SVG elements as these provide the offset
16+
* relative to the SVG itself, which for <svg /> is usually 0x0.
17+
*/
18+
const svgBoundingBox = current.getBoundingClientRect()
19+
current = current.parentElement!
20+
const parentBoundingBox = current.getBoundingClientRect()
21+
inset.x += svgBoundingBox.left - parentBoundingBox.left
22+
inset.y += svgBoundingBox.top - parentBoundingBox.top
23+
} else if (current instanceof SVGGraphicsElement) {
24+
const { x, y } = current.getBBox()
25+
inset.x += x
26+
inset.y += y
1427

15-
/**
16-
* Assign the next parent element as the <svg /> tag.
17-
*/
18-
while (current && current.tagName !== "svg") {
19-
current = current.parentNode as SVGElement
20-
}
28+
let svg: SVGElement | null = null
29+
let parent: SVGElement = current.parentNode as SVGElement
30+
while (!svg) {
31+
if (parent.tagName === "svg") {
32+
svg = parent
33+
}
34+
parent = current.parentNode as SVGElement
35+
}
36+
current = svg
37+
} else {
38+
break
39+
}
2140
}
22-
}
2341

24-
return inset
42+
return inset
2543
}

0 commit comments

Comments
 (0)