Fiks scroll-til-spillerliste for godt: mål header-høyden live, ikke hardkodet
Presis scratch-måling avdekket at den hardkodede 112px-antagelsen (arvet fra scroll-mt-28) var feil -- ekte headerhøyde var 141.5px for en runde med bane+utslag+dato+klokkeslett samtidig, siden headeren pakkes til flere linjer avhengig av innhold. Ny getHeaderHeight() måler live i stedet for å gjette. scrollToPlayerList() erstatter scrollIntoView med manuell beregning; playerListReached bruker samme terskel (delt SCROLL_GAP_PX-konstant) -- måtte rettes en gang til underveis da de to brukte ulike terskler. Se CHANGELOG.md #142. Ikke rullet ut ennå. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
76b306cb29
commit
116600cd3c
2 changed files with 89 additions and 12 deletions
39
CHANGELOG.md
39
CHANGELOG.md
|
|
@ -13584,3 +13584,42 @@ Neste steg:
|
||||||
spillerlisten -- knappen dukket korrekt opp igjen. `tsc --noEmit` rent.
|
spillerlisten -- knappen dukket korrekt opp igjen. `tsc --noEmit` rent.
|
||||||
|
|
||||||
**Ikke rullet ut ennå** -- venter på bekreftelse fra bruker.
|
**Ikke rullet ut ennå** -- venter på bekreftelse fra bruker.
|
||||||
|
|
||||||
|
142. **Nei, ikke helt -- ekte målebasert oppfølging av #141,
|
||||||
|
2026-08-20.** Bruker spurte direkte: scroller det nå faktisk langt
|
||||||
|
nok ned til å vise HELE spillerkort 1? Presis scratch-måling (ikke
|
||||||
|
skjermbilde-eyeballing) avdekket at svaret fortsatt var nei: det
|
||||||
|
hardkodede `112px` (lånt fra `scroll-mt-28`, brukt både i CSS-en for
|
||||||
|
scroll-hoppet og i #141 sin `playerListReached`-terskel) stemte ikke
|
||||||
|
-- headerens EKTE høyde målte 141,5px for denne runden (bane + utslag
|
||||||
|
+ dato + klokkeslett samtidig), ~30px mer enn antatt. Toppen av
|
||||||
|
spillerkort 1 lå dermed fortsatt delvis bak headeren.
|
||||||
|
|
||||||
|
Rot-årsak: `RoundHeader` sin høyde er reelt DYNAMISK (rundenavn/
|
||||||
|
utslag/dato/klokkeslett kan pakkes til flere linjer avhengig av
|
||||||
|
runden) -- en NY hardkodet konstant (f.eks. 160px) ville bare flyttet
|
||||||
|
samme sårbarhet til en annen runde med enda lenger innhold, ikke løst
|
||||||
|
den. Derfor målt LIVE i stedet:
|
||||||
|
|
||||||
|
- Ny `getHeaderHeight()`-hjelper: `document.querySelector("header")
|
||||||
|
?.getBoundingClientRect().height` (112 kun som aller siste
|
||||||
|
nødfallback).
|
||||||
|
- `scrollToPlayerList()` erstatter `scrollIntoView` + statisk
|
||||||
|
`scroll-mt-28` -- regner selv ut nøyaktig mål-scrollposisjon med
|
||||||
|
`getHeaderHeight()` + en delt `SCROLL_GAP_PX = 16`-konstant.
|
||||||
|
- `playerListReached`-sjekken (#141) oppdatert til å bruke SAMME
|
||||||
|
`getHeaderHeight() + SCROLL_GAP_PX`-terskel -- måtte rettes en gang
|
||||||
|
til underveis i denne rundens scratch-verifisering: brukte først
|
||||||
|
kun `getHeaderHeight()` uten gap-konstanten, som fikk knappen til å
|
||||||
|
henge synlig selv etter et korrekt landet hopp (16px avvik mellom
|
||||||
|
hvor scrollet faktisk landet og hvor "reached" ble regnet ut).
|
||||||
|
|
||||||
|
**Verifisert i scratch, presist (ikke bare skjermbilde):** målte
|
||||||
|
`getBoundingClientRect()` for spillerkort 1 og headeren direkte via
|
||||||
|
`evaluate_script` rett etter scroll-hoppet. Bekreftet numerisk:
|
||||||
|
`cardTop: 157.5`, `headerBottom: 141.5`, `gapBelowHeader: 16`,
|
||||||
|
`fullyVisible: true` (både over header OG innenfor viewport-bunnen).
|
||||||
|
Bekreftet at den flytende knappen samtidig er `aria-hidden="true"`
|
||||||
|
(korrekt skjult) i samme øyeblikk. `tsc --noEmit` rent.
|
||||||
|
|
||||||
|
**Ikke rullet ut ennå** -- venter på bekreftelse fra bruker.
|
||||||
|
|
|
||||||
|
|
@ -6634,6 +6634,26 @@ function sumForPlayer(list: ApiHole[] | undefined, numbers: number[]): { strokes
|
||||||
// Trykk åpner SAMME eksisterende fullskjerm-veiviser som før (onOpenEntry)
|
// Trykk åpner SAMME eksisterende fullskjerm-veiviser som før (onOpenEntry)
|
||||||
// -- selve registreringsvinduet er bevisst IKKE rørt av denne rundens
|
// -- selve registreringsvinduet er bevisst IKKE rørt av denne rundens
|
||||||
// V0-prompt.
|
// V0-prompt.
|
||||||
|
// Målt LIVE, ikke en hardkodet konstant -- den sticky RoundHeader-en sin
|
||||||
|
// høyde er reelt dynamisk (rundenavn/dato/klokkeslett kan pakkes til flere
|
||||||
|
// linjer avhengig av runden), så en fast pixel-gjetning viste seg feil for
|
||||||
|
// en runde med lengre innhold enn den opprinnelige gjetningen (112px) var
|
||||||
|
// kalibrert mot (scratch-verifisering 2026-08-20: ekte høyde 141,5px for
|
||||||
|
// en runde med bane+utslag+dato+klokkeslett samtidig). Brukt av BÅDE
|
||||||
|
// scroll-hoppet til spillerlisten og `playerListReached`-sjekken under.
|
||||||
|
function getHeaderHeight(): number {
|
||||||
|
return document.querySelector("header")?.getBoundingClientRect().height ?? 112
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pusterom under headeren når spillerlisten scrolles til -- brukt for
|
||||||
|
// BÅDE selve scroll-målposisjonen og "reached"-terskelen (se begge
|
||||||
|
// under). Delt konstant med vilje: scroll-hoppet lander bevisst
|
||||||
|
// `SCROLL_GAP_PX` under headeren (ikke flush), så "reached"-sjekken må
|
||||||
|
// bruke SAMME avstand, ellers forblir den flytende knappen synlig etter
|
||||||
|
// at hoppet har landet (funnet i scratch-verifisering 2026-08-20 -- de to
|
||||||
|
// brukte ulike terskler).
|
||||||
|
const SCROLL_GAP_PX = 16
|
||||||
|
|
||||||
// "Registrer score" -- samme knapp, to plasseringer (se PlayerHoleCards):
|
// "Registrer score" -- samme knapp, to plasseringer (se PlayerHoleCards):
|
||||||
// i normal side-flyt når banekartet er kollapset, flytende festet nederst
|
// i normal side-flyt når banekartet er kollapset, flytende festet nederst
|
||||||
// på skjermen når det er utvidet (V0-eksport, 2026-08-20, brukerønske --
|
// på skjermen når det er utvidet (V0-eksport, 2026-08-20, brukerønske --
|
||||||
|
|
@ -6725,28 +6745,35 @@ function PlayerHoleCards({
|
||||||
// brukeren allerede hadde scrollet forbi/til spillerlisten, der den ikke
|
// brukeren allerede hadde scrollet forbi/til spillerlisten, der den ikke
|
||||||
// lenger gir mening ("Registrer score ↓" når du allerede ser
|
// lenger gir mening ("Registrer score ↓" når du allerede ser
|
||||||
// spillerne). "Reached" = listens toppkant har nådd/passert headerens
|
// spillerne). "Reached" = listens toppkant har nådd/passert headerens
|
||||||
// faktiske høyde (samme 112px som scroll-mt-28 andre steder i denne
|
// faktiske høyde -- forblir sant BÅDE mens listen vises OG etter at
|
||||||
// filen) -- forblir sant BÅDE mens listen vises OG etter at brukeren
|
// brukeren har scrollet helt forbi den (listen ligger da over synlig
|
||||||
// har scrollet helt forbi den (listen ligger da over synlig område,
|
// område, `top` er negativ). Kun scroll TILBAKE OPP forbi listen gjør
|
||||||
// `top` er negativ). Kun scroll TILBAKE OPP forbi listen (top blir
|
// at knappen skal dukke opp igjen.
|
||||||
// igjen større enn 112) gjør at knappen skal dukke opp igjen.
|
|
||||||
//
|
//
|
||||||
// Første forsøk brukte IntersectionObserver med en krympet rootMargin
|
// Første forsøk brukte IntersectionObserver med en krympet rootMargin
|
||||||
// -- viste seg FEIL i scratch-verifisering: med standard threshold=[0]
|
// -- viste seg FEIL i scratch-verifisering: med standard threshold=[0]
|
||||||
// fyrer den kun ved 0%->>0%-kryssinger (element helt inn/ut av roten),
|
// fyrer den kun ved 0%->>0%-kryssinger (element helt inn/ut av roten),
|
||||||
// ikke ved at `boundingClientRect.top` passerer en INTERN linje midt i
|
// ikke ved at `boundingClientRect.top` passerer en INTERN linje midt i
|
||||||
// et allerede-overlappende element. Listen ble dermed hengende på
|
// et allerede-overlappende element. Listen ble dermed hengende på
|
||||||
// "ikke nådd" gjennom hele resten av scrollingen forbi 112px-grensen.
|
// "ikke nådd" gjennom hele resten av scrollingen forbi grensen. Vanlig
|
||||||
// Vanlig scroll-lytter med direkte `getBoundingClientRect()`-sjekk gir
|
// scroll-lytter med direkte `getBoundingClientRect()`-sjekk gir korrekt,
|
||||||
// korrekt, kontinuerlig oppdatering.
|
// kontinuerlig oppdatering.
|
||||||
|
//
|
||||||
|
// ANNET funn i scratch, samme runde: en HARDKODET grense (112px, lånt
|
||||||
|
// fra `scroll-mt-28` andre steder i filen) var selv FEIL -- headeren
|
||||||
|
// sin faktiske høyde målte 141.5px for denne runden, ikke 112. Headeren
|
||||||
|
// sitt innhold (rundenavn/dato/klokkeslett) kan pakkes til flere linjer
|
||||||
|
// avhengig av runden, så høyden er reelt DYNAMISK, ikke en konstant --
|
||||||
|
// en ny hardkodet gjetning (f.eks. 160px) ville bare flyttet samme
|
||||||
|
// sårbarhet til en annen runde med enda lenger innhold. Målt LIVE i
|
||||||
|
// stedet, se `getHeaderHeight()` under.
|
||||||
const [playerListReached, setPlayerListReached] = useState(false)
|
const [playerListReached, setPlayerListReached] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapExpanded) return
|
if (!mapExpanded) return
|
||||||
const HEADER_HEIGHT_PX = 112
|
|
||||||
function check() {
|
function check() {
|
||||||
const el = playerListRef.current
|
const el = playerListRef.current
|
||||||
if (!el) return
|
if (!el) return
|
||||||
setPlayerListReached(el.getBoundingClientRect().top <= HEADER_HEIGHT_PX)
|
setPlayerListReached(el.getBoundingClientRect().top <= getHeaderHeight() + SCROLL_GAP_PX)
|
||||||
}
|
}
|
||||||
check()
|
check()
|
||||||
window.addEventListener("scroll", check, { passive: true })
|
window.addEventListener("scroll", check, { passive: true })
|
||||||
|
|
@ -6756,6 +6783,17 @@ function PlayerHoleCards({
|
||||||
window.removeEventListener("resize", check)
|
window.removeEventListener("resize", check)
|
||||||
}
|
}
|
||||||
}, [mapExpanded])
|
}, [mapExpanded])
|
||||||
|
// Manuell scroll (ikke `scrollIntoView` + CSS `scroll-margin-top`) --
|
||||||
|
// samme begrunnelse som `getHeaderHeight()` over: en CSS-klasse som
|
||||||
|
// `scroll-mt-28` kan ikke være dynamisk, og headerens høyde varierer
|
||||||
|
// reelt med runde-innholdet. Regner ut nøyaktig mål-posisjon selv, med
|
||||||
|
// et lite pusterom (16px) under headeren.
|
||||||
|
function scrollToPlayerList() {
|
||||||
|
const el = playerListRef.current
|
||||||
|
if (!el) return
|
||||||
|
const top = el.getBoundingClientRect().top + window.scrollY - getHeaderHeight() - SCROLL_GAP_PX
|
||||||
|
window.scrollTo({ top, behavior: "smooth" })
|
||||||
|
}
|
||||||
const holeMeta: Record<number, { par: number; index: number }> = {}
|
const holeMeta: Record<number, { par: number; index: number }> = {}
|
||||||
for (const list of Object.values(holesByParticipant)) {
|
for (const list of Object.values(holesByParticipant)) {
|
||||||
for (const h of list) {
|
for (const h of list) {
|
||||||
|
|
@ -6896,7 +6934,7 @@ function PlayerHoleCards({
|
||||||
kunne dytte knappen helt utenfor synlig skjermbilde). */}
|
kunne dytte knappen helt utenfor synlig skjermbilde). */}
|
||||||
{!mapExpanded && (
|
{!mapExpanded && (
|
||||||
<RegisterScoreButton
|
<RegisterScoreButton
|
||||||
onClick={() => playerListRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })}
|
onClick={scrollToPlayerList}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -7169,7 +7207,7 @@ function PlayerHoleCards({
|
||||||
>
|
>
|
||||||
<div className="rounded-2xl shadow-xl shadow-black/25">
|
<div className="rounded-2xl shadow-xl shadow-black/25">
|
||||||
<RegisterScoreButton
|
<RegisterScoreButton
|
||||||
onClick={() => playerListRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })}
|
onClick={scrollToPlayerList}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue