From 5f391f7fff67e2681e3730d97d2f7c6d1d780b05 Mon Sep 17 00:00:00 2001 From: Guillem Arias Fauste Date: Thu, 11 Jun 2026 15:52:07 +0200 Subject: [PATCH] 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. --- .../controllers/settings_scroll_controller.js | 39 +++++++++++++++++++ app/views/layouts/settings.html.erb | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 app/javascript/controllers/settings_scroll_controller.js diff --git a/app/javascript/controllers/settings_scroll_controller.js b/app/javascript/controllers/settings_scroll_controller.js new file mode 100644 index 000000000..585be9bcc --- /dev/null +++ b/app/javascript/controllers/settings_scroll_controller.js @@ -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; + } +} diff --git a/app/views/layouts/settings.html.erb b/app/views/layouts/settings.html.erb index 887789332..8b112e3dd 100644 --- a/app/views/layouts/settings.html.erb +++ b/app/views/layouts/settings.html.erb @@ -11,7 +11,7 @@
-
+
<% if content_for?(:breadcrumbs) %> <%= yield :breadcrumbs %>