From 3c022cef68196a3a39d70715fd474eb1b0d3704f Mon Sep 17 00:00:00 2001 From: dtoro Date: Tue, 21 Apr 2026 22:21:29 +0200 Subject: [PATCH] ui(timeline): auto-focus first photo on every section switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/src/components/timeline/Timeline.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/timeline/Timeline.tsx b/frontend/src/components/timeline/Timeline.tsx index ae29ded..b6971f9 100644 --- a/frontend/src/components/timeline/Timeline.tsx +++ b/frontend/src/components/timeline/Timeline.tsx @@ -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(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.