feat(reader): swipe left/right to turn pages in single mode
Wire horizontal swipe into the mobile tap-zone overlay (single mode only, so continuous mode keeps native vertical scroll). A leftward swipe past the threshold turns to the next page, rightward to the previous; a mostly- vertical drag is ignored so panning a tall page never turns it. The gesture suppresses the synthesized zone tap so it doesn't double-fire. Swipe classification is a pure, unit-tested helper; e2e drives real touch pointer events for horizontal (both directions) and vertical cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { swipeDirection } from '$lib/swipe';
|
||||
|
||||
let {
|
||||
onPrev,
|
||||
@@ -27,6 +28,9 @@
|
||||
|
||||
let pressTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let pressStart: { x: number; y: number } | null = null;
|
||||
// Swipe origin — tracked for every touch pointerdown (independent of the
|
||||
// long-press timer) so a horizontal drag can turn the page.
|
||||
let swipeStart: { x: number; y: number } | null = null;
|
||||
// Self-expiring suppression: cleared either by an actual click
|
||||
// reaching `withSuppress` or by the SUPPRESS_EXPIRY_MS fallback
|
||||
// timer below. The latter matters when the user lifts off the
|
||||
@@ -54,8 +58,11 @@
|
||||
}
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
if (!onLongPress) return;
|
||||
if (e.pointerType !== 'touch') return;
|
||||
// Record the swipe origin for every touch, whether or not long-press
|
||||
// is wired up.
|
||||
swipeStart = { x: e.clientX, y: e.clientY };
|
||||
if (!onLongPress) return;
|
||||
clearPress();
|
||||
pressStart = { x: e.clientX, y: e.clientY };
|
||||
const startX = e.clientX;
|
||||
@@ -79,17 +86,35 @@
|
||||
if (Math.hypot(dx, dy) > MOVE_TOLERANCE) clearPress();
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
function onPointerUp(e: PointerEvent) {
|
||||
if (swipeStart && e.pointerType === 'touch') {
|
||||
const dir = swipeDirection(e.clientX - swipeStart.x, e.clientY - swipeStart.y);
|
||||
if (dir) {
|
||||
// A swipe consumed the gesture — suppress the click the
|
||||
// browser may synthesize on the zone so it doesn't also fire
|
||||
// that zone's tap action (reusing the long-press suppressor).
|
||||
suppressNextClick = true;
|
||||
if (suppressExpiryTimer != null) clearTimeout(suppressExpiryTimer);
|
||||
suppressExpiryTimer = setTimeout(() => {
|
||||
suppressNextClick = false;
|
||||
suppressExpiryTimer = null;
|
||||
}, SUPPRESS_EXPIRY_MS);
|
||||
(dir === 'next' ? onNext : onPrev)();
|
||||
}
|
||||
}
|
||||
swipeStart = null;
|
||||
clearPress();
|
||||
}
|
||||
|
||||
function onPointerCancel() {
|
||||
swipeStart = null;
|
||||
clearPress();
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
// Scrolling while pressed almost always means the user is
|
||||
// panning, not deliberately holding. Cancel.
|
||||
swipeStart = null;
|
||||
clearPress();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user