Skip to content

fix: cancel timeline clip interactions when the pointer stream is interrupted#135

Merged
dazzatronus merged 2 commits into
mainfrom
derk/fix-stuck-clip-drag
Jul 7, 2026
Merged

fix: cancel timeline clip interactions when the pointer stream is interrupted#135
dazzatronus merged 2 commits into
mainfrom
derk/fix-stuck-clip-drag

Conversation

@dazzatronus

@dazzatronus dazzatronus commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What

A timeline clip drag could stick to the cursor. If the browser never delivered pointerup (for example, releasing over a native context menu, switching apps mid-drag, or a touch scroll takeover), the interaction state machine remained in dragging, so every subsequent hover move kept repositioning the clip. Clicking to recover could start a new interaction over the stale one, leaving the clip stranded at position: fixed with pointer-events: none until a re-render.

How

  • Only the primary button starts drags, resizes, and selection clears
  • The tracks container captures the pointer, so the release is delivered even outside the window
  • pointercancel, or any move without the primary button held, cancels the interaction: the clip is restored to its original position, the ghost is removed, and no command is executed
  • A stale interaction is cancelled before a new pointerdown starts one, and on dispose()
  • Events are filtered by pointer id, so a second touch cannot drive or complete another pointer's drag
  • touch-action: none on clips stops the browser from reclaiming touch drags for scrolling (empty track area still scrolls)

Verify

Normal drag/drop/resize behaviour is unchanged: 29 unit tests (8 new), plus drag-and-drop and drag-cancel exercised in-browser on this branch. To reproduce the lost pointerup deterministically, paste this in the console on any page showing the timeline — no manual dragging needed:

await (async () => {
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const clips = [...document.querySelectorAll(".ss-clip")];
  const clip = clips[1] || clips[0];
  if (!clip) return console.log("no .ss-clip found");
  const rect = clip.getBoundingClientRect();
  const opt = (x, y, buttons) => ({ bubbles: true, pointerId: 1, isPrimary: true, button: 0, buttons, clientX: x, clientY: y });
  const x = rect.left + Math.min(40, rect.width / 2), y = rect.top + rect.height / 2;
  clip.dispatchEvent(new PointerEvent("pointerdown", opt(x, y, 1)));
  for (let i = 1; i <= 6; i++) { document.dispatchEvent(new PointerEvent("pointermove", opt(x + i * 30, y - i * 25, 1))); await sleep(20); }
  // the release is never dispatched — same as a pointerup eaten by a native menu or app switch
  document.dispatchEvent(new PointerEvent("pointermove", opt(x + 400, y - 300, 0)));
  await sleep(30);
  console.log(clips.some(c => c.style.position === "fixed")
    ? "BUG: clip is glued to the cursor — move the mouse to watch it follow; click to force a drop"
    : "FIXED: drag self-cancelled and the clip was restored");
})();

Verified on 2.13.0: prints BUG and the clip visibly chases the real mouse. On this branch: prints FIXED and the clip snaps back to its original position with no edit applied.

@dazzatronus dazzatronus self-assigned this Jul 7, 2026
@dazzatronus dazzatronus added the bug Something isn't working label Jul 7, 2026
@dazzatronus dazzatronus merged commit 19404cd into main Jul 7, 2026
1 check passed
@dazzatronus dazzatronus deleted the derk/fix-stuck-clip-drag branch July 7, 2026 03:25
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 2.13.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant