From 807899b137e8ebdd34e2ed71fd61c6b3b1bb955f Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Wed, 21 Oct 2026 20:21:07 -0400 Subject: [PATCH] fix(reader): arrows stay page turns while the progress slider holds focus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking the bottom progress slider left it focused, and the reader's keydown guard treats INPUT focus as typing โ€” so arrow keys stepped the range input instead of turning pages. With step="any" each press moved ~1% of the book and key repeat crossed the whole book in seconds; the debounced progress saves then persisted every wild landing (positions scattered from page 5 to 285 to 100%, "progress no longer saves correctly after page turns"). Two layers: - reader.templ: all three progress sliders blur on pointerup, so a mouse scrub no longer leaves focus behind. - reader.ts handleKeydown: ArrowLeft/ArrowRight on a focused progress slider preventDefault, blur, and fall through to the page-turn handlers; every other key still belongs to the control, so a keyboard user who Tabbed to the slider keeps arrows for it. --- templates/reader.templ | 3 +++ templates/reader_templ.go | 4 ++-- web/src/reader/reader.ts | 14 +++++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/templates/reader.templ b/templates/reader.templ index 130fd3a8..bdc0601f 100644 --- a/templates/reader.templ +++ b/templates/reader.templ @@ -350,6 +350,7 @@ templ ReaderChrome(metadata ReaderMetadata) { step="any" list="tick-marks" @input="goToFraction($event.target.value)" + @pointerup="$el.blur()" class="grow" /> @@ -392,6 +393,7 @@ templ ReaderChrome(metadata ReaderMetadata) { max="1" step="any" @input="goToFraction($event.target.value)" + @pointerup="$el.blur()" class="grow min-w-[3rem]" />
โ€”
โ€”
0/0
Zoom
Fit
Page position
Magnifier
Night mode
Pointer
Double page
Contents
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
โ€”
โ€”
0/0
Zoom
Fit
Page position
Magnifier
Night mode
Pointer
Double page
Contents
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -230,7 +230,7 @@ func drawerHeader(title string) templ.Component { var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 580, Col: 35} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 583, Col: 35} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts index 020520f3..bd16a208 100644 --- a/web/src/reader/reader.ts +++ b/web/src/reader/reader.ts @@ -3993,6 +3993,8 @@ document.addEventListener("alpine:init", () => { // Escape stays live so popovers/drawers can still be dismissed from // the keyboard even mid-note. const t = event.target as HTMLElement | null; + const isProgressSlider = + t instanceof HTMLInputElement && t.type === "range" && t.id.startsWith("progress-slider"); const typing = t?.tagName === "INPUT" || t?.tagName === "SELECT" || @@ -4000,7 +4002,17 @@ document.addEventListener("alpine:init", () => { !!t?.isContentEditable; // NOTE: no chrome poke here โ€” keyboard page turns are reading // actions and must leave the chrome alone (handoff item 7). - if (typing && k !== "Escape") return; + if (typing && k !== "Escape") { + // A focused progress range swallows arrows as ~1% book-jumps โ€” + // with key repeat, holding an arrow crosses the entire book in + // seconds and the debounced saves persist every wild landing + // (2026-10-21 incident). Arrows are page turns everywhere in the + // reader: release the slider and fall through to the turn + // handlers. Every other key still belongs to the control. + if (!(isProgressSlider && (k === "ArrowLeft" || k === "ArrowRight"))) return; + event.preventDefault(); + t.blur(); + } if (k === "ArrowLeft" || k === "h") { if (event.altKey) { event.preventDefault();