diff --git a/frontend/src/lib/person/genealogy/panZoomGestures.ts b/frontend/src/lib/person/genealogy/panZoomGestures.ts index 8173d93c..45f6bf52 100644 --- a/frontend/src/lib/person/genealogy/panZoomGestures.ts +++ b/frontend/src/lib/person/genealogy/panZoomGestures.ts @@ -104,11 +104,11 @@ export const panZoomGestures: Action = (no const onPointerDown = (e: PointerEvent) => { cancelInertia(); - try { - node.setPointerCapture(e.pointerId); - } catch { - /* pointer not capturable (e.g. synthetic event) — drag still works */ - } + // NB: do NOT capture the pointer here — capturing on pointerdown makes the + // browser dispatch the trailing `click` at this element instead of the + // node under the pointer, which silently breaks node selection (a tap must + // still reach the node's onclick). Capture is deferred to the first move + // that crosses the drag threshold (see onPointerMove). pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }); p.onGestureStart?.(); @@ -146,7 +146,17 @@ export const panZoomGestures: Action = (no if (!dragging) return; const dxPx = e.clientX - lastX; const dyPx = e.clientY - lastY; - if (!moved && Math.hypot(dxPx, dyPx) >= DRAG_THRESHOLD_PX) moved = true; + if (!moved && Math.hypot(dxPx, dyPx) >= DRAG_THRESHOLD_PX) { + // A real drag has started — now capture so we keep receiving move/up + // even if the pointer leaves the canvas. (Deferred from pointerdown so + // taps still select nodes.) + moved = true; + try { + node.setPointerCapture(e.pointerId); + } catch { + /* pointer not capturable (e.g. synthetic event) — drag still works */ + } + } const { dx, dy } = screenDeltaToSvg( dxPx,