fix(ux): Scrollposition nicht durch Seitenwechsel-Kollaps auf 0 überschreiben
Echte Ursache (per Firefox/Chromium gegen die Live-App verifiziert): Beim Wegklicken eines Listeneintrags wird die Seite ausgehängt → der Inhalt von .app-main kollabiert → der Container springt auf scrollTop 0 → das löste ein scroll-Event aus, das 0 als neue Position speicherte und die gemerkte Stelle ZERSTÖRTE, kurz bevor wir wegnavigierten. Fix: - scroll-Events ignorieren, wenn der Container gerade NICHT scrollbar ist (scrollHeight ~ clientHeight = Inhalt kollabiert/Seitenwechsel) → kein „unechter" 0-Save. - Wiederherstellung hält den Schutz über die gesamte Settling-Phase: bei jeder DOM-Änderung Position erneut setzen, erst nach ~350 ms Ruhe freigeben (gegen Reflow-Races beim Nachladen). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -66,8 +66,12 @@ export function useScrollRestoration() {
|
||||
}
|
||||
if (!Number.isFinite(y) || y <= 0) return
|
||||
|
||||
// WICHTIG gegen Race: `restoringRef` bleibt während der GESAMTEN Nachlade-/Settling-Phase
|
||||
// true. So kann ein durch das Re-Rendern/Nachladen ausgelöster Sprung nach oben (scroll→0)
|
||||
// NICHT als neue Position gespeichert werden und die gemerkte Position überschreiben.
|
||||
restoringRef.current = true
|
||||
let done = false
|
||||
let quiet: number | undefined
|
||||
const stop = () => {
|
||||
if (done) return
|
||||
done = true
|
||||
@@ -77,18 +81,27 @@ export function useScrollRestoration() {
|
||||
window.removeEventListener('touchmove', onUser)
|
||||
window.removeEventListener('keydown', onUser)
|
||||
window.clearTimeout(safety)
|
||||
window.clearTimeout(quiet)
|
||||
}
|
||||
// Sobald die Nutzerin selbst scrollt, brechen wir ab — nie gegen sie ankämpfen.
|
||||
const onUser = () => stop()
|
||||
const tryScroll = () => {
|
||||
if (done) return
|
||||
const el = getScroller()
|
||||
setTop(el, y)
|
||||
if (Math.abs(getTop(el) - y) <= 2) stop() // erreicht → fertig
|
||||
const assert = () => {
|
||||
if (!done) setTop(getScroller(), y)
|
||||
}
|
||||
// EVENT-basiert: jede DOM-Änderung im Inhalt (Liste lädt nach / kommt zurück) → erneut zur
|
||||
// gemerkten Position springen. Zuverlässiger als ein fester Timer.
|
||||
const mo = new MutationObserver(() => requestAnimationFrame(tryScroll))
|
||||
// „Ruhe-Timer": erst freigeben, wenn der Inhalt ~350 ms lang stabil war (keine DOM-Änderung
|
||||
// mehr) — bis dahin wird die Position bei jeder Änderung erneut gesetzt.
|
||||
const armQuiet = () => {
|
||||
window.clearTimeout(quiet)
|
||||
quiet = window.setTimeout(() => {
|
||||
assert()
|
||||
stop()
|
||||
}, 350)
|
||||
}
|
||||
const onMutate = () => {
|
||||
assert()
|
||||
armQuiet()
|
||||
}
|
||||
const mo = new MutationObserver(onMutate)
|
||||
const passive = { passive: true } as AddEventListenerOptions
|
||||
window.addEventListener('wheel', onUser, passive)
|
||||
window.addEventListener('touchmove', onUser, passive)
|
||||
@@ -96,7 +109,8 @@ export function useScrollRestoration() {
|
||||
const safety = window.setTimeout(stop, RESTORE_OBSERVE_CAP_MS)
|
||||
const scroller = getScroller()
|
||||
if (scroller) mo.observe(scroller, { childList: true, subtree: true })
|
||||
requestAnimationFrame(tryScroll)
|
||||
assert()
|
||||
armQuiet()
|
||||
}, [])
|
||||
|
||||
// Auf Navigation reagieren: POP = wiederherstellen, sonst neue Seite oben starten.
|
||||
@@ -118,6 +132,11 @@ export function useScrollRestoration() {
|
||||
useEffect(() => {
|
||||
const onScroll = () => {
|
||||
if (restoringRef.current || document.visibilityState !== 'visible') return
|
||||
// Beim Seitenwechsel kollabiert der Inhalt kurz (scrollHeight ~ clientHeight) und der
|
||||
// Container springt auf 0. Diesen „unechten" 0-Sprung NICHT speichern, sonst überschreibt
|
||||
// er die gemerkte Position kurz bevor wir wegnavigieren.
|
||||
const el = getScroller()
|
||||
if (el && el.scrollHeight <= el.clientHeight + 4) return
|
||||
if (rafSaveRef.current != null) return
|
||||
rafSaveRef.current = requestAnimationFrame(() => {
|
||||
rafSaveRef.current = null
|
||||
|
||||
Reference in New Issue
Block a user