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) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 22:14:19 +02:00
parent 7bb03be51a
commit eb16564b84

View File

@@ -178,6 +178,7 @@ export function Timeline() {
const sortBy = useFilterStore((s) => s.sortBy) const sortBy = useFilterStore((s) => s.sortBy)
const groupBy = useFilterStore((s) => s.groupBy) const groupBy = useFilterStore((s) => s.groupBy)
const viewMode = usePhotoStore((s) => s.viewMode)
// Calculate number of columns based on container width. // Calculate number of columns based on container width.
const columns = useMemo(() => { const columns = useMemo(() => {
@@ -322,7 +323,14 @@ export function Timeline() {
// Handle keyboard shortcuts for photo navigation. Operates on the // Handle keyboard shortcuts for photo navigation. Operates on the
// grouped grid the user sees, so a half-full last row of a group // grouped grid the user sees, so a half-full last row of a group
// doesn't make ArrowDown skip into the wrong place. // 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(() => { useEffect(() => {
if (viewMode !== 'grid') return
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (photoRows.length === 0) return if (photoRows.length === 0) return
const target = e.target as HTMLElement | null const target = e.target as HTMLElement | null
@@ -413,7 +421,7 @@ export function Timeline() {
window.addEventListener('keydown', handleKeyDown) window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [photoRows, photos, selectedPhotos, activePhotoId]) }, [viewMode, photoRows, photos, selectedPhotos, activePhotoId])
if (isLoading) { if (isLoading) {
return ( return (