fix(client): handle pointercancel and make drag-start idempotent
Add onCancel handler for browser/OS gesture takeover (scroll, palm rejection) that previously leaked window listeners and left drag stuck. Extract detach() helper called by onUp, onCancel, and start() for idempotency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,12 @@ function makeDrag() {
|
|||||||
let suppressClickOn: Square | null = null;
|
let suppressClickOn: Square | null = null;
|
||||||
const THRESHOLD = 6;
|
const THRESHOLD = 6;
|
||||||
|
|
||||||
|
function detach() {
|
||||||
|
window.removeEventListener('pointermove', onMove);
|
||||||
|
window.removeEventListener('pointerup', onUp);
|
||||||
|
window.removeEventListener('pointercancel', onCancel);
|
||||||
|
}
|
||||||
|
|
||||||
function onMove(e: PointerEvent) {
|
function onMove(e: PointerEvent) {
|
||||||
if (!state.active) return;
|
if (!state.active) return;
|
||||||
state.x = e.clientX;
|
state.x = e.clientX;
|
||||||
@@ -36,15 +42,23 @@ function makeDrag() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pointercancel fires instead of pointerup when the browser/OS takes over
|
||||||
|
// the gesture (common on touch). Abort the drag: clean up, drop nothing.
|
||||||
|
function onCancel() {
|
||||||
|
detach();
|
||||||
|
state.active = null;
|
||||||
|
state.moved = false;
|
||||||
|
}
|
||||||
|
|
||||||
function onUp(e: PointerEvent) {
|
function onUp(e: PointerEvent) {
|
||||||
window.removeEventListener('pointermove', onMove);
|
detach();
|
||||||
window.removeEventListener('pointerup', onUp);
|
|
||||||
const src = state.active;
|
const src = state.active;
|
||||||
const wasDrag = state.moved;
|
const wasDrag = state.moved;
|
||||||
state.active = null;
|
state.active = null;
|
||||||
state.moved = false;
|
state.moved = false;
|
||||||
if (!src || !wasDrag) return; // a tap — the board click handler deals with it
|
if (!src || !wasDrag) return; // a tap — the board click handler deals with it
|
||||||
|
|
||||||
|
// elementFromPoint returns null off-viewport — treated as an off-board drop.
|
||||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
const sqEl = el?.closest('[data-square]') as HTMLElement | null;
|
const sqEl = el?.closest('[data-square]') as HTMLElement | null;
|
||||||
const target = sqEl?.dataset.square as Square | undefined;
|
const target = sqEl?.dataset.square as Square | undefined;
|
||||||
@@ -61,6 +75,7 @@ function makeDrag() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function start(src: DragSource, e: PointerEvent) {
|
function start(src: DragSource, e: PointerEvent) {
|
||||||
|
detach(); // idempotency — drop any listeners from an unfinished prior drag
|
||||||
suppressClickOn = null;
|
suppressClickOn = null;
|
||||||
state.active = src;
|
state.active = src;
|
||||||
state.x = startX = e.clientX;
|
state.x = startX = e.clientX;
|
||||||
@@ -68,6 +83,7 @@ function makeDrag() {
|
|||||||
state.moved = false;
|
state.moved = false;
|
||||||
window.addEventListener('pointermove', onMove);
|
window.addEventListener('pointermove', onMove);
|
||||||
window.addEventListener('pointerup', onUp);
|
window.addEventListener('pointerup', onUp);
|
||||||
|
window.addEventListener('pointercancel', onCancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The board calls this first in its square-click handler. */
|
/** The board calls this first in its square-click handler. */
|
||||||
|
|||||||
Reference in New Issue
Block a user