fix(tickets): Tab + Scrollposition überleben Browser-Zurück

Zwei Ursachen, warum es auf der Tickets-Seite nicht ging (am echten Firefox/Chromium
gegen die Live-App verifiziert):

1) Tab ging verloren: der gewählte Filter-Tab (z. B. „Geschlossen") war reiner
   Component-State und sprang nach „Zurück" (Remount) auf „Offen" zurück — also war man
   in der falschen, kurzen Liste. Jetzt wird der Tab in der Session gemerkt und
   wiederhergestellt (userPicked startet weiterhin false, damit ?focus=-Deep-Links
   weiter funktionieren).

2) Scroll-Save-Heuristik zu unzuverlässig: Beim Wegnavigieren schrumpft der Inhalt von
   .app-main → der Container klemmt auf eine kleinere Position → dieses scroll-Event
   überschrieb die gemerkte Stelle. Erkennung präzisiert: nur verwerfen, wenn Position
   UND scrollHeight gleichzeitig SINKEN (= Inhalt geschrumpft/Seitenwechsel). Eine echte
   Nutzer-Scrollung verkleinert die scrollHeight nie — damit kein fälschliches Verwerfen
   echter Positionen mehr (vorherige „Höhe geändert"-Heuristik schluckte zu viel).

Tests: scroll-restore.spec erweitert (realistisches 2-Schritt-Scrollen + Tickets-Tab
überlebt Zurück), desktop+phone grün; vitest 149 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 14:54:55 +02:00
parent 54cc493230
commit 8aaddea4ef
3 changed files with 99 additions and 40 deletions

View File

@@ -260,7 +260,18 @@ export default function TicketsPage() {
// Default-Ansicht: „Offen" (neue, noch unbearbeitete Tickets) — fällt auf die erste
// nicht-leere Gruppe zurück, falls „Offen" leer ist. Solange die Nutzerin keinen Filter
// selbst gewählt hat (userPicked), darf ein ?focus=-Sprung die Ansicht bestimmen.
const [view, setView] = useState<TicketView>('open')
// Der zuletzt gewählte Tab wird in der Session gemerkt, damit man nach „Zurück" (Remount)
// NICHT wieder im falschen Tab („Offen") landet. userPicked startet bewusst false, damit ein
// frischer ?focus=-Deep-Link weiterhin in den passenden Tab springen darf.
const [view, setView] = useState<TicketView>(() => {
try {
const v = sessionStorage.getItem('tickets-view')
if (v === 'dialog' || v === 'open' || v === 'closed' || v === 'deleted') return v
} catch {
/* ignorieren */
}
return 'open'
})
const [userPicked, setUserPicked] = useState(false)
async function handleToggleStatus(ticket: FeedbackTicket) {
@@ -551,6 +562,11 @@ export default function TicketsPage() {
onClick={() => {
setUserPicked(true)
setView(v)
try {
sessionStorage.setItem('tickets-view', v)
} catch {
/* ignorieren */
}
}}
>
{t.filters[v]}