From bce79867e4ee2188416e3c405a602e36e410273e Mon Sep 17 00:00:00 2001 From: Masahiko AMANO Date: Thu, 11 Jun 2026 18:01:35 +0300 Subject: [PATCH] fix(frontend): scroll the grid to follow the keyboard focus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arrowing up/down moved the focus ring but the view didn't follow: the card was scrolled with scrollIntoView({block:'nearest'}), which aligns to the scroller's edges and is unaware of the fixed bottom navbar overlaying the scroll area — so the newly focused row slid under the navbar. Replace it with a manual scroll that keeps the focused card inside the scroller with a top margin and a bottom margin sized for the navbar. Co-Authored-By: Claude Opus 4.8 --- frontend/src/routes/files/+page.svelte | 29 ++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/frontend/src/routes/files/+page.svelte b/frontend/src/routes/files/+page.svelte index 6402f40..a408875 100644 --- a/frontend/src/routes/files/+page.svelte +++ b/frontend/src/routes/files/+page.svelte @@ -71,12 +71,29 @@ focusedId = files[next]?.id ?? null; if (next >= files.length - gridCols() * 2 && hasMore && !loading) void loadMore(); const id = focusedId; - requestAnimationFrame(() => { - const idx = files.findIndex((f) => f.id === id); - scrollContainer - ?.querySelector(`[data-file-index="${idx}"]`) - ?.scrollIntoView({ block: 'nearest' }); - }); + 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); + } } // Action keys operate on the selection; with nothing selected they fall back to