From 12d570087692c90fdfd7a43b54413c6cfc67db5d Mon Sep 17 00:00:00 2001 From: Joe Celaster Date: Thu, 25 Sep 2025 23:05:33 +0530 Subject: [PATCH] done --- .../stylesheets/application/sidebar.css | 3 ++ .../controllers/maintain_scroll_controller.js | 41 ++++++++++++++++++ app/javascript/models/scroll_manager.js | 43 ++++++++++++++++++- 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/application/sidebar.css b/app/assets/stylesheets/application/sidebar.css index 4eb9312b..f8fa7087 100644 --- a/app/assets/stylesheets/application/sidebar.css +++ b/app/assets/stylesheets/application/sidebar.css @@ -10,6 +10,9 @@ max-block-size: 100dvh; padding-inline-end: var(--sidebar-tools-width); position: relative; + /* Safari-specific: Prevent scrollbar jumping during DOM updates */ + scroll-behavior: auto; + overscroll-behavior: contain; } .sidebar__tools { diff --git a/app/javascript/controllers/maintain_scroll_controller.js b/app/javascript/controllers/maintain_scroll_controller.js index 196118e7..d30170ff 100644 --- a/app/javascript/controllers/maintain_scroll_controller.js +++ b/app/javascript/controllers/maintain_scroll_controller.js @@ -3,9 +3,15 @@ import ScrollManager from "models/scroll_manager" export default class extends Controller { #scrollManager + #isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) connect() { this.#scrollManager = new ScrollManager(this.element) + + // Safari-specific: Add additional scroll jump prevention + if (this.#isSafari) { + this.#preventSafariScrollJump() + } } // Actions @@ -37,4 +43,39 @@ export default class extends Controller { #isAboveFold(element) { return element.getBoundingClientRect().top < this.element.clientHeight } + + #preventSafariScrollJump() { + // Safari-specific: Prevent scroll jumping during Turbo Stream updates + this.element.addEventListener('turbo:before-stream-render', (event) => { + if (this.element.scrollTop > 0) { + // Store current scroll position + const currentScrollTop = this.element.scrollTop + + // Temporarily lock scroll position + this.element.style.scrollBehavior = 'auto' + + // Restore scroll position after DOM update + requestAnimationFrame(() => { + if (Math.abs(this.element.scrollTop - currentScrollTop) > 5) { + this.element.scrollTop = currentScrollTop + } + // Reset scroll behavior + this.element.style.scrollBehavior = '' + }) + } + }) + + // Additional Safari fix for room navigation + this.element.addEventListener('turbo:render', () => { + if (this.element.scrollTop > 0) { + // Force scroll position stability after render + const scrollTop = this.element.scrollTop + setTimeout(() => { + if (this.element.scrollTop !== scrollTop) { + this.element.scrollTo({ top: scrollTop, behavior: 'auto' }) + } + }, 0) + } + }) + } } diff --git a/app/javascript/models/scroll_manager.js b/app/javascript/models/scroll_manager.js index 6bf2ec61..b343bf22 100644 --- a/app/javascript/models/scroll_manager.js +++ b/app/javascript/models/scroll_manager.js @@ -38,6 +38,26 @@ export default class ScrollManager { const scrollTop = this.#container.scrollTop const scrollHeight = this.#cachedScrollHeight // Use cached value + // Safari-specific: Temporarily disable scroll restoration + const originalScrollRestoration = history.scrollRestoration + if (history.scrollRestoration) { + history.scrollRestoration = 'manual' + } + + // Safari-specific: Force immediate scroll position lock + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) + if (isSafari && scrollTop > 0) { + // Prevent Safari from jumping by setting a temporary fixed position + const originalOverflow = this.#container.style.overflow + this.#container.style.overflow = 'hidden' + this.#container.style.scrollBehavior = 'auto' + + // Restore after a minimal delay to prevent the jump + setTimeout(() => { + this.#container.style.overflow = originalOverflow + }, 0) + } + await render() // Update cache after render @@ -45,10 +65,29 @@ export default class ScrollManager { const newScrollTop = top ? scrollTop + (this.#container.scrollHeight - scrollHeight) : scrollTop + // Safari-specific: Use multiple restoration attempts + const restoreScroll = () => { + this.#container.scrollTo({ top: newScrollTop, behavior: scrollBehaviour }) + + // Safari sometimes needs a second attempt + if (isSafari && Math.abs(this.#container.scrollTop - newScrollTop) > 1) { + requestAnimationFrame(() => { + this.#container.scrollTo({ top: newScrollTop, behavior: 'auto' }) + }) + } + } + if (delay) { - requestAnimationFrame(() => this.#container.scrollTo({ top: newScrollTop, behavior: scrollBehaviour })) + requestAnimationFrame(restoreScroll) } else { - this.#container.scrollTo({ top: newScrollTop, behavior: scrollBehaviour }) + restoreScroll() + } + + // Restore scroll restoration setting + if (originalScrollRestoration) { + setTimeout(() => { + history.scrollRestoration = originalScrollRestoration + }, 100) } }) }