ui(timeline): auto-focus first photo on every section switch

The previous auto-focus guard was one-shot for the lifetime of the
component, so switching from All Photos → Discarded (or any other
filter-based section) carried over the old activePhotoId — and if it
wasn't in the new view, nothing was focused at all. A new effect
watches currentSection and, on any change (or fresh mount after a
Duplicates/Memories detour), resets the guard and clears the stale
selection so the existing auto-focus picks the first visible photo of
the new view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 22:21:29 +02:00
parent b6be24c357
commit 3c022cef68

View File

@@ -207,9 +207,11 @@ export function Timeline() {
const prevViewModeRef = useRef(viewMode)
// Auto-focus the first photo on initial grid load so arrow-key nav
// works immediately without a pre-click. One-shot — after the user
// explicitly clears the selection (Escape), we don't re-focus, so
// the metadata sidebar can collapse and stay collapsed.
// works immediately without a pre-click. One-shot per section — after
// the user explicitly clears the selection (Escape), we don't
// re-focus within the same section, so the metadata sidebar can
// collapse and stay collapsed. Reset by the section-change effect
// below so switching views (e.g. All → Discarded) re-focuses.
const didAutoFocusRef = useRef(false)
// visibleSequence isn't in scope yet (derived below from items). Read
// it through a ref so this effect can focus the first *visually*
@@ -230,6 +232,23 @@ export function Timeline() {
selectPhoto(firstVisible)
}, [viewMode, activePhotoId, photos, selectPhoto])
// On section switch (e.g. All Photos → Discarded) or fresh mount,
// reset the one-shot guard and clear the stale active photo so the
// effect above picks the first photo of the new view once its query
// resolves. The previous section's activePhotoId almost never
// belongs to the new section — carrying it over leaves the metadata
// sidebar showing something that isn't even in the visible grid.
// Ref starts null so the first mount after a Duplicates/Memories
// detour also trips this path.
const clearSelection = usePhotoStore((s) => s.clearSelection)
const prevSectionRef = useRef<string | null>(null)
useEffect(() => {
if (prevSectionRef.current === currentSection) return
prevSectionRef.current = currentSection
didAutoFocusRef.current = false
clearSelection()
}, [currentSection, clearSelection])
// Membership in the active heap (drives the green tint on each
// thumbnail). Subscribed once at this level so we don't have hundreds
// of thumbnails each subscribing to the same query.