Skip to content

Commit

Permalink
Merge pull request #1988 from framer/fix/async-event-handlers
Browse files Browse the repository at this point in the history
Frame-lock external event handlers to allow for React batching
  • Loading branch information
mergetron[bot] authored Mar 6, 2023
2 parents 39c8a54 + 4f20da6 commit 88a7f76
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 35 deletions.
11 changes: 8 additions & 3 deletions packages/framer-motion/src/gestures/__tests__/hover.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { transformValues } from "../../motion/__tests__/util-transform-values"
import { sync } from "../../frameloop"

describe("hover", () => {
test("hover event listeners fire", () => {
test("hover event listeners fire", async () => {
const hoverIn = jest.fn()
const hoverOut = jest.fn()
const Component = () => (
Expand All @@ -22,8 +22,13 @@ describe("hover", () => {
pointerEnter(container.firstChild as Element)
pointerLeave(container.firstChild as Element)

expect(hoverIn).toBeCalledTimes(1)
expect(hoverOut).toBeCalledTimes(1)
return new Promise<void>((resolve) => {
sync.render(() => {
expect(hoverIn).toBeCalledTimes(1)
expect(hoverOut).toBeCalledTimes(1)
resolve()
})
})
})

test("whileHover applied", async () => {
Expand Down
48 changes: 37 additions & 11 deletions packages/framer-motion/src/gestures/__tests__/press.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import {
} from "../../../jest.setup"
import { drag, MockDrag } from "../drag/__tests__/utils"
import { fireEvent } from "@testing-library/dom"
import { nextFrame } from "./utils"

const enterKey = {
key: "Enter",
}

describe("press", () => {
test("press event listeners fire", () => {
test("press event listeners fire", async () => {
const press = jest.fn()
const Component = () => <motion.div onTap={() => press()} />

Expand All @@ -26,10 +27,12 @@ describe("press", () => {
pointerDown(container.firstChild as Element)
pointerUp(container.firstChild as Element)

await nextFrame()

expect(press).toBeCalledTimes(1)
})

test("press event listeners fire via keyboard", () => {
test("press event listeners fire via keyboard", async () => {
const press = jest.fn()
const pressStart = jest.fn()
const pressCancel = jest.fn()
Expand All @@ -47,16 +50,20 @@ describe("press", () => {
fireEvent.focus(container.firstChild as Element)
fireEvent.keyDown(container.firstChild as Element, enterKey)

await nextFrame()

expect(pressStart).toBeCalledTimes(1)

fireEvent.keyUp(container.firstChild as Element, enterKey)

await nextFrame()

expect(pressStart).toBeCalledTimes(1)
expect(press).toBeCalledTimes(1)
expect(pressCancel).toBeCalledTimes(0)
})

test("press cancel event listeners fire via keyboard", () => {
test("press cancel event listeners fire via keyboard", async () => {
const press = jest.fn()
const pressStart = jest.fn()
const pressCancel = jest.fn()
Expand All @@ -74,16 +81,20 @@ describe("press", () => {
fireEvent.focus(container.firstChild as Element)
fireEvent.keyDown(container.firstChild as Element, enterKey)

await nextFrame()

expect(pressStart).toBeCalledTimes(1)

fireEvent.blur(container.firstChild as Element)

await nextFrame()

expect(pressStart).toBeCalledTimes(1)
expect(press).toBeCalledTimes(0)
expect(pressCancel).toBeCalledTimes(1)
})

test("press cancel event listeners not fired via keyboard after keyUp", () => {
test("press cancel event listeners not fired via keyboard after keyUp", async () => {
const press = jest.fn()
const pressStart = jest.fn()
const pressCancel = jest.fn()
Expand All @@ -102,31 +113,37 @@ describe("press", () => {
fireEvent.keyDown(container.firstChild as Element, enterKey)
fireEvent.keyUp(container.firstChild as Element, enterKey)

await nextFrame()

expect(pressStart).toBeCalledTimes(1)

fireEvent.blur(container.firstChild as Element)

await nextFrame()

expect(press).toBeCalledTimes(1)
expect(pressStart).toBeCalledTimes(1)
expect(pressCancel).toBeCalledTimes(0)
})

test("press event listeners are cleaned up", () => {
test("press event listeners are cleaned up", async () => {
const press = jest.fn()
const { container, rerender } = render(
<motion.div onTap={() => press()} />
)
rerender(<motion.div onTap={() => press()} />)
pointerDown(container.firstChild as Element)
pointerUp(container.firstChild as Element)
await nextFrame()
expect(press).toBeCalledTimes(1)
rerender(<motion.div />)
pointerDown(container.firstChild as Element)
pointerUp(container.firstChild as Element)
await nextFrame()
expect(press).toBeCalledTimes(1)
})

test("onTapCancel is correctly removed from a component", () => {
test("onTapCancel is correctly removed from a component", async () => {
const cancelA = jest.fn()

const Component = () => (
Expand All @@ -147,15 +164,17 @@ describe("press", () => {

pointerDown(a)
pointerUp(a)
await nextFrame()

expect(cancelA).not.toHaveBeenCalled()

pointerDown(b)
pointerUp(b)
await nextFrame()
expect(cancelA).not.toHaveBeenCalled()
})

test("press event listeners fire if triggered by child", () => {
test("press event listeners fire if triggered by child", async () => {
const press = jest.fn()
const Component = () => (
<motion.div onTap={() => press()}>
Expand All @@ -168,11 +187,12 @@ describe("press", () => {

pointerDown(getByTestId("child"))
pointerUp(getByTestId("child"))
await nextFrame()

expect(press).toBeCalledTimes(1)
})

test("press event listeners fire if triggered by child and released on bound element", () => {
test("press event listeners fire if triggered by child and released on bound element", async () => {
const press = jest.fn()
const Component = () => (
<motion.div onTap={() => press()}>
Expand All @@ -185,11 +205,12 @@ describe("press", () => {

pointerDown(getByTestId("child"))
pointerUp(container.firstChild as Element)
await nextFrame()

expect(press).toBeCalledTimes(1)
})

test("press event listeners fire if triggered by bound element and released on child", () => {
test("press event listeners fire if triggered by bound element and released on child", async () => {
const press = jest.fn()
const Component = () => (
<motion.div onTap={() => press()}>
Expand All @@ -202,11 +223,12 @@ describe("press", () => {

pointerDown(container.firstChild as Element)
pointerUp(getByTestId("child"))
await nextFrame()

expect(press).toBeCalledTimes(1)
})

test("press cancel fires if press released outside element", () => {
test("press cancel fires if press released outside element", async () => {
const pressCancel = jest.fn()
const Component = () => (
<motion.div>
Expand All @@ -222,6 +244,7 @@ describe("press", () => {

pointerDown(getByTestId("child"))
pointerUp(container.firstChild as Element)
await nextFrame()

expect(pressCancel).toBeCalledTimes(1)
})
Expand All @@ -246,6 +269,7 @@ describe("press", () => {
await pointer.to(10, 10)
pointer.end()

await nextFrame()
expect(press).toBeCalledTimes(0)
})

Expand All @@ -267,11 +291,12 @@ describe("press", () => {

const pointer = await drag(getByTestId("pressTarget")).to(0.5, 0.5)
pointer.end()
await nextFrame()

expect(press).toBeCalledTimes(1)
})

test("press event listeners unset", () => {
test("press event listeners unset", async () => {
const press = jest.fn()
const Component = () => <motion.div onTap={() => press()} />

Expand All @@ -290,6 +315,7 @@ describe("press", () => {

pointerDown(container.firstChild as Element)
pointerUp(container.firstChild as Element)
await nextFrame()

expect(press).toBeCalledTimes(3)
})
Expand Down
7 changes: 7 additions & 0 deletions packages/framer-motion/src/gestures/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { sync } from "../../frameloop"

export async function nextFrame() {
return new Promise<void>((resolve) => {
sync.render(() => resolve())
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { calcLength } from "../../projection/geometry/delta-calc"
import { mix } from "../../utils/mix"
import { percent } from "../../value/types/numbers/units"
import { createMotionValueAnimation } from "../../animation"
import { sync } from "../../frameloop"

export const elementDragControls = new WeakMap<
VisualElement,
Expand Down Expand Up @@ -149,7 +150,10 @@ export class VisualElementDragControls {
})

// Fire onDragStart event
onDragStart && onDragStart(event, info)
if (onDragStart) {
sync.update(() => onDragStart(event, info))
}

const { animationState } = this.visualElement
animationState && animationState.setActive("whileDrag", true)
}
Expand Down Expand Up @@ -223,7 +227,9 @@ export class VisualElementDragControls {
this.startAnimation(velocity)

const { onDragEnd } = this.getProps()
onDragEnd && onDragEnd(event, info)
if (onDragEnd) {
sync.update(() => onDragEnd(event, info))
}
}

private cancel() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react"
import { pointerDown, render } from "../../../../jest.setup"
import { BoundingBox, motion, motionValue, MotionValue } from "../../../"
import { MockDrag, drag, deferred, frame, Point, sleep } from "./utils"
import { nextFrame } from "../../__tests__/utils"

describe("drag", () => {
test("onDragStart fires", async () => {
Expand All @@ -19,6 +20,8 @@ describe("drag", () => {

pointer.end()

await nextFrame()

expect(onDragStart).toBeCalledTimes(1)
})
})
Expand All @@ -43,6 +46,8 @@ describe("dragging", () => {

pointer.end()

await nextFrame()

expect(onDragStart).toBeCalledTimes(0)
})

Expand All @@ -60,6 +65,8 @@ describe("dragging", () => {
const pointer = await drag(container.firstChild).to(100, 100)
pointer.end()

await nextFrame()

expect(onDragEnd).toBeCalledTimes(1)
})

Expand All @@ -78,6 +85,8 @@ describe("dragging", () => {

pointer.end()

await nextFrame()

expect(onDragEnd).not.toBeCalled()
})

Expand Down Expand Up @@ -105,6 +114,8 @@ describe("dragging", () => {

pointer.end()

await nextFrame()

expect(onDragStart).toBeCalledTimes(1)
expect(onDragEnd).toBeCalledTimes(1)
})
Expand Down Expand Up @@ -177,7 +188,7 @@ describe("dragging", () => {
const pointer = await drag(getByTestId("draggable")).to(50, 50)
pointer.end()

expect(onDragEnd.promise).resolves.toEqual(p)
await expect(onDragEnd.promise).resolves.toEqual(p)
})

test("panSessionStart fires", async () => {
Expand All @@ -194,6 +205,8 @@ describe("dragging", () => {
const pointer = await drag(container.firstChild).to(100, 100)
pointer.end()

await nextFrame()

expect(onDragStart).toBeCalledTimes(1)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react"
import { render } from "../../../../jest.setup"
import { motion, useDragControls } from "../../../"
import { MockDrag, drag } from "./utils"
import { nextFrame } from "../../__tests__/utils"

describe("useDragControls", () => {
test(".start triggers dragging on a different component", async () => {
Expand Down Expand Up @@ -34,6 +35,8 @@ describe("useDragControls", () => {

pointer.end()

await nextFrame()

expect(onDragStart).toBeCalledTimes(1)
})

Expand Down Expand Up @@ -67,7 +70,7 @@ describe("useDragControls", () => {
).to(100, 100)

pointer.end()

await nextFrame()
expect(onDragStart).toBeCalledTimes(1)
})
})
3 changes: 2 additions & 1 deletion packages/framer-motion/src/gestures/hover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isDragActive } from "./drag/utils/lock"
import { EventInfo } from "../events/types"
import type { VisualElement } from "../render/VisualElement"
import { Feature } from "../motion/features/Feature"
import { sync } from "../frameloop"

function addHoverEvent(node: VisualElement<Element>, isActive: boolean) {
const eventName = "pointer" + (isActive ? "enter" : "leave")
Expand All @@ -19,7 +20,7 @@ function addHoverEvent(node: VisualElement<Element>, isActive: boolean) {
}

if (props[callbackName]) {
props[callbackName](event, info)
sync.update(() => props[callbackName](event, info))
}
}

Expand Down
Loading

0 comments on commit 88a7f76

Please sign in to comment.