From 81f3602134744b89b63ca3594644bde92e1cab1d Mon Sep 17 00:00:00 2001 From: Manjunath Paparaju Date: Fri, 13 Nov 2026 13:03:12 +0530 Subject: [PATCH] Hold an edge-scroll step that comes too soon instead of dropping it A scroll that reached a step within 500 ms of the last one was used up without stepping, so mouse wheel notches 300 ms apart stepped only every other notch and a second swipe inside the gap did nothing. Such a step now waits and fires once the gap has passed (one held at most, skipped if the pointer has left the strip), and the held timer is cleared on teardown. A comment naming the items cockpit hides is corrected. Co-authored-by: Sameh Khamis --- src/ui/railEdgeScroll.js | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/ui/railEdgeScroll.js b/src/ui/railEdgeScroll.js index c66b461ab..7b965cda0 100644 --- a/src/ui/railEdgeScroll.js +++ b/src/ui/railEdgeScroll.js @@ -18,8 +18,8 @@ const GESTURE_GAP_MS = 180; const STEP_COOLDOWN_MS = 220; /** * The least time (ms) between two steps, whatever the wheel does. A motion - * that would step sooner is used up without stepping, so panels never switch - * back to back. + * that would step sooner waits and steps once the gap has passed (one step + * held at most), so panels never switch back to back and no step is lost. */ const STEP_GAP_MS = 500; /** A wheel event this large (pixels) is a mouse wheel notch, not a trackpad. */ @@ -49,6 +49,8 @@ export function installRailEdgeScroll(doc = globalThis.document) { let stepped = false; /** When the last step was taken, the fastest the gesture has been, and whether it has slowed since. */ let steppedAt = 0; + /** A step waiting for the gap to pass, if any. */ + let heldStep = 0; let peakSpeed = 0; let slowed = false; let frame = 0; @@ -59,7 +61,9 @@ export function installRailEdgeScroll(doc = globalThis.document) { .filter(({ rail }) => rail && rail.getBoundingClientRect().width > 0); const items = (rail) => - [...rail.querySelectorAll('.ui-rail__item')].filter((item) => !item.disabled); + // Only items that show: a theme or mode may hide some, and the wheel must + // not open those. + [...rail.querySelectorAll('.ui-rail__item')].filter((item) => !item.disabled && item.offsetWidth > 0); // ── The sliding highlight ── // Each rail draws it as a ::before under its items, placed by custom @@ -233,9 +237,22 @@ export function installRailEdgeScroll(doc = globalThis.document) { travel += event.deltaY; if (Math.abs(travel) < STEP_DELTA) return; stepped = true; - if (now - steppedAt < STEP_GAP_MS) return; - steppedAt = now; - step(Math.sign(travel)); + const direction = Math.sign(travel); + const wait = STEP_GAP_MS - (now - steppedAt); + if (wait <= 0) { + steppedAt = now; + step(direction); + return; + } + // Too soon after the last step: hold this one until the gap has passed. + if (heldStep) return; + heldStep = win.setTimeout(() => { + heldStep = 0; + // The pointer may have left the strip meanwhile; then nothing steps. + if (!hot) return; + steppedAt = win.performance?.now?.() ?? Date.now(); + step(direction); + }, wait); } const onResize = () => follow(2); @@ -246,6 +263,7 @@ export function installRailEdgeScroll(doc = globalThis.document) { follow(); return () => { + win.clearTimeout(heldStep); setHot(null); observer?.disconnect(); win.cancelAnimationFrame(frame);