From 5255880d78cb9ffd26bec6fe02fd98c355e65677 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 21 Apr 2026 12:01:51 +0200 Subject: [PATCH] feat: preserve scroll position across case-action redirects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking close/reopen or any bulk form submits a POST and follows the 303 to the listing, which is a fresh navigation — browsers do not restore scroll on navigation, only on reload(). A user scrolled to row 30 of a long list loses focus and has to scroll back manually. Fix: small JS on my_cases.html that stashes window.scrollY in sessionStorage before every form submit and restores it on the next page load. Silent no-op when sessionStorage is blocked (strict privacy mode), so the feature is purely additive. No server changes — kept the redirect URLs clean. An earlier iteration with a #case- anchor on the redirect turned out to race with the explicit scrollTo on any page where the referer scrollY was near 0; dropping the anchor made the JS the sole source of truth. --- server/templates/my_cases.html | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/server/templates/my_cases.html b/server/templates/my_cases.html index e802224..2c999fe 100644 --- a/server/templates/my_cases.html +++ b/server/templates/my_cases.html @@ -160,6 +160,38 @@ try { }); })(); +// Preserve scroll position across POST-redirects (close, reopen, +// bulk-*). Browsers keep scroll on location.reload() but NOT on a +// redirect-driven navigation, even when the target URL matches the +// referer. Without this, clicking close on row 30 of a long list +// dumps the user back at scrollY=0 with the action-row off-screen. +// +// Mechanism: capture scrollY in sessionStorage before every form +// submit on this page, restore it on the very next load, then delete. +// Using the classic two-arg scrollTo(0, y) form — the options-object +// variant is silently ignored in some older browsers. +(() => { + const STASH = 'doctate:scrollY'; + try { + document.querySelectorAll('form').forEach((f) => { + f.addEventListener('submit', () => { + sessionStorage.setItem(STASH, String(window.scrollY)); + }); + }); + const stashed = sessionStorage.getItem(STASH); + if (stashed !== null) { + sessionStorage.removeItem(STASH); + const y = parseInt(stashed, 10); + if (!isNaN(y)) { + window.scrollTo(0, y); + } + } + } catch (_) { + // sessionStorage may be blocked (strict privacy mode) — silent fail, + // scroll returns to 0 like it did before this feature. + } +})(); + // Live-update bus: any case event for this user triggers a debounced // page reload. Debounce collapses event bursts (e.g. bulk-delete) into // one reload. EventSource auto-reconnects on network drops.