From 6ec9f5ec61534da6fc2a316e7ef93f642b198735 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 23 Mar 2026 16:15:43 -0700 Subject: [PATCH] fix(canvas): prevent drag/snap/selection on shape content interactions FolkShape now uses stopImmediatePropagation for pointerdown on non-drag targets (scrollable content, inputs, buttons) so canvas selection and snap listeners don't fire. Canvas capture-phase listener now checks composedPath to only track actual drag targets (host, handles, headers). Co-Authored-By: Claude Opus 4.6 --- lib/folk-shape.ts | 8 ++++++-- website/canvas.html | 11 +++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/folk-shape.ts b/lib/folk-shape.ts index d183868..4bbda8e 100644 --- a/lib/folk-shape.ts +++ b/lib/folk-shape.ts @@ -576,10 +576,14 @@ export class FolkShape extends FolkElement { const isDragHandle = target?.closest?.(".header, [data-drag]") !== null; if (event instanceof PointerEvent) { + // For pointerdown on non-drag targets (content areas, inputs, etc.), + // stop immediate propagation so canvas selection/snap listeners don't fire. + if (event.type === "pointerdown" && target !== this && !handle && !isDragHandle) { + event.stopImmediatePropagation(); + return; + } event.stopPropagation(); if (event.type === "pointerdown") { - // Allow drag from: the host itself, a handle, or a drag handle element - if (target !== this && !handle && !isDragHandle) return; // If clicking on the shape body (not a handle), check if a text input // is under the pointer — if so, enter edit mode immediately instead of dragging. diff --git a/website/canvas.html b/website/canvas.html index 9d265a5..f3dc736 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -3777,8 +3777,15 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest const shapeLastPos = new Map(); function setupShapeEventListeners(shape) { - // Track position for group dragging - shape.addEventListener("pointerdown", () => { + // Track position for group dragging — only for actual drag targets + // (host element, resize/rotation handles, or drag handles like .header) + shape.addEventListener("pointerdown", (e) => { + const target = e.composedPath()[0]; + const isHost = target === shape; + const isHandle = target?.getAttribute?.("part")?.startsWith?.("resize") || + target?.getAttribute?.("part")?.startsWith?.("rotation"); + const isDragHandle = target?.closest?.(".header, [data-drag]") !== null; + if (!isHost && !isHandle && !isDragHandle) return; shapeLastPos.set(shape.id, { x: shape.x, y: shape.y }); onShapeMoveStart(shape); }, { capture: true });