fix(ui): a board pinch-zoom no longer triggers swipe-back
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 58s

The edge swipe-back armed on the first finger and fired on release even when a
second finger had joined (a pinch-zoom — the board is not .zoomed yet at the first
touch, so the hit-test could not skip it). Track the live pointer count in the
capture phase and cancel the swipe the moment a second pointer joins; the back
navigation fires only for a lone finger. Synthetic-PointerEvent e2e covers both.
This commit is contained in:
Ilia Denisov
2026-06-15 13:52:52 +02:00
parent 30a7c24140
commit 800a692766
2 changed files with 86 additions and 10 deletions
+41 -10
View File
@@ -42,22 +42,53 @@
// a zoomed-in board (it pans), and text inputs.
const EDGE_FRACTION = 0.5;
const SWIPE_SKIP = '[data-rack], .cell.pending, .viewport.zoomed, input, textarea, select';
// A pinch-zoom on the board is two fingers; its second finger lands after the gesture began
// (the board is not yet .zoomed at the first touch, so the hit-test above can't skip it), and
// releasing it would otherwise read as a horizontal back-swipe. Track the live pointer count
// (in the capture phase, so we see every touch even if the board stops propagation) and cancel
// the swipe the moment a second finger joins. The swipe fires only for a lone finger.
$effect(() => {
let startX = 0;
let startY = 0;
let active = 0; // pointers currently down
let tracking = false; // a candidate single-finger edge swipe is in progress
let pinched = false; // a second finger joined — not a back-swipe
function onDown(e: PointerEvent) {
active++;
if (active > 1) {
pinched = true;
return;
}
tracking = false;
pinched = false;
if (!back || e.pointerType === 'mouse' || e.clientX > window.innerWidth * EDGE_FRACTION) return;
if (document.elementFromPoint(e.clientX, e.clientY)?.closest(SWIPE_SKIP)) return;
const x0 = e.clientX;
const y0 = e.clientY;
const onUp = (ev: PointerEvent) => {
window.removeEventListener('pointerup', onUp, true);
const dx = ev.clientX - x0;
const dy = ev.clientY - y0;
if (back && dx > 64 && Math.abs(dx) > Math.abs(dy) * 1.4) navigate(back);
};
window.addEventListener('pointerup', onUp, true);
startX = e.clientX;
startY = e.clientY;
tracking = true;
}
function onUp(e: PointerEvent) {
if (active > 0) active--;
if (active > 0) return; // wait until every finger has lifted
if (tracking && !pinched && back) {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (dx > 64 && Math.abs(dx) > Math.abs(dy) * 1.4) navigate(back);
}
tracking = false;
pinched = false;
}
window.addEventListener('pointerdown', onDown, true);
return () => window.removeEventListener('pointerdown', onDown, true);
window.addEventListener('pointerup', onUp, true);
window.addEventListener('pointercancel', onUp, true);
return () => {
window.removeEventListener('pointerdown', onDown, true);
window.removeEventListener('pointerup', onUp, true);
window.removeEventListener('pointercancel', onUp, true);
};
});
</script>