diff --git a/dev/html/package.json b/dev/html/package.json index aee76d4866..f201a90050 100644 --- a/dev/html/package.json +++ b/dev/html/package.json @@ -1,7 +1,7 @@ { "name": "html-env", "private": true, - "version": "12.4.3", + "version": "12.4.4-alpha.0", "type": "module", "scripts": { "dev": "vite", @@ -10,9 +10,9 @@ "preview": "vite preview" }, "dependencies": { - "framer-motion": "^12.4.3", - "motion": "^12.4.3", - "motion-dom": "^12.0.0" + "framer-motion": "^12.4.4-alpha.0", + "motion": "^12.4.4-alpha.0", + "motion-dom": "^12.4.4-alpha.0" }, "devDependencies": { "vite": "^5.2.0" diff --git a/dev/html/public/playwright/gestures/press.html b/dev/html/public/playwright/gestures/press.html index 23d6f6f509..4ec95902c0 100644 --- a/dev/html/public/playwright/gestures/press.html +++ b/dev/html/public/playwright/gestures/press.html @@ -11,7 +11,7 @@ background-color: #0077ff; } - .box:focus { + .box:focus-visible { background-color: #ff0000; } @@ -25,11 +25,14 @@ press
press
+ + diff --git a/dev/next/package.json b/dev/next/package.json index e4ff399b98..6dbc40a991 100644 --- a/dev/next/package.json +++ b/dev/next/package.json @@ -1,7 +1,7 @@ { "name": "next-env", "private": true, - "version": "12.4.3", + "version": "12.4.4-alpha.0", "type": "module", "scripts": { "dev": "next dev", @@ -9,7 +9,7 @@ "start": "next start" }, "dependencies": { - "motion": "^12.4.3", + "motion": "^12.4.4-alpha.0", "next": "14.x", "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/dev/react-19/package.json b/dev/react-19/package.json index 2f73156670..21e05ee7e1 100644 --- a/dev/react-19/package.json +++ b/dev/react-19/package.json @@ -1,7 +1,7 @@ { "name": "react-19-env", "private": true, - "version": "12.4.3", + "version": "12.4.4-alpha.0", "type": "module", "scripts": { "dev": "vite", @@ -11,7 +11,7 @@ "preview": "vite preview" }, "dependencies": { - "motion": "^12.4.3", + "motion": "^12.4.4-alpha.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/dev/react/package.json b/dev/react/package.json index 1b24844a70..75de064687 100644 --- a/dev/react/package.json +++ b/dev/react/package.json @@ -1,7 +1,7 @@ { "name": "react-env", "private": true, - "version": "12.4.3", + "version": "12.4.4-alpha.0", "type": "module", "scripts": { "dev": "vite", @@ -11,7 +11,7 @@ "preview": "vite preview" }, "dependencies": { - "framer-motion": "^12.4.3", + "framer-motion": "^12.4.4-alpha.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/dev/react/src/examples/Events-whileTap.tsx b/dev/react/src/examples/Events-whileTap.tsx index a2e3de3834..88ab596563 100644 --- a/dev/react/src/examples/Events-whileTap.tsx +++ b/dev/react/src/examples/Events-whileTap.tsx @@ -8,7 +8,11 @@ const style = { export const App = () => { return ( - + console.log("tap")} + onTapCancel={() => console.log("tap cancel")} + > { + const [width, setWidth] = useState(100) + const [presenceState, setPresenceState] = useState(true) + + useEffect(() => { + if (width === 200) return + const timeout = setTimeout(() => { + setWidth(50) + + setTimeout(() => { + setWidth(200) + }, 1000) + }, 1000) + + return () => clearTimeout(timeout) + }, [width]) + + useEffect(() => { + setTimeout(() => { + setPresenceState(false) + }, 2100) + }, [presenceState]) + + return ( + <> + + {presenceState && ( + + + Presence + + + )} + + + ) +} diff --git a/lerna.json b/lerna.json index cdda50d59b..4639f048af 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "12.4.3", + "version": "12.4.4-alpha.0", "packages": [ "packages/*", "dev/*" diff --git a/packages/framer-motion-3d/package.json b/packages/framer-motion-3d/package.json index 836812a00f..128312c5b1 100644 --- a/packages/framer-motion-3d/package.json +++ b/packages/framer-motion-3d/package.json @@ -1,6 +1,6 @@ { "name": "framer-motion-3d", - "version": "12.4.3", + "version": "12.4.4-alpha.0", "description": "A simple and powerful React animation library for @react-three/fiber", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", @@ -45,7 +45,7 @@ "postpublish": "git push --tags" }, "dependencies": { - "framer-motion": "^12.4.3", + "framer-motion": "^12.4.4-alpha.0", "react-merge-refs": "^2.0.1" }, "peerDependencies": { diff --git a/packages/framer-motion/jest.setup.tsx b/packages/framer-motion/jest.setup.tsx index 3217a22358..9e6660b14b 100644 --- a/packages/framer-motion/jest.setup.tsx +++ b/packages/framer-motion/jest.setup.tsx @@ -3,13 +3,14 @@ import "@testing-library/jest-dom" // because @testing-library/react one switches out pointerEnter and pointerLeave import { fireEvent, getByTestId } from "@testing-library/dom" import { render as testRender } from "@testing-library/react" -import { act, StrictMode, Fragment } from "react" +import { act, Fragment, StrictMode } from "react" /** * Stub PointerEvent - this is so we can pass through PointerEvent.isPrimary */ const pointerEventProps = ["isPrimary", "pointerType", "button"] class PointerEventFake extends Event { + pointerId?: number = 1 constructor(type: any, props: any) { super(type, props) if (!props) return diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index 46b03df755..00499bf88c 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -1,6 +1,6 @@ { "name": "framer-motion", - "version": "12.4.3", + "version": "12.4.4-alpha.0", "description": "A simple and powerful JavaScript animation library", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", @@ -89,7 +89,7 @@ "measure": "rollup -c ./rollup.size.config.mjs" }, "dependencies": { - "motion-dom": "^12.0.0", + "motion-dom": "^12.4.4-alpha.0", "motion-utils": "^12.0.0", "tslib": "^2.4.0" }, diff --git a/packages/framer-motion/src/gestures/__tests__/press.test.tsx b/packages/framer-motion/src/gestures/__tests__/press.test.tsx index 03e424d66b..3cfe38222a 100644 --- a/packages/framer-motion/src/gestures/__tests__/press.test.tsx +++ b/packages/framer-motion/src/gestures/__tests__/press.test.tsx @@ -263,7 +263,8 @@ describe("press", () => { expect(press).toBeCalledTimes(1) }) - test("press cancel fires if press released outside element", async () => { + // Replaced with end to end test but ideally would also run here + test.skip("press cancel fires if press released outside element", async () => { const pressCancel = jest.fn() const Component = () => ( diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index f192dad62a..3cfcdbf2fa 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -1,39 +1,38 @@ -import { invariant } from "motion-utils" import { setDragLock } from "motion-dom" -import { PanSession, PanInfo } from "../pan/PanSession" -import { ResolvedConstraints } from "./types" -import { isRefObject } from "../../utils/is-ref-object" +import { invariant } from "motion-utils" +import { animateMotionValue } from "../../animation/interfaces/motion-value" +import { addDomEvent } from "../../events/add-dom-event" import { addPointerEvent } from "../../events/add-pointer-event" -import { - calcRelativeConstraints, - calcViewportConstraints, - applyConstraints, - rebaseAxisConstraints, - resolveDragElastic, - defaultElastic, - calcOrigin, -} from "./utils/constraints" -import type { VisualElement } from "../../render/VisualElement" -import { MotionProps } from "../../motion/types" -import { Axis, Point } from "../../projection/geometry/types" -import { createBox } from "../../projection/geometry/models" -import { eachAxis } from "../../projection/utils/each-axis" -import { measurePageBox } from "../../projection/utils/measure" import { extractEventInfo } from "../../events/event-info" -import { Transition } from "../../types" +import { frame } from "../../frameloop" +import { MotionProps } from "../../motion/types" import { convertBoundingBoxToBox, convertBoxToBoundingBox, } from "../../projection/geometry/conversion" -import { LayoutUpdateData } from "../../projection/node/types" -import { addDomEvent } from "../../events/add-dom-event" import { calcLength } from "../../projection/geometry/delta-calc" +import { createBox } from "../../projection/geometry/models" +import { Axis, Point } from "../../projection/geometry/types" +import { LayoutUpdateData } from "../../projection/node/types" +import { eachAxis } from "../../projection/utils/each-axis" +import { measurePageBox } from "../../projection/utils/measure" +import type { VisualElement } from "../../render/VisualElement" +import { Transition } from "../../types" +import { isRefObject } from "../../utils/is-ref-object" import { mixNumber } from "../../utils/mix/number" import { percent } from "../../value/types/numbers/units" -import { animateMotionValue } from "../../animation/interfaces/motion-value" -import { getContextWindow } from "../../utils/get-context-window" -import { frame } from "../../frameloop" import { addValueToWillChange } from "../../value/use-will-change/add-will-change" +import { PanInfo, PanSession } from "../pan/PanSession" +import { ResolvedConstraints } from "./types" +import { + applyConstraints, + calcOrigin, + calcRelativeConstraints, + calcViewportConstraints, + defaultElastic, + rebaseAxisConstraints, + resolveDragElastic, +} from "./utils/constraints" export const elementDragControls = new WeakMap< VisualElement, @@ -230,7 +229,6 @@ export class VisualElementDragControls { { transformPagePoint: this.visualElement.getTransformPagePoint(), dragSnapToOrigin, - contextWindow: getContextWindow(this.visualElement), } ) } diff --git a/packages/framer-motion/src/gestures/drag/__tests__/utils.tsx b/packages/framer-motion/src/gestures/drag/__tests__/utils.tsx index 49458487f6..aa148ae0d3 100644 --- a/packages/framer-motion/src/gestures/drag/__tests__/utils.tsx +++ b/packages/framer-motion/src/gestures/drag/__tests__/utils.tsx @@ -1,7 +1,7 @@ import * as React from "react" -import { frame } from "../../../frameloop" -import { MotionConfig } from "../../../components/MotionConfig" import { pointerDown, pointerMove, pointerUp } from "../../../../jest.setup" +import { MotionConfig } from "../../../components/MotionConfig" +import { frame } from "../../../frameloop" export type Point = { x: number @@ -41,7 +41,7 @@ export const drag = (element: any, triggerElement?: any) => { pos.y = y await React.act(async () => { - pointerMove(document.body) + pointerMove(element) await dragFrame.postRender() }) diff --git a/packages/framer-motion/src/gestures/pan/PanSession.ts b/packages/framer-motion/src/gestures/pan/PanSession.ts index 32c1ad5f3d..df08e3d7c9 100644 --- a/packages/framer-motion/src/gestures/pan/PanSession.ts +++ b/packages/framer-motion/src/gestures/pan/PanSession.ts @@ -95,7 +95,6 @@ interface PanSessionHandlers { interface PanSessionOptions { transformPagePoint?: TransformPoint - contextWindow?: (Window & typeof globalThis) | null dragSnapToOrigin?: boolean } @@ -149,19 +148,10 @@ export class PanSession { */ private dragSnapToOrigin: boolean - /** - * @internal - */ - private contextWindow: PanSessionOptions["contextWindow"] = window - constructor( event: PointerEvent, handlers: Partial, - { - transformPagePoint, - contextWindow, - dragSnapToOrigin = false, - }: PanSessionOptions = {} + { transformPagePoint, dragSnapToOrigin = false }: PanSessionOptions = {} ) { // If we have more than one touch, don't start detecting this gesture if (!isPrimaryPointer(event)) return @@ -169,7 +159,6 @@ export class PanSession { this.dragSnapToOrigin = dragSnapToOrigin this.handlers = handlers this.transformPagePoint = transformPagePoint - this.contextWindow = contextWindow || window const info = extractEventInfo(event) const initialInfo = transformPoint(info, this.transformPagePoint) @@ -183,21 +172,28 @@ export class PanSession { onSessionStart && onSessionStart(event, getPanInfo(initialInfo, this.history)) + capturePointer(event, "set") + this.removeListeners = pipe( addPointerEvent( - this.contextWindow, + event.currentTarget!, "pointermove", this.handlePointerMove ), addPointerEvent( - this.contextWindow, + event.currentTarget!, "pointerup", this.handlePointerUp ), addPointerEvent( - this.contextWindow, + event.currentTarget!, "pointercancel", this.handlePointerUp + ), + addPointerEvent( + event.currentTarget!, + "lostpointercapture", + this.handlePointerUp ) ) } @@ -231,6 +227,18 @@ export class PanSession { } private handlePointerMove = (event: PointerEvent, info: EventInfo) => { + if ( + event.currentTarget instanceof Element && + event.currentTarget.hasPointerCapture && + event.pointerId !== undefined + ) { + try { + if (!event.currentTarget.hasPointerCapture(event.pointerId)) { + return + } + } catch (e) {} + } + this.lastMoveEvent = event this.lastMoveEventInfo = transformPoint(info, this.transformPagePoint) @@ -239,6 +247,8 @@ export class PanSession { } private handlePointerUp = (event: PointerEvent, info: EventInfo) => { + capturePointer(event, "release") + this.end() const { onEnd, onSessionEnd, resumeAnimation } = this.handlers @@ -247,7 +257,8 @@ export class PanSession { if (!(this.lastMoveEvent && this.lastMoveEventInfo)) return const panInfo = getPanInfo( - event.type === "pointercancel" + event.type === "pointercancel" || + event.type === "lostpointercapture" ? this.lastMoveEventInfo : transformPoint(info, this.transformPagePoint), this.history @@ -342,3 +353,19 @@ function getVelocity(history: TimestampedPoint[], timeDelta: number): Point { return currentVelocity } + +function capturePointer(event: PointerEvent, action: "set" | "release") { + const actionName = `${action}PointerCapture` as + | "setPointerCapture" + | "releasePointerCapture" + + if ( + event.currentTarget instanceof Element && + actionName in event.currentTarget && + event.pointerId !== undefined + ) { + try { + event.currentTarget[actionName](event.pointerId) + } catch (e) {} + } +} diff --git a/packages/framer-motion/src/gestures/pan/index.ts b/packages/framer-motion/src/gestures/pan/index.ts index 574dd432ed..3d545897b7 100644 --- a/packages/framer-motion/src/gestures/pan/index.ts +++ b/packages/framer-motion/src/gestures/pan/index.ts @@ -1,9 +1,8 @@ -import { PanInfo, PanSession } from "./PanSession" -import { addPointerEvent } from "../../events/add-pointer-event" -import { Feature } from "../../motion/features/Feature" import { noop } from "motion-utils" -import { getContextWindow } from "../../utils/get-context-window" +import { addPointerEvent } from "../../events/add-pointer-event" import { frame } from "../../frameloop" +import { Feature } from "../../motion/features/Feature" +import { PanInfo, PanSession } from "./PanSession" type PanEventHandler = (event: PointerEvent, info: PanInfo) => void const asyncHandler = @@ -24,7 +23,6 @@ export class PanGesture extends Feature { this.createPanHandlers(), { transformPagePoint: this.node.getTransformPagePoint(), - contextWindow: getContextWindow(this.node), } ) } diff --git a/packages/framer-motion/src/utils/get-context-window.ts b/packages/framer-motion/src/utils/get-context-window.ts deleted file mode 100644 index ac4fbb6cf0..0000000000 --- a/packages/framer-motion/src/utils/get-context-window.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { VisualElement } from "../render/VisualElement" - -// Fixes https://github.com/motiondivision/motion/issues/2270 -export const getContextWindow = ({ current }: VisualElement) => { - return current ? current.ownerDocument.defaultView : null -} diff --git a/packages/motion-dom/package.json b/packages/motion-dom/package.json index e6515dd485..06a205a6bb 100644 --- a/packages/motion-dom/package.json +++ b/packages/motion-dom/package.json @@ -1,6 +1,6 @@ { "name": "motion-dom", - "version": "12.0.0", + "version": "12.4.4-alpha.0", "author": "Matt Perry", "license": "MIT", "repository": "https://github.com/motiondivision/motion", diff --git a/packages/motion-dom/src/gestures/press/index.ts b/packages/motion-dom/src/gestures/press/index.ts index 781088dd2d..85dd868bad 100644 --- a/packages/motion-dom/src/gestures/press/index.ts +++ b/packages/motion-dom/src/gestures/press/index.ts @@ -41,33 +41,49 @@ export interface PointerEventOptions extends EventOptions { * @public */ export function press( - elementOrSelector: ElementOrSelector, + targetOrSelector: ElementOrSelector, onPressStart: OnPressStartEvent, options: PointerEventOptions = {} ): VoidFunction { - const [elements, eventOptions, cancelEvents] = setupGesture( - elementOrSelector, + const [targets, eventOptions, cancelEvents] = setupGesture( + targetOrSelector, options ) const startPress = (startEvent: PointerEvent) => { - const element = startEvent.currentTarget as Element + const target = startEvent.currentTarget as Element - if (!isValidPressEvent(startEvent) || isPressing.has(element)) return + if (!target || !isValidPressEvent(startEvent) || isPressing.has(target)) + return - isPressing.add(element) + isPressing.add(target) - const onPressEnd = onPressStart(element, startEvent) + if (target.setPointerCapture && startEvent.pointerId !== undefined) { + try { + target.setPointerCapture(startEvent.pointerId) + } catch (e) {} + } + + const onPressEnd = onPressStart(target, startEvent) const onPointerEnd = (endEvent: PointerEvent, success: boolean) => { - window.removeEventListener("pointerup", onPointerUp) - window.removeEventListener("pointercancel", onPointerCancel) + target.removeEventListener("pointerup", onPointerUp) + target.removeEventListener("pointercancel", onPointerCancel) + + if ( + target.releasePointerCapture && + endEvent.pointerId !== undefined + ) { + try { + target.releasePointerCapture(endEvent.pointerId) + } catch (e) {} + } - if (!isValidPressEvent(endEvent) || !isPressing.has(element)) { + if (!isValidPressEvent(endEvent) || !isPressing.has(target)) { return } - isPressing.delete(element) + isPressing.delete(target) if (typeof onPressEnd === "function") { onPressEnd(endEvent, { success }) @@ -75,42 +91,87 @@ export function press( } const onPointerUp = (upEvent: PointerEvent) => { - onPointerEnd( - upEvent, - options.useGlobalTarget || - isNodeOrChild(element, upEvent.target as Element) - ) + const isOutside = !upEvent.isTrusted + ? false + : checkOutside( + upEvent, + target instanceof Element + ? target.getBoundingClientRect() + : { + left: 0, + top: 0, + right: window.innerWidth, + bottom: window.innerHeight, + } + ) + + if (isOutside) { + onPointerEnd(upEvent, false) + } else { + onPointerEnd( + upEvent, + !(target instanceof Element) || + isNodeOrChild(target, upEvent.target as Element) + ) + } } const onPointerCancel = (cancelEvent: PointerEvent) => { onPointerEnd(cancelEvent, false) } - window.addEventListener("pointerup", onPointerUp, eventOptions) - window.addEventListener("pointercancel", onPointerCancel, eventOptions) + target.addEventListener("pointerup", onPointerUp, eventOptions) + target.addEventListener("pointercancel", onPointerCancel, eventOptions) + target.addEventListener( + "lostpointercapture", + onPointerCancel, + eventOptions + ) } - elements.forEach((element: Element) => { - if ( - !isElementKeyboardAccessible(element) && - element.getAttribute("tabindex") === null - ) { - ;(element as HTMLElement).tabIndex = 0 + targets.forEach((target: EventTarget) => { + target = options.useGlobalTarget ? window : target + + let canAddKeyboardAccessibility = false + + if (target instanceof HTMLElement) { + canAddKeyboardAccessibility = true + + if ( + !isElementKeyboardAccessible(target) && + target.getAttribute("tabindex") === null + ) { + target.tabIndex = 0 + } } - const target = options.useGlobalTarget ? window : element target.addEventListener( "pointerdown", startPress as EventListener, eventOptions ) - element.addEventListener( - "focus", - (event) => enableKeyboardPress(event as FocusEvent, eventOptions), - eventOptions - ) + if (canAddKeyboardAccessibility) { + target.addEventListener( + "focus", + (event) => + enableKeyboardPress(event as FocusEvent, eventOptions), + eventOptions + ) + } }) return cancelEvents } + +function checkOutside( + event: PointerEvent, + rect: { left: number; top: number; right: number; bottom: number } +) { + return ( + event.clientX < rect.left || + event.clientX > rect.right || + event.clientY < rect.top || + event.clientY > rect.bottom + ) +} diff --git a/packages/motion-dom/src/gestures/press/utils/state.ts b/packages/motion-dom/src/gestures/press/utils/state.ts index 1775e4c8d4..adb0876d79 100644 --- a/packages/motion-dom/src/gestures/press/utils/state.ts +++ b/packages/motion-dom/src/gestures/press/utils/state.ts @@ -1 +1 @@ -export const isPressing = new WeakSet() +export const isPressing = new WeakSet() diff --git a/packages/motion-dom/src/utils/resolve-elements.ts b/packages/motion-dom/src/utils/resolve-elements.ts index 6cc1799589..a2fffe7fe4 100644 --- a/packages/motion-dom/src/utils/resolve-elements.ts +++ b/packages/motion-dom/src/utils/resolve-elements.ts @@ -22,7 +22,7 @@ export function resolveElements( scope?: AnimationScope, selectorCache?: SelectorCache ): Element[] { - if (elementOrSelector instanceof Element) { + if (elementOrSelector instanceof EventTarget) { return [elementOrSelector] } else if (typeof elementOrSelector === "string") { let root: WithQuerySelectorAll = document diff --git a/packages/motion/package.json b/packages/motion/package.json index 764ba5de02..a07548b947 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,6 +1,6 @@ { "name": "motion", - "version": "12.4.3", + "version": "12.4.4-alpha.0", "description": "An animation library for JavaScript and React.", "main": "dist/cjs/index.js", "module": "dist/es/motion/lib/index.mjs", @@ -76,7 +76,7 @@ "postpublish": "git push --tags" }, "dependencies": { - "framer-motion": "^12.4.3", + "framer-motion": "^12.4.4-alpha.0", "tslib": "^2.4.0" }, "peerDependencies": { diff --git a/tests/gestures/press.spec.ts b/tests/gestures/press.spec.ts index b672ddf018..c9462fb2ea 100644 --- a/tests/gestures/press.spec.ts +++ b/tests/gestures/press.spec.ts @@ -4,6 +4,11 @@ test.beforeEach(async ({ page }) => { await page.goto("gestures/press.html") }) +const pointerOptions = { + isPrimary: true, + pointerId: 1, +} + test.describe("press events", () => { // CI pointers not working well if (process.env.CI) { @@ -88,11 +93,11 @@ test.describe("press events", () => { const pressDiv = page.locator("#press-div") // Start press - await pressDiv.dispatchEvent("pointerdown", { isPrimary: true }) + await pressDiv.dispatchEvent("pointerdown", pointerOptions) await expect(pressDiv).toHaveText("start") // Release pointer - should trigger press end - await page.dispatchEvent("body", "pointerup", { isPrimary: true }) + await pressDiv.dispatchEvent("pointerup", pointerOptions) await expect(pressDiv).toHaveText("end") }) @@ -102,11 +107,11 @@ test.describe("press events", () => { const pressDiv = page.locator("#press-button-disabled") // Start press - await pressDiv.dispatchEvent("pointerdown", { isPrimary: true }) + await pressDiv.dispatchEvent("pointerdown", pointerOptions) await expect(pressDiv).not.toHaveText("start") // Release pointer - should trigger press end - await page.dispatchEvent("body", "pointerup", { isPrimary: true }) + await pressDiv.dispatchEvent("pointerup", pointerOptions) await expect(pressDiv).not.toHaveText("end") }) @@ -115,12 +120,12 @@ test.describe("press events", () => { const pressDivCancel = page.locator("#press-div-cancel") // Start press on first element - await pressDiv.dispatchEvent("pointerdown", { isPrimary: true }) + await pressDiv.dispatchEvent("pointerdown", pointerOptions) await expect(pressDiv).toHaveText("start") // Move pointer to second element await pressDivCancel.dispatchEvent("pointerenter") - await page.dispatchEvent("body", "pointerup", { isPrimary: true }) + await pressDiv.dispatchEvent("pointerup", pointerOptions) // Check first element returned to blue const pressDivColor = await pressDiv.evaluate((el) => { @@ -132,13 +137,15 @@ test.describe("press events", () => { await expect(pressDiv).toHaveText("end") // Press sequence on second element - await pressDivCancel.dispatchEvent("pointerdown", { isPrimary: true }) + await pressDivCancel.dispatchEvent("pointerdown", pointerOptions) await expect(pressDivCancel).toHaveText("start") - await pressDivCancel.dispatchEvent("pointerup", { isPrimary: true }) + await pressDivCancel.dispatchEvent("pointerup", pointerOptions) await expect(pressDivCancel).toHaveText("end") - await pressDivCancel.dispatchEvent("pointerdown", { isPrimary: true }) + await page.mouse.move(10, 110) + await page.mouse.down() await expect(pressDivCancel).toHaveText("start") - await page.dispatchEvent("body", "pointerup", { isPrimary: true }) + await page.mouse.move(1000, 1000) + await page.mouse.up() await expect(pressDivCancel).toHaveText("cancel") }) @@ -155,7 +162,7 @@ test.describe("press events", () => { await expect(pressDiv).not.toHaveText("start") // Release right click - await page.dispatchEvent("body", "pointerup", { + await pressDiv.dispatchEvent("pointerup", { button: 2, isPrimary: false, }) @@ -163,7 +170,52 @@ test.describe("press events", () => { // Text should still not have changed await expect(pressDiv).not.toHaveText("end") }) + + test("press handles window events correctly", async ({ page }) => { + const windowOutput = page.locator("#window-output") + + // Start press on window + await page.mouse.move(100, 100) + await page.mouse.down() + await expect(windowOutput).toHaveValue("start") + + // Release pointer inside window - should trigger press end + await page.mouse.up() + await expect(windowOutput).toHaveValue("end") + + // Start another press + await page.mouse.down() + await expect(windowOutput).toHaveValue("start") + + // Move pointer outside window and release - should cancel press + await page.mouse.move(-10, -10) + await page.mouse.up() + await expect(windowOutput).toHaveValue("cancel") + }) + + test("press handles document events correctly", async ({ page }) => { + const documentOutput = page.locator("#document-output") + + // Start press on document + await page.mouse.move(100, 100) + await page.mouse.down() + await expect(documentOutput).toHaveValue("start") + + // Release pointer inside document - should trigger press end + await page.mouse.up() + await expect(documentOutput).toHaveValue("end") + + // Start another press + await page.mouse.down() + await expect(documentOutput).toHaveValue("start") + + // Move pointer outside document and release - should cancel press + await page.mouse.move(-10, -10) + await page.mouse.up() + await expect(documentOutput).toHaveValue("cancel") + }) }) + test.describe("press accessibility", () => { test("button", async ({ page }) => { const button = page.locator("#press-no-tab-index-1") diff --git a/yarn.lock b/yarn.lock index 7151e665b2..c80c6a9f58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6911,7 +6911,7 @@ __metadata: "@react-three/fiber": 8.2.2 "@react-three/test-renderer": ^9.0.0 "@rollup/plugin-commonjs": ^22.0.1 - framer-motion: ^12.4.3 + framer-motion: ^12.4.4-alpha.0 react-merge-refs: ^2.0.1 three: ^0.137.0 peerDependencies: @@ -6922,12 +6922,12 @@ __metadata: languageName: unknown linkType: soft -"framer-motion@^12.4.3, framer-motion@workspace:packages/framer-motion": +"framer-motion@^12.4.4-alpha.0, framer-motion@workspace:packages/framer-motion": version: 0.0.0-use.local resolution: "framer-motion@workspace:packages/framer-motion" dependencies: "@types/three": 0.137.0 - motion-dom: ^12.0.0 + motion-dom: ^12.4.4-alpha.0 motion-utils: ^12.0.0 three: 0.137.0 tslib: ^2.4.0 @@ -7685,9 +7685,9 @@ __metadata: version: 0.0.0-use.local resolution: "html-env@workspace:dev/html" dependencies: - framer-motion: ^12.4.3 - motion: ^12.4.3 - motion-dom: ^12.0.0 + framer-motion: ^12.4.4-alpha.0 + motion: ^12.4.4-alpha.0 + motion-dom: ^12.4.4-alpha.0 vite: ^5.2.0 languageName: unknown linkType: soft @@ -10441,7 +10441,7 @@ __metadata: languageName: node linkType: hard -"motion-dom@^12.0.0, motion-dom@workspace:packages/motion-dom": +"motion-dom@^12.4.4-alpha.0, motion-dom@workspace:packages/motion-dom": version: 0.0.0-use.local resolution: "motion-dom@workspace:packages/motion-dom" dependencies: @@ -10516,11 +10516,11 @@ __metadata: languageName: unknown linkType: soft -"motion@^12.4.3, motion@workspace:packages/motion": +"motion@^12.4.4-alpha.0, motion@workspace:packages/motion": version: 0.0.0-use.local resolution: "motion@workspace:packages/motion" dependencies: - framer-motion: ^12.4.3 + framer-motion: ^12.4.4-alpha.0 tslib: ^2.4.0 peerDependencies: "@emotion/is-prop-valid": "*" @@ -10637,7 +10637,7 @@ __metadata: version: 0.0.0-use.local resolution: "next-env@workspace:dev/next" dependencies: - motion: ^12.4.3 + motion: ^12.4.4-alpha.0 next: 14.x react: ^18.3.1 react-dom: ^18.3.1 @@ -12099,7 +12099,7 @@ __metadata: "@typescript-eslint/parser": ^7.2.0 "@vitejs/plugin-react-swc": ^3.5.0 eslint-plugin-react-refresh: ^0.4.6 - motion: ^12.4.3 + motion: ^12.4.4-alpha.0 react: ^19.0.0 react-dom: ^19.0.0 vite: ^5.2.0 @@ -12171,7 +12171,7 @@ __metadata: "@typescript-eslint/parser": ^7.2.0 "@vitejs/plugin-react-swc": ^3.5.0 eslint-plugin-react-refresh: ^0.4.6 - framer-motion: ^12.4.3 + framer-motion: ^12.4.4-alpha.0 react: ^18.3.1 react-dom: ^18.3.1 vite: ^5.2.0