fix(settings): preserve content scroll position per page across navigation (#2277)

Settings nav items are plain Turbo Drive links (full-body visits); Turbo only
restores window scroll, so the nested overflow-y-auto content container snapped
to the top on every settings navigation.

Add a settings-scroll Stimulus controller that saves/restores the content scroll
keyed by pathname: returning to a page restores its scroll, a new page opens at
the top, and a same-page re-render (settings form auto-submit) keeps scroll.
Distinct from the nav's preserve-scroll controller, which keys by element id to
intentionally carry one position across pages.

Verified live: scroll 250 on Preferences -> Appearance opens at top -> back to
Preferences restores 250.
This commit is contained in:
Guillem Arias Fauste
2026-06-11 15:52:07 +02:00
committed by GitHub
parent 74d0452a2b
commit 5f391f7fff
2 changed files with 40 additions and 1 deletions

View File

@@ -0,0 +1,39 @@
import { Controller } from "@hotwired/stimulus";
// Preserves the settings content scroll position PER PAGE across Turbo Drive
// navigation. Settings nav items are plain links (full-body Turbo visits), and
// Turbo only restores window scroll — so this nested overflow-y-auto container
// snaps to top on every visit.
//
// Keyed by pathname: returning to a page restores its scroll, a brand-new page
// starts at the top, and a same-page re-render (e.g. a settings form that
// auto-submits) keeps scroll. This differs from the nav's `preserve-scroll`
// controller, which intentionally carries one position across all pages because
// the nav is the same persistent element on every settings page.
export default class extends Controller {
static positions = {};
connect() {
this.save = this.save.bind(this);
document.addEventListener("turbo:before-cache", this.save);
this.restore();
}
disconnect() {
document.removeEventListener("turbo:before-cache", this.save);
}
save() {
this.constructor.positions[window.location.pathname] = {
top: this.element.scrollTop,
left: this.element.scrollLeft,
};
}
restore() {
const pos = this.constructor.positions[window.location.pathname];
if (!pos) return;
this.element.scrollTop = pos.top;
this.element.scrollLeft = pos.left;
}
}