From 48ea1fe7209c638ace8aded555d633dc3b6de648 Mon Sep 17 00:00:00 2001 From: Masahiko AMANO Date: Thu, 11 Jun 2026 20:19:08 +0300 Subject: [PATCH] fix(frontend): make the grid actually follow keyboard focus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous attempt wrote scrollContainer.scrollTop, but
isn't the scroll element here (the window/document scrolls, as getScroller and the infinite-scroll listeners assume) — so it was a no-op and the grid stopped following the focus. Move back to scrollIntoView({block:'nearest'}), which scrolls whatever element actually scrolls, and give the card scroll-margin-top/-bottom so it clears the sticky header and the fixed navbar instead of sliding under them. Co-Authored-By: Claude Opus 4.8 --- .../src/lib/components/file/FileCard.svelte | 4 +++ frontend/src/routes/files/+page.svelte | 33 ++++++------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/frontend/src/lib/components/file/FileCard.svelte b/frontend/src/lib/components/file/FileCard.svelte index 51712fb..7ea084e 100644 --- a/frontend/src/lib/components/file/FileCard.svelte +++ b/frontend/src/lib/components/file/FileCard.svelte @@ -174,6 +174,10 @@ flex-shrink: 0; user-select: none; -webkit-user-select: none; + /* Keyboard scrollIntoView leaves room for the sticky header above and the + fixed bottom navbar below, so the focused card never hides under them. */ + scroll-margin-top: 52px; + scroll-margin-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); } .thumb { diff --git a/frontend/src/routes/files/+page.svelte b/frontend/src/routes/files/+page.svelte index fd00616..ebd7e4e 100644 --- a/frontend/src/routes/files/+page.svelte +++ b/frontend/src/routes/files/+page.svelte @@ -71,29 +71,16 @@ focusedId = files[next]?.id ?? null; if (next >= files.length - gridCols() * 2 && hasMore && !loading) void loadMore(); const id = focusedId; - requestAnimationFrame(() => keepFocusedInView(id)); - } - - // Keep the focused card within the scroller, leaving a margin at the bottom for - // the fixed navbar (which overlaps the scroll area and otherwise hides the row - // the focus moves onto). scrollIntoView can't account for that overlay. - const FOCUS_MARGIN_TOP = 8; - const FOCUS_MARGIN_BOTTOM = 72; // ~navbar height + gap - - function keepFocusedInView(id: string | null) { - if (!id || !scrollContainer) return; - const idx = files.findIndex((f) => f.id === id); - const card = scrollContainer.querySelector(`[data-file-index="${idx}"]`); - if (!card) return; - const cardRect = card.getBoundingClientRect(); - const scRect = scrollContainer.getBoundingClientRect(); - const top = cardRect.top - scRect.top; - const bottom = cardRect.bottom - scRect.top; - if (top < FOCUS_MARGIN_TOP) { - scrollContainer.scrollTop += top - FOCUS_MARGIN_TOP; - } else if (bottom > scRect.height - FOCUS_MARGIN_BOTTOM) { - scrollContainer.scrollTop += bottom - (scRect.height - FOCUS_MARGIN_BOTTOM); - } + // scrollIntoView scrolls whichever element actually scrolls (the window + // here, not
), so the grid follows the focus. The card's + // scroll-margin-bottom leaves room for the fixed navbar so it doesn't slide + // underneath. + requestAnimationFrame(() => { + const idx = files.findIndex((f) => f.id === id); + scrollContainer + ?.querySelector(`[data-file-index="${idx}"]`) + ?.scrollIntoView({ block: 'nearest' }); + }); } // Action keys operate on the selection; with nothing selected they fall back to