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 <[email protected]>
This commit is contained in:
Manjunath PaparajuandSameh Khamis authored and Sameh Khamis committed 2026-11-14 13:41:07 -08:00
1 parent 422a11d9a4
commit 81f3602134
1 file changed
+24 -6
+24 -6
View File
@@ -18,8 +18,8 @@ const GESTURE_GAP_MS = 180;
const STEP_COOLDOWN_MS = 220; const STEP_COOLDOWN_MS = 220;
/** /**
* The least time (ms) between two steps, whatever the wheel does. A motion * 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 * that would step sooner waits and steps once the gap has passed (one step
* back to back. * held at most), so panels never switch back to back and no step is lost.
*/ */
const STEP_GAP_MS = 500; const STEP_GAP_MS = 500;
/** A wheel event this large (pixels) is a mouse wheel notch, not a trackpad. */ /** 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; let stepped = false;
/** When the last step was taken, the fastest the gesture has been, and whether it has slowed since. */ /** When the last step was taken, the fastest the gesture has been, and whether it has slowed since. */
let steppedAt = 0; let steppedAt = 0;
/** A step waiting for the gap to pass, if any. */
let heldStep = 0;
let peakSpeed = 0; let peakSpeed = 0;
let slowed = false; let slowed = false;
let frame = 0; let frame = 0;
@@ -59,7 +61,9 @@ export function installRailEdgeScroll(doc = globalThis.document) {
.filter(({ rail }) => rail && rail.getBoundingClientRect().width > 0); .filter(({ rail }) => rail && rail.getBoundingClientRect().width > 0);
const items = (rail) => 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 ── // ── The sliding highlight ──
// Each rail draws it as a ::before under its items, placed by custom // 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; travel += event.deltaY;
if (Math.abs(travel) < STEP_DELTA) return; if (Math.abs(travel) < STEP_DELTA) return;
stepped = true; stepped = true;
if (now - steppedAt < STEP_GAP_MS) return; const direction = Math.sign(travel);
steppedAt = now; const wait = STEP_GAP_MS - (now - steppedAt);
step(Math.sign(travel)); 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); const onResize = () => follow(2);
@@ -246,6 +263,7 @@ export function installRailEdgeScroll(doc = globalThis.document) {
follow(); follow();
return () => { return () => {
win.clearTimeout(heldStep);
setHot(null); setHot(null);
observer?.disconnect(); observer?.disconnect();
win.cancelAnimationFrame(frame); win.cancelAnimationFrame(frame);