From eb16564b84def93b9272a3a15fe02375e4767cff Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 8 Apr 2026 22:14:19 +0200 Subject: [PATCH] fix: gate Timeline arrow keys to grid mode PreviewView mounts its own arrow handlers via useHotkeys. The Timeline also installed a window-level keydown listener for grid arrow nav, with no viewMode check, so in preview mode BOTH handlers fired on every arrow press and raced to call setActivePhoto. The grid handler walks photoRows (grid cells) while preview walks the visible-order array, and whichever store update landed last won, making preview nav land on the wrong photo. Telltale: Shift+arrow worked because PreviewView's plain useHotkeys ('left'/'right') doesn't match Shift+arrow, so only Timeline fired and its visual-grid path got the right neighbor. Fix: early-return Timeline's keyboard effect when viewMode !== 'grid'. The listener stays attached to viewMode in the dep array so it re-engages instantly on closePreview. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/timeline/Timeline.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/timeline/Timeline.tsx b/frontend/src/components/timeline/Timeline.tsx index 26ba45e..2dd6d36 100644 --- a/frontend/src/components/timeline/Timeline.tsx +++ b/frontend/src/components/timeline/Timeline.tsx @@ -178,6 +178,7 @@ export function Timeline() { const sortBy = useFilterStore((s) => s.sortBy) const groupBy = useFilterStore((s) => s.groupBy) + const viewMode = usePhotoStore((s) => s.viewMode) // Calculate number of columns based on container width. const columns = useMemo(() => { @@ -322,7 +323,14 @@ export function Timeline() { // Handle keyboard shortcuts for photo navigation. Operates on the // grouped grid the user sees, so a half-full last row of a group // doesn't make ArrowDown skip into the wrong place. + // + // Inert in preview mode — PreviewView mounts its own arrow handlers, + // and a window-level grid handler firing alongside them used to race + // against PreviewView's setActivePhoto, landing the user on the wrong + // photo. The grid handler stays attached so it can re-engage the + // moment the user closes preview. useEffect(() => { + if (viewMode !== 'grid') return const handleKeyDown = (e: KeyboardEvent) => { if (photoRows.length === 0) return const target = e.target as HTMLElement | null @@ -413,7 +421,7 @@ export function Timeline() { window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [photoRows, photos, selectedPhotos, activePhotoId]) + }, [viewMode, photoRows, photos, selectedPhotos, activePhotoId]) if (isLoading) { return (