All files / src/hooks useTouchNavigation.ts

100% Statements 35/35
100% Branches 8/8
100% Functions 5/5
100% Lines 33/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86                                  2x                               19x 19x 19x 19x   19x 19x 19x 19x   19x 17x 17x   17x 10x 10x 10x     17x 10x 10x 10x     10x     8x   6x 2x 2x     4x 3x   1x       17x 17x   17x 17x 17x        
/**
 * useTouchNavigation Hook
 * Story 10.8 mobile enhancement: swipe left/right to navigate sections.
 *
 * Attaches passive window-level touch listeners so it never blocks scroll
 * on other pages. The hook fires:
 *   - Swipe left  (dx < -threshold, |dx| > |dy|) → onNext
 *   - Swipe right (dx > +threshold, |dx| > |dy|) → onPrev
 *   - Any qualifying swipe while blank is active   → onToggleBlank (dismiss break)
 *
 * Tap detection (movement < TAP_MAX_MOVE) is intentionally NOT handled here —
 * that belongs to the TouchZones component which owns tap zones for left/right/bottom.
 */
 
import { useEffect, useRef } from 'react';
 
/** Minimum horizontal distance (px) that counts as a swipe. */
const SWIPE_THRESHOLD = 50;
 
interface UseTouchNavigationOptions {
  isBlankActive: boolean;
  onNext: () => void;
  onPrev: () => void;
  onToggleBlank: () => void;
}
 
export function useTouchNavigation({
  isBlankActive,
  onNext,
  onPrev,
  onToggleBlank,
}: UseTouchNavigationOptions): void {
  // Stable refs so the listener never needs to be re-registered when callbacks change
  const isBlankRef = useRef(isBlankActive);
  const onNextRef = useRef(onNext);
  const onPrevRef = useRef(onPrev);
  const onToggleBlankRef = useRef(onToggleBlank);
 
  isBlankRef.current = isBlankActive;
  onNextRef.current = onNext;
  onPrevRef.current = onPrev;
  onToggleBlankRef.current = onToggleBlank;
 
  useEffect(() => {
    let startX = 0;
    let startY = 0;
 
    const handleTouchStart = (e: TouchEvent) => {
      const t = e.touches[0];
      startX = t.clientX;
      startY = t.clientY;
    };
 
    const handleTouchEnd = (e: TouchEvent) => {
      const t = e.changedTouches[0];
      const dx = t.clientX - startX;
      const dy = t.clientY - startY;
 
      // Ignore taps and near-taps (too little movement)
      if (Math.abs(dx) < SWIPE_THRESHOLD) return;
 
      // Ignore swipes that are not strictly more horizontal than vertical
      if (Math.abs(dy) >= Math.abs(dx)) return;
 
      if (isBlankRef.current) {
        onToggleBlankRef.current();
        return;
      }
 
      if (dx < 0) {
        onNextRef.current(); // swipe left → advance
      } else {
        onPrevRef.current(); // swipe right → go back
      }
    };
 
    window.addEventListener('touchstart', handleTouchStart, { passive: true });
    window.addEventListener('touchend', handleTouchEnd, { passive: true });
 
    return () => {
      window.removeEventListener('touchstart', handleTouchStart);
      window.removeEventListener('touchend', handleTouchEnd);
    };
  }, []); // empty: listeners are stable; refs carry current values
}