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