Fiks ScrollFade: sprettende scroll-hint overlappet en ekte pil-knapp

Bruker (skjermbilde): "Den sprettende ned-pilen fungerer ikke. Den
ligger OPPÅ en annen nedpil." -- gjelder Retning-steget (Utslag/
Innspill) i score-registreringen, hvor "Kort"-knappen har sitt eget
ArrowDown-ikon.

To reelle feil i ScrollFade (round-detail.tsx):

1. ResizeObserver observerte scroll-BEHOLDEREN, ikke innholdet.
   Beholderens boks-størrelse er fast (flex-1), så den endrer seg
   aldri når man bytter steg i veiviseren -- observeren fyrte derfor
   ALDRI når et steg med reelt overflow-innhold ble vist. Hintet
   dukket rett og slett ikke opp der det trengtes (f.eks. nettopp
   Retning-steget). Rettet ved å observere innholds-diven i stedet.

2. Fade-/pil-overlayet er absolutt posisjonert over de siste 64px av
   scroll-området, uten noen garanti om at ekte innhold ikke havner
   der. På Retning-steget landet "Kort"-knappens egen pil nøyaktig i
   den sonen -- to piler oppå hverandre. Rettet med en usynlig
   64px-buffer etter innholdet (samme høyde som overlayet), slik at
   overlayet aldri kan dekke et ekte element. hasMore-utregningen
   trekker fra bufferhøyden så den ikke gir falske positiver på kort
   innhold som uansett får plass.

Scratch-verifisert (egen DB/rolle/MinIO/API/frontend-container, ekte
bruker med fullt kølle-utvalg for å reprodusere nøyaktig samme
rutenett som i brukerens skjermbilde): hintet vises korrekt når
Retning-steget faktisk overflower, forsvinner korrekt ved reell bunn,
"Kort"-knappen fullt synlig og utenfor overlay-sonen ved skrolling,
lys+mørk, ingen konsollfeil. tsc --noEmit rent, 45/45 vitest grønt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erol Haagenrud 2026-08-11 14:41:22 +02:00
parent b704a9c378
commit ee91d03514

View file

@ -6577,30 +6577,49 @@ function WizardSection({ title, children }: { title: string; children: React.Rea
// finnes mer innhold under fold-linjen -- og skjuler seg av seg selv idet man
// har scrollet helt ned. Fanger både resize og innholdsendringer (ulikt
// stat_level/par gir ulik høyde) via en ResizeObserver på selve scrollboksen.
// Høyden (px) på fade-/piloverlayet under -- MÅ matche spacer-diven sin
// h-16 og trekkes fra i hasMore-utregningen, se begrunnelse der.
const SCROLL_FADE_HEIGHT_PX = 64
function ScrollFade({ children, className }: { children: React.ReactNode; className?: string }) {
const ref = useRef<HTMLDivElement>(null)
const scrollRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
const [hasMore, setHasMore] = useState(false)
useEffect(() => {
const el = ref.current
if (!el) return
const scrollEl = scrollRef.current
const contentEl = contentRef.current
if (!scrollEl || !contentEl) return
const update = () => {
// 4px slingring: unngår at en sub-piksel-rest holder hintet i live
// når man i praksis ER nederst.
setHasMore(el.scrollTop + el.clientHeight < el.scrollHeight - 4)
// Trekker fra spacer-divens egen høyde (samme SCROLL_FADE_HEIGHT_PX)
// i tillegg til 4px slingring -- uten dette ville "hasMore" blitt
// sant utelukkende fordi spaceren selv gjør scrollHeight større, selv
// når ALT ekte innhold allerede er synlig (falskt positivt hint).
setHasMore(scrollEl.scrollTop + scrollEl.clientHeight < scrollEl.scrollHeight - SCROLL_FADE_HEIGHT_PX - 4)
}
update()
el.addEventListener("scroll", update, { passive: true })
scrollEl.addEventListener("scroll", update, { passive: true })
// Observerer INNHOLDS-diven, IKKE scroll-beholderen selv --
// beholderens egen boks-størrelse er fast (flex-1, bundet av
// veiviserens layout), den endrer seg ALDRI når man bytter steg. Uten
// dette ble "hasMore" aldri regnet på nytt ved stegbytte -- den
// sprettende pilen dukket rett og slett ikke opp på steg som faktisk
// trengte scrolling (f.eks. "Retning"-steget med kølle-rutenett).
const ro = new ResizeObserver(update)
ro.observe(el)
ro.observe(contentEl)
return () => {
el.removeEventListener("scroll", update)
scrollEl.removeEventListener("scroll", update)
ro.disconnect()
}
}, [])
return (
<div className="relative flex min-h-0 flex-1 flex-col">
<div ref={ref} className={className}>
{children}
<div ref={scrollRef} className={className}>
<div ref={contentRef}>{children}</div>
{/* Usynlig buffer, SAMME høyde som fade-/piloverlayet -- garanterer at
overlayet aldri dekker et ekte, interaktivt element (f.eks. en
retningsknapp med sin egen pil-ikon nederst i steget). Uten denne
kunne den sprettende pilen havne rett oppå en annen pil. */}
<div aria-hidden="true" className="h-16 shrink-0" />
</div>
{/* Rent dekorativt -- selve scrollingen er fullt mulig uten dette. */}
<div