fix(gallery): use ref for swipe-start to avoid stale-closure miss (#332)
Found via real-browser verification: with useState the prior commit's handleTouchEnd captures swipeStart from its render closure, so when touchstart and touchend fire inside the same React batch (fast swipe, synthetic events, or a tight render cycle) the end handler reads the stale null and skips navigation. useRef sidesteps the closure entirely and is the right primitive for cross-event scratchpad state anyway. Verified in a 4-photo gallery on mobile-emulation (390x844 touch): - left swipe (-200px) advances 1/4 → 2/4 - right swipe (+200px) returns 2/4 → 1/4 - 20px swipe (under threshold) does not navigate - vertical swipe (dy 300, dx 20) does not navigate
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useDevToolsProtection } from '../../hooks/useDevToolsProtection';
|
||||
import { X, ChevronLeft, ChevronRight, Download, ZoomIn, ZoomOut, MessageSquare, Heart, Star, Loader2 } from 'lucide-react';
|
||||
import type { Photo } from '../../types';
|
||||
@@ -47,7 +47,9 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||||
const [touchDistance, setTouchDistance] = useState<number | null>(null);
|
||||
const [swipeStart, setSwipeStart] = useState<{ x: number; y: number; t: number } | null>(null);
|
||||
// Ref (not state) so handleTouchEnd reads the value set by handleTouchStart
|
||||
// even when both fire in the same render batch.
|
||||
const swipeStartRef = useRef<{ x: number; y: number; t: number } | null>(null);
|
||||
const [showFeedback, setShowFeedback] = useState(initialShowFeedback);
|
||||
const [isSmallScreen, setIsSmallScreen] = useState<boolean>(typeof window !== 'undefined' ? window.innerWidth < 640 : false);
|
||||
const [feedbackSettings, setFeedbackSettings] = useState<{
|
||||
@@ -374,10 +376,10 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
touch2.clientY - touch1.clientY
|
||||
);
|
||||
setTouchDistance(distance);
|
||||
setSwipeStart(null);
|
||||
swipeStartRef.current = null;
|
||||
} else if (e.touches.length === 1 && zoom <= 1) {
|
||||
const t = e.touches[0];
|
||||
setSwipeStart({ x: t.clientX, y: t.clientY, t: Date.now() });
|
||||
swipeStartRef.current = { x: t.clientX, y: t.clientY, t: Date.now() };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -399,18 +401,19 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
|
||||
const handleTouchEnd = (e: React.TouchEvent) => {
|
||||
setTouchDistance(null);
|
||||
if (swipeStart && e.changedTouches.length > 0) {
|
||||
const start = swipeStartRef.current;
|
||||
if (start && e.changedTouches.length > 0) {
|
||||
const t = e.changedTouches[0];
|
||||
const dx = t.clientX - swipeStart.x;
|
||||
const dy = t.clientY - swipeStart.y;
|
||||
const dt = Date.now() - swipeStart.t;
|
||||
const dx = t.clientX - start.x;
|
||||
const dy = t.clientY - start.y;
|
||||
const dt = Date.now() - start.t;
|
||||
// Horizontal swipe: > 50px and dominant over vertical, completed in < 600ms.
|
||||
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.2 && dt < 600) {
|
||||
if (dx > 0) goToPrevious();
|
||||
else goToNext();
|
||||
}
|
||||
}
|
||||
setSwipeStart(null);
|
||||
swipeStartRef.current = null;
|
||||
};
|
||||
|
||||
// Apply protection class to the lightbox container
|
||||
|
||||
Reference in New Issue
Block a user