diff --git a/src/components/timeline/interaction/interaction-controller.ts b/src/components/timeline/interaction/interaction-controller.ts index b87c50a..d7e411f 100644 --- a/src/components/timeline/interaction/interaction-controller.ts +++ b/src/components/timeline/interaction/interaction-controller.ts @@ -181,7 +181,10 @@ export class InteractionController implements TimelineInteractionRegistration { const clip = this.stateManager.getClipAt(clipRef.trackIndex, clipRef.clipIndex); if (!clip) return; - this.beginPointerInteraction(e); + // No pointer capture yet: capturing retargets the eventual click event to + // the tracks container, which would break click handlers on clip children + // (e.g. the mask badge). Capture happens if this becomes a real drag. + this.activePointerId = e.pointerId; this.state = createPendingState({ x: e.clientX, y: e.clientY }, clipRef, clip.config.start); } @@ -297,6 +300,7 @@ export class InteractionController implements TimelineInteractionRegistration { const ghost = createDragGhost(clip.config.length, clipAssetType, sourceTrackHeight, pps); this.feedbackElements.container.appendChild(ghost); + this.beginPointerInteraction(e); this.state = createDraggingState(state, clipElement, ghost, dragOffsetX, dragOffsetY, originalStyles, clip.config.length, e.altKey); // Position ghost at current clip position initially diff --git a/tests/interaction-controller.test.ts b/tests/interaction-controller.test.ts index 4d92c94..a9779fd 100644 --- a/tests/interaction-controller.test.ts +++ b/tests/interaction-controller.test.ts @@ -1079,6 +1079,22 @@ describe("InteractionController", () => { document.dispatchEvent(createPointerEvent("pointermove", { clientX: 100, clientY: 20 })); }; + it("captures the pointer only when a drag starts, not on plain pointerdown", () => { + createController(); + const clipElement = mockDOM.tracksContainer.querySelector(".ss-clip") as HTMLElement; + const setPointerCapture = jest.fn(); + mockDOM.tracksContainer.setPointerCapture = setPointerCapture; + + // Plain pointerdown (a click on the clip or a child like the mask badge) + // must not capture, or the resulting click event retargets away from the child + clipElement.dispatchEvent(createPointerEvent("pointerdown", { clientX: 50, clientY: 20 })); + expect(setPointerCapture).not.toHaveBeenCalled(); + + document.dispatchEvent(createPointerEvent("pointermove", { clientX: 100, clientY: 20 })); + expect(controller.isDragging(0, 0)).toBe(true); + expect(setPointerCapture).toHaveBeenCalled(); + }); + it("cancels drag on pointercancel, restoring the clip without executing commands", () => { createController(); const clipElement = mockDOM.tracksContainer.querySelector(".ss-clip") as HTMLElement;