Skip to content

Commit 79a4097

Browse files
authored
Merge pull request #3555 from motiondivision/scroll-acc
Fix useScroll accelerate when target is provided
2 parents 30a30ce + dbb66c9 commit 79a4097

8 files changed

Lines changed: 213 additions & 32 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { motion, useScroll, useTransform } from "framer-motion"
2+
import * as React from "react"
3+
import { useRef } from "react"
4+
5+
export const App = () => {
6+
const targetRef = useRef<HTMLDivElement>(null)
7+
const { scrollYProgress } = useScroll({
8+
target: targetRef,
9+
offset: ["start end", "end start"],
10+
})
11+
12+
const opacity = useTransform(scrollYProgress, [0, 1], [1, 0])
13+
const y = useTransform(scrollYProgress, [0, 1], [0, -100])
14+
15+
return (
16+
<>
17+
<div style={spacer} />
18+
<div ref={targetRef} style={targetStyle}>
19+
<motion.div
20+
id="target"
21+
style={{ ...box, opacity, y }}
22+
/>
23+
</div>
24+
<div style={spacer} />
25+
<div style={spacer} />
26+
<span id="has-accelerate">
27+
{scrollYProgress.accelerate ? "true" : "false"}
28+
</span>
29+
</>
30+
)
31+
}
32+
33+
const spacer = { height: "100vh" }
34+
const targetStyle: React.CSSProperties = {
35+
height: "100vh",
36+
display: "flex",
37+
alignItems: "center",
38+
justifyContent: "center",
39+
}
40+
const box: React.CSSProperties = {
41+
width: 100,
42+
height: 100,
43+
background: "red",
44+
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ describe("scroll timeline WAAPI acceleration", () => {
44
.wait(200)
55
.get("#direct-accelerated")
66
.should(([$el]: any) => {
7-
expect($el.innerText).to.equal("true")
7+
const expected = (window as any).ScrollTimeline
8+
? "true"
9+
: "false"
10+
expect($el.innerText).to.equal(expected)
811
})
912
})
1013

@@ -16,7 +19,10 @@ describe("scroll timeline WAAPI acceleration", () => {
1619
// backgroundColor gets accelerate config propagated,
1720
// but VisualElement skips WAAPI creation since it's
1821
// not in the acceleratedValues set
19-
expect($el.innerText).to.equal("true")
22+
const expected = (window as any).ScrollTimeline
23+
? "true"
24+
: "false"
25+
expect($el.innerText).to.equal(expected)
2026
})
2127
})
2228

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
describe("useScroll with target does not set accelerate", () => {
2+
it("Does not set accelerate when target is provided", () => {
3+
cy.visit("?test=scroll-target-transform")
4+
.wait(200)
5+
.get("#has-accelerate")
6+
.should(([$el]: any) => {
7+
expect($el.innerText).to.equal("false")
8+
})
9+
})
10+
11+
it("Opacity updates via useTransform when scrolling", () => {
12+
cy.visit("?test=scroll-target-transform")
13+
.wait(200)
14+
.get("#target")
15+
.should(([$el]: any) => {
16+
// Before scrolling, opacity should be near initial value
17+
const initialOpacity = parseFloat(
18+
getComputedStyle($el).opacity
19+
)
20+
expect(initialOpacity).to.be.greaterThan(0)
21+
})
22+
cy.scrollTo("bottom", { duration: 300 })
23+
.wait(200)
24+
.get("#target")
25+
.should(([$el]: any) => {
26+
// After scrolling, opacity should have changed
27+
const opacity = parseFloat(getComputedStyle($el).opacity)
28+
expect(opacity).to.be.lessThan(1)
29+
})
30+
})
31+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { supportsScrollTimeline } from "motion-dom"
2+
3+
export function canUseNativeTimeline(target?: Element) {
4+
return (
5+
typeof window !== "undefined" && !target && supportsScrollTimeline()
6+
)
7+
}

packages/framer-motion/src/render/dom/scroll/utils/get-timeline.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { ProgressTimeline, supportsScrollTimeline } from "motion-dom"
1+
import { ProgressTimeline } from "motion-dom"
22
import { scrollInfo } from "../track"
33
import { ScrollOptionsWithDefaults } from "../types"
4+
import { canUseNativeTimeline } from "./can-use-native-timeline"
45

56
declare global {
67
interface Window {
@@ -50,7 +51,7 @@ export function getTimeline({
5051

5152
if (!targetCache[axisKey]) {
5253
targetCache[axisKey] =
53-
!options.target && supportsScrollTimeline()
54+
canUseNativeTimeline(options.target)
5455
? new ScrollTimeline({ source: container, axis } as any)
5556
: scrollTimelineFallback({ container, ...options })
5657
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { supportsFlags } from "motion-dom"
2+
import { useRef } from "react"
3+
import { render } from "../../jest.setup"
4+
import { useScroll } from "../use-scroll"
5+
import { useTransform } from "../use-transform"
6+
7+
describe("useScroll accelerate", () => {
8+
afterEach(() => {
9+
supportsFlags.scrollTimeline = undefined
10+
})
11+
12+
test("sets accelerate on progress values when ScrollTimeline is supported and no target", () => {
13+
supportsFlags.scrollTimeline = true
14+
15+
let accelerateX: any
16+
let accelerateY: any
17+
18+
const Component = () => {
19+
const { scrollXProgress, scrollYProgress } = useScroll()
20+
accelerateX = scrollXProgress.accelerate
21+
accelerateY = scrollYProgress.accelerate
22+
return null
23+
}
24+
25+
render(<Component />)
26+
27+
expect(accelerateX).toBeDefined()
28+
expect(accelerateY).toBeDefined()
29+
})
30+
31+
test("does not set accelerate when target ref is provided", () => {
32+
supportsFlags.scrollTimeline = true
33+
34+
let accelerateX: any
35+
let accelerateY: any
36+
37+
const Component = () => {
38+
const target = useRef<HTMLDivElement>(null)
39+
const { scrollXProgress, scrollYProgress } = useScroll({
40+
target,
41+
})
42+
accelerateX = scrollXProgress.accelerate
43+
accelerateY = scrollYProgress.accelerate
44+
return <div ref={target} />
45+
}
46+
47+
render(<Component />)
48+
49+
expect(accelerateX).toBeUndefined()
50+
expect(accelerateY).toBeUndefined()
51+
})
52+
53+
test("does not set accelerate when ScrollTimeline is not supported", () => {
54+
supportsFlags.scrollTimeline = false
55+
56+
let accelerateX: any
57+
let accelerateY: any
58+
59+
const Component = () => {
60+
const { scrollXProgress, scrollYProgress } = useScroll()
61+
accelerateX = scrollXProgress.accelerate
62+
accelerateY = scrollYProgress.accelerate
63+
return null
64+
}
65+
66+
render(<Component />)
67+
68+
expect(accelerateX).toBeUndefined()
69+
expect(accelerateY).toBeUndefined()
70+
})
71+
72+
test("propagates accelerate through useTransform", () => {
73+
supportsFlags.scrollTimeline = true
74+
75+
let transformAccelerate: any
76+
77+
const Component = () => {
78+
const { scrollYProgress } = useScroll()
79+
const opacity = useTransform(scrollYProgress, [0, 1], [0, 1])
80+
transformAccelerate = opacity.accelerate
81+
return null
82+
}
83+
84+
render(<Component />)
85+
86+
expect(transformAccelerate).toBeDefined()
87+
})
88+
})

packages/framer-motion/src/value/use-scroll.ts

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { invariant } from "motion-utils"
55
import { RefObject, useCallback, useEffect, useRef } from "react"
66
import { scroll } from "../render/dom/scroll"
77
import { ScrollInfoOptions } from "../render/dom/scroll/types"
8+
import { canUseNativeTimeline } from "../render/dom/scroll/utils/can-use-native-timeline"
89
import { useConstant } from "../utils/use-constant"
910
import { useIsomorphicLayoutEffect } from "../utils/use-isomorphic-effect"
1011

@@ -26,38 +27,40 @@ const isRefPending = (ref?: RefObject<HTMLElement | null>) => {
2627
return !ref.current
2728
}
2829

30+
function makeAccelerateConfig(
31+
axis: "x" | "y",
32+
options: Omit<UseScrollOptions, "container" | "target">,
33+
container?: Element
34+
) {
35+
return {
36+
factory: (animation: AnimationPlaybackControls) =>
37+
scroll(animation, { ...options, axis, container }),
38+
times: [0, 1],
39+
keyframes: [0, 1],
40+
ease: (v: number) => v,
41+
duration: 1,
42+
}
43+
}
44+
2945
export function useScroll({
3046
container,
3147
target,
3248
...options
3349
}: UseScrollOptions = {}) {
3450
const values = useConstant(createScrollMotionValues)
3551

36-
values.scrollXProgress.accelerate = {
37-
factory: (animation: AnimationPlaybackControls) =>
38-
scroll(animation, {
39-
...options,
40-
axis: "x",
41-
container: container?.current || undefined,
42-
target: target?.current || undefined,
43-
}),
44-
times: [0, 1],
45-
keyframes: [0, 1],
46-
ease: (v: number) => v,
47-
duration: 1,
48-
}
49-
values.scrollYProgress.accelerate = {
50-
factory: (animation: AnimationPlaybackControls) =>
51-
scroll(animation, {
52-
...options,
53-
axis: "y",
54-
container: container?.current || undefined,
55-
target: target?.current || undefined,
56-
}),
57-
times: [0, 1],
58-
keyframes: [0, 1],
59-
ease: (v: number) => v,
60-
duration: 1,
52+
if (!target && canUseNativeTimeline()) {
53+
const resolvedContainer = container?.current || undefined
54+
values.scrollXProgress.accelerate = makeAccelerateConfig(
55+
"x",
56+
options,
57+
resolvedContainer
58+
)
59+
values.scrollYProgress.accelerate = makeAccelerateConfig(
60+
"y",
61+
options,
62+
resolvedContainer
63+
)
6164
}
6265

6366
const scrollAnimation = useRef<VoidFunction | null>(null)

packages/motion-dom/src/utils/supports/scroll-timeline.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { memo } from "motion-utils"
21
import { ProgressTimeline } from "../.."
2+
import { memoSupports } from "./memo"
33

44
declare global {
55
interface Window {
@@ -15,6 +15,7 @@ declare class ScrollTimeline implements ProgressTimeline {
1515
cancel?: VoidFunction
1616
}
1717

18-
export const supportsScrollTimeline = /* @__PURE__ */ memo(
19-
() => window.ScrollTimeline !== undefined
18+
export const supportsScrollTimeline = /* @__PURE__ */ memoSupports(
19+
() => window.ScrollTimeline !== undefined,
20+
"scrollTimeline"
2021
)

0 commit comments

Comments
 (0)