refactor(web): extract touch event forwarding
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import type { TouchEvent } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
forwardCanvasTouchEnd,
|
||||
forwardCanvasTouchMove,
|
||||
forwardTouchStartAsMouseDown,
|
||||
} from "./timelineTouchEvents.js";
|
||||
|
||||
function createTouchEvent<TCurrentTarget>(
|
||||
target: TCurrentTarget,
|
||||
point: { clientX: number; clientY: number },
|
||||
) {
|
||||
return {
|
||||
currentTarget: target,
|
||||
preventDefault: vi.fn(),
|
||||
touches: [point],
|
||||
changedTouches: [point],
|
||||
} as unknown as TouchEvent<TCurrentTarget>;
|
||||
}
|
||||
|
||||
describe("timelineTouchEvents", () => {
|
||||
it("forwards touch starts as mouse-down events and marks the decision state", () => {
|
||||
const onMouseDown = vi.fn();
|
||||
const touchStartRef = { current: { x: 0, y: 0, decided: false } };
|
||||
const event = createTouchEvent({ id: "row" }, { clientX: 18, clientY: 42 });
|
||||
|
||||
forwardTouchStartAsMouseDown({
|
||||
event,
|
||||
touchStartRef,
|
||||
decided: true,
|
||||
onMouseDown,
|
||||
opts: { allocationId: "alloc-1" },
|
||||
});
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalledOnce();
|
||||
expect(touchStartRef.current).toEqual({ x: 18, y: 42, decided: true });
|
||||
expect(onMouseDown).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ clientX: 18, button: 0, currentTarget: { id: "row" } }),
|
||||
{ allocationId: "alloc-1" },
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps touch move in scroll mode until drag handling is approved", () => {
|
||||
const onCanvasMouseMove = vi.fn();
|
||||
const touchStartRef = { current: { x: 10, y: 10, decided: false } };
|
||||
|
||||
forwardCanvasTouchMove({
|
||||
event: createTouchEvent({ id: "canvas" }, { clientX: 11, clientY: 28 }),
|
||||
touchStartRef,
|
||||
onCanvasMouseMove,
|
||||
});
|
||||
|
||||
expect(onCanvasMouseMove).not.toHaveBeenCalled();
|
||||
expect(touchStartRef.current).toEqual({ x: 10, y: 10, decided: true });
|
||||
});
|
||||
|
||||
it("forwards touch move once the touch policy resolves to drag handling", () => {
|
||||
const onCanvasMouseMove = vi.fn();
|
||||
const touchStartRef = { current: { x: 10, y: 10, decided: false } };
|
||||
|
||||
forwardCanvasTouchMove({
|
||||
event: createTouchEvent({ id: "canvas" }, { clientX: 40, clientY: 12 }),
|
||||
touchStartRef,
|
||||
onCanvasMouseMove,
|
||||
});
|
||||
|
||||
expect(onCanvasMouseMove).toHaveBeenCalledWith(expect.objectContaining({ clientX: 40, clientY: 12 }));
|
||||
expect(touchStartRef.current).toEqual({ x: 10, y: 10, decided: true });
|
||||
});
|
||||
|
||||
it("awaits touch-end forwarding through the canvas mouse-up path", async () => {
|
||||
const onCanvasMouseUp = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
await forwardCanvasTouchEnd({
|
||||
event: createTouchEvent({ id: "canvas" }, { clientX: 55, clientY: 21 }),
|
||||
onCanvasMouseUp,
|
||||
});
|
||||
|
||||
expect(onCanvasMouseUp).toHaveBeenCalledWith(expect.objectContaining({ clientX: 55, clientY: 21 }));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { TouchEvent } from "react";
|
||||
import { createTouchCanvasPointerEvent, createTouchMouseDownEvent } from "./timelineTouchAdapters.js";
|
||||
import { getTouchPoint, resolveTouchDragDecision } from "./timelineTouch.js";
|
||||
|
||||
type MutableCurrent<T> = { current: T };
|
||||
type TouchDecisionState = { x: number; y: number; decided: boolean };
|
||||
|
||||
export function forwardTouchStartAsMouseDown<TCurrentTarget, TOptions>({
|
||||
event,
|
||||
touchStartRef,
|
||||
decided,
|
||||
onMouseDown,
|
||||
opts,
|
||||
}: {
|
||||
event: TouchEvent<TCurrentTarget>;
|
||||
touchStartRef: MutableCurrent<TouchDecisionState>;
|
||||
decided: boolean;
|
||||
onMouseDown: (event: ReturnType<typeof createTouchMouseDownEvent>, opts: TOptions) => void;
|
||||
opts: TOptions;
|
||||
}) {
|
||||
event.preventDefault();
|
||||
const point = getTouchPoint(event);
|
||||
touchStartRef.current = { x: point.clientX, y: point.clientY, decided };
|
||||
onMouseDown(createTouchMouseDownEvent(point, event.currentTarget), opts);
|
||||
}
|
||||
|
||||
export function forwardCanvasTouchMove<TCurrentTarget>({
|
||||
event,
|
||||
touchStartRef,
|
||||
onCanvasMouseMove,
|
||||
}: {
|
||||
event: TouchEvent<TCurrentTarget>;
|
||||
touchStartRef: MutableCurrent<TouchDecisionState>;
|
||||
onCanvasMouseMove: (event: ReturnType<typeof createTouchCanvasPointerEvent>) => void;
|
||||
}) {
|
||||
const point = getTouchPoint(event);
|
||||
const decision = resolveTouchDragDecision(touchStartRef.current, point);
|
||||
touchStartRef.current = decision.nextState;
|
||||
if (!decision.shouldHandleDrag) return;
|
||||
onCanvasMouseMove(createTouchCanvasPointerEvent(point));
|
||||
}
|
||||
|
||||
export function forwardCanvasTouchEnd<TCurrentTarget>({
|
||||
event,
|
||||
onCanvasMouseUp,
|
||||
}: {
|
||||
event: TouchEvent<TCurrentTarget>;
|
||||
onCanvasMouseUp: (event: ReturnType<typeof createTouchCanvasPointerEvent>) => Promise<void> | void;
|
||||
}) {
|
||||
return onCanvasMouseUp(createTouchCanvasPointerEvent(getTouchPoint(event)));
|
||||
}
|
||||
@@ -26,6 +26,11 @@ import { resolveAllocationDragPosition, resolveProjectDragPosition } from "./tim
|
||||
import { attachDocumentMouseDrag } from "./timelineDocumentDrag.js";
|
||||
import { buildProjectShiftMutationInput, createProjectDragState } from "./timelineProjectDrag.js";
|
||||
import { beginProjectDragSession } from "./timelineProjectDragSession.js";
|
||||
import {
|
||||
forwardCanvasTouchEnd,
|
||||
forwardCanvasTouchMove,
|
||||
forwardTouchStartAsMouseDown,
|
||||
} from "./timelineTouchEvents.js";
|
||||
import {
|
||||
completeMultiSelectDraft,
|
||||
createMultiSelectState,
|
||||
@@ -34,16 +39,7 @@ import {
|
||||
import { beginCanvasMultiSelectSession } from "./timelineMultiSelectSession.js";
|
||||
import { reconcileOptimisticEntries } from "./timelineOptimisticAllocations.js";
|
||||
import { createRangeSelectionState, finalizeRangeSelection, updateRangeSelectionDraft } from "./timelineRangeSelection.js";
|
||||
import {
|
||||
createTouchCanvasPointerEvent,
|
||||
createTouchMouseDownEvent,
|
||||
type TouchCanvasPointerEvent,
|
||||
type TouchMouseDownEvent,
|
||||
} from "./timelineTouchAdapters.js";
|
||||
import {
|
||||
getTouchPoint,
|
||||
resolveTouchDragDecision,
|
||||
} from "./timelineTouch.js";
|
||||
import { type TouchCanvasPointerEvent, type TouchMouseDownEvent } from "./timelineTouchAdapters.js";
|
||||
|
||||
const DRAG_CLICK_THRESHOLD_PX = 5;
|
||||
|
||||
@@ -796,10 +792,13 @@ export function useTimelineDrag({
|
||||
endDate: Date;
|
||||
},
|
||||
) => {
|
||||
e.preventDefault();
|
||||
const point = getTouchPoint(e);
|
||||
touchStartRef.current = { x: point.clientX, y: point.clientY, decided: true };
|
||||
onProjectBarMouseDown(createTouchMouseDownEvent(point, e.currentTarget), opts);
|
||||
forwardTouchStartAsMouseDown({
|
||||
event: e,
|
||||
touchStartRef,
|
||||
decided: true,
|
||||
onMouseDown: onProjectBarMouseDown,
|
||||
opts,
|
||||
});
|
||||
},
|
||||
[onProjectBarMouseDown],
|
||||
);
|
||||
@@ -821,10 +820,13 @@ export function useTimelineDrag({
|
||||
scope?: AllocDragScope;
|
||||
},
|
||||
) => {
|
||||
e.preventDefault();
|
||||
const point = getTouchPoint(e);
|
||||
touchStartRef.current = { x: point.clientX, y: point.clientY, decided: true };
|
||||
onAllocMouseDown(createTouchMouseDownEvent(point, e.currentTarget), opts);
|
||||
forwardTouchStartAsMouseDown({
|
||||
event: e,
|
||||
touchStartRef,
|
||||
decided: true,
|
||||
onMouseDown: onAllocMouseDown,
|
||||
opts,
|
||||
});
|
||||
},
|
||||
[onAllocMouseDown],
|
||||
);
|
||||
@@ -838,33 +840,31 @@ export function useTimelineDrag({
|
||||
suggestedProjectId?: string;
|
||||
},
|
||||
) => {
|
||||
e.preventDefault();
|
||||
const point = getTouchPoint(e);
|
||||
touchStartRef.current = { x: point.clientX, y: point.clientY, decided: false };
|
||||
onRowMouseDown(createTouchMouseDownEvent(point, e.currentTarget), opts);
|
||||
forwardTouchStartAsMouseDown({
|
||||
event: e,
|
||||
touchStartRef,
|
||||
decided: false,
|
||||
onMouseDown: onRowMouseDown,
|
||||
opts,
|
||||
});
|
||||
},
|
||||
[onRowMouseDown],
|
||||
);
|
||||
|
||||
const onCanvasTouchMove = useCallback(
|
||||
(e: React.TouchEvent) => {
|
||||
const point = getTouchPoint(e);
|
||||
|
||||
// Scroll vs drag disambiguation: once decided, stick with the decision
|
||||
const decision = resolveTouchDragDecision(touchStartRef.current, point);
|
||||
touchStartRef.current = decision.nextState;
|
||||
if (!decision.shouldHandleDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
onCanvasMouseMove(createTouchCanvasPointerEvent(point));
|
||||
forwardCanvasTouchMove({
|
||||
event: e,
|
||||
touchStartRef,
|
||||
onCanvasMouseMove,
|
||||
});
|
||||
},
|
||||
[onCanvasMouseMove],
|
||||
);
|
||||
|
||||
const onCanvasTouchEnd = useCallback(
|
||||
async (e: React.TouchEvent) => {
|
||||
await onCanvasMouseUp(createTouchCanvasPointerEvent(getTouchPoint(e)));
|
||||
await forwardCanvasTouchEnd({ event: e, onCanvasMouseUp });
|
||||
},
|
||||
[onCanvasMouseUp],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user