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
44 changes: 44 additions & 0 deletions dev/react/src/tests/scroll-target-transform.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { motion, useScroll, useTransform } from "framer-motion"
import * as React from "react"
import { useRef } from "react"

export const App = () => {
const targetRef = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: targetRef,
offset: ["start end", "end start"],
})

const opacity = useTransform(scrollYProgress, [0, 1], [1, 0])
const y = useTransform(scrollYProgress, [0, 1], [0, -100])

return (
<>
<div style={spacer} />
<div ref={targetRef} style={targetStyle}>
<motion.div
id="target"
style={{ ...box, opacity, y }}
/>
</div>
<div style={spacer} />
<div style={spacer} />
<span id="has-accelerate">
{scrollYProgress.accelerate ? "true" : "false"}
</span>
</>
)
}

const spacer = { height: "100vh" }
const targetStyle: React.CSSProperties = {
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
}
const box: React.CSSProperties = {
width: 100,
height: 100,
background: "red",
}
10 changes: 8 additions & 2 deletions packages/framer-motion/cypress/integration/scroll-accelerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ describe("scroll timeline WAAPI acceleration", () => {
.wait(200)
.get("#direct-accelerated")
.should(([$el]: any) => {
expect($el.innerText).to.equal("true")
const expected = (window as any).ScrollTimeline
? "true"
: "false"
expect($el.innerText).to.equal(expected)
})
})

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
describe("useScroll with target does not set accelerate", () => {
it("Does not set accelerate when target is provided", () => {
cy.visit("?test=scroll-target-transform")
.wait(200)
.get("#has-accelerate")
.should(([$el]: any) => {
expect($el.innerText).to.equal("false")
})
})

it("Opacity updates via useTransform when scrolling", () => {
cy.visit("?test=scroll-target-transform")
.wait(200)
.get("#target")
.should(([$el]: any) => {
// Before scrolling, opacity should be near initial value
const initialOpacity = parseFloat(
getComputedStyle($el).opacity
)
expect(initialOpacity).to.be.greaterThan(0)
})
cy.scrollTo("bottom", { duration: 300 })
.wait(200)
.get("#target")
.should(([$el]: any) => {
// After scrolling, opacity should have changed
const opacity = parseFloat(getComputedStyle($el).opacity)
expect(opacity).to.be.lessThan(1)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { supportsScrollTimeline } from "motion-dom"

export function canUseNativeTimeline(target?: Element) {
return (
typeof window !== "undefined" && !target && supportsScrollTimeline()
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ProgressTimeline, supportsScrollTimeline } from "motion-dom"
import { ProgressTimeline } from "motion-dom"
import { scrollInfo } from "../track"
import { ScrollOptionsWithDefaults } from "../types"
import { canUseNativeTimeline } from "./can-use-native-timeline"

declare global {
interface Window {
Expand Down Expand Up @@ -50,7 +51,7 @@ export function getTimeline({

if (!targetCache[axisKey]) {
targetCache[axisKey] =
!options.target && supportsScrollTimeline()
canUseNativeTimeline(options.target)
? new ScrollTimeline({ source: container, axis } as any)
: scrollTimelineFallback({ container, ...options })
}
Expand Down
88 changes: 88 additions & 0 deletions packages/framer-motion/src/value/__tests__/use-scroll.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { supportsFlags } from "motion-dom"
import { useRef } from "react"
import { render } from "../../jest.setup"
import { useScroll } from "../use-scroll"
import { useTransform } from "../use-transform"

describe("useScroll accelerate", () => {
afterEach(() => {
supportsFlags.scrollTimeline = undefined
})

test("sets accelerate on progress values when ScrollTimeline is supported and no target", () => {
supportsFlags.scrollTimeline = true

let accelerateX: any
let accelerateY: any

const Component = () => {
const { scrollXProgress, scrollYProgress } = useScroll()
accelerateX = scrollXProgress.accelerate
accelerateY = scrollYProgress.accelerate
return null
}

render(<Component />)

expect(accelerateX).toBeDefined()
expect(accelerateY).toBeDefined()
})

test("does not set accelerate when target ref is provided", () => {
supportsFlags.scrollTimeline = true

let accelerateX: any
let accelerateY: any

const Component = () => {
const target = useRef<HTMLDivElement>(null)
const { scrollXProgress, scrollYProgress } = useScroll({
target,
})
accelerateX = scrollXProgress.accelerate
accelerateY = scrollYProgress.accelerate
return <div ref={target} />
}

render(<Component />)

expect(accelerateX).toBeUndefined()
expect(accelerateY).toBeUndefined()
})

test("does not set accelerate when ScrollTimeline is not supported", () => {
supportsFlags.scrollTimeline = false

let accelerateX: any
let accelerateY: any

const Component = () => {
const { scrollXProgress, scrollYProgress } = useScroll()
accelerateX = scrollXProgress.accelerate
accelerateY = scrollYProgress.accelerate
return null
}

render(<Component />)

expect(accelerateX).toBeUndefined()
expect(accelerateY).toBeUndefined()
})

test("propagates accelerate through useTransform", () => {
supportsFlags.scrollTimeline = true

let transformAccelerate: any

const Component = () => {
const { scrollYProgress } = useScroll()
const opacity = useTransform(scrollYProgress, [0, 1], [0, 1])
transformAccelerate = opacity.accelerate
return null
}

render(<Component />)

expect(transformAccelerate).toBeDefined()
})
})
53 changes: 28 additions & 25 deletions packages/framer-motion/src/value/use-scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { invariant } from "motion-utils"
import { RefObject, useCallback, useEffect, useRef } from "react"
import { scroll } from "../render/dom/scroll"
import { ScrollInfoOptions } from "../render/dom/scroll/types"
import { canUseNativeTimeline } from "../render/dom/scroll/utils/can-use-native-timeline"
import { useConstant } from "../utils/use-constant"
import { useIsomorphicLayoutEffect } from "../utils/use-isomorphic-effect"

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

function makeAccelerateConfig(
axis: "x" | "y",
options: Omit<UseScrollOptions, "container" | "target">,
container?: Element
) {
return {
factory: (animation: AnimationPlaybackControls) =>
scroll(animation, { ...options, axis, container }),
times: [0, 1],
keyframes: [0, 1],
ease: (v: number) => v,
duration: 1,
}
}

export function useScroll({
container,
target,
...options
}: UseScrollOptions = {}) {
const values = useConstant(createScrollMotionValues)

values.scrollXProgress.accelerate = {
factory: (animation: AnimationPlaybackControls) =>
scroll(animation, {
...options,
axis: "x",
container: container?.current || undefined,
target: target?.current || undefined,
}),
times: [0, 1],
keyframes: [0, 1],
ease: (v: number) => v,
duration: 1,
}
values.scrollYProgress.accelerate = {
factory: (animation: AnimationPlaybackControls) =>
scroll(animation, {
...options,
axis: "y",
container: container?.current || undefined,
target: target?.current || undefined,
}),
times: [0, 1],
keyframes: [0, 1],
ease: (v: number) => v,
duration: 1,
if (!target && canUseNativeTimeline()) {
const resolvedContainer = container?.current || undefined
values.scrollXProgress.accelerate = makeAccelerateConfig(
"x",
options,
resolvedContainer
)
values.scrollYProgress.accelerate = makeAccelerateConfig(
"y",
options,
resolvedContainer
)
}

const scrollAnimation = useRef<VoidFunction | null>(null)
Expand Down
7 changes: 4 additions & 3 deletions packages/motion-dom/src/utils/supports/scroll-timeline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { memo } from "motion-utils"
import { ProgressTimeline } from "../.."
import { memoSupports } from "./memo"

declare global {
interface Window {
Expand All @@ -15,6 +15,7 @@ declare class ScrollTimeline implements ProgressTimeline {
cancel?: VoidFunction
}

export const supportsScrollTimeline = /* @__PURE__ */ memo(
() => window.ScrollTimeline !== undefined
export const supportsScrollTimeline = /* @__PURE__ */ memoSupports(
() => window.ScrollTimeline !== undefined,
"scrollTimeline"
)