diff --git a/internal/gui/assets/routing.css b/internal/gui/assets/routing.css index 9b557f75..448053b6 100644 --- a/internal/gui/assets/routing.css +++ b/internal/gui/assets/routing.css @@ -201,7 +201,7 @@ .rt-livedot { width: 6px; height: 6px; margin-left: 7px; border-radius: 50%; background: var(--green); animation: rtlive 1.8s infinite; --fg: var(--green); } @media (prefers-reduced-motion: reduce) { .rt-livedot { animation: none; } } /* routing groups, and the providers routing over several of their own */ -.rt-gsec { display: grid; gap: 6px; margin-bottom: 16px; } +.rt-gsec { display: grid; gap: 6px; margin-top: 16px; } .rt-gsec .row-head .note { margin-right: 6px; } .rt-gsec .rt-pools { margin-bottom: 0; } .rt-gsec .row-head + .list + .row-head { margin-top: 8px; } diff --git a/internal/gui/assets/routing.js b/internal/gui/assets/routing.js index f51c7b88..372f9388 100644 --- a/internal/gui/assets/routing.js +++ b/internal/gui/assets/routing.js @@ -1023,6 +1023,7 @@ reqNote.textContent = day ? t(pastCut ? "the last {n} of {day}" : "{n} on {day}", { n: rs.length, day: dayName(day) }) : t("the last {n} the gateway keeps", { n: rs.length }); renderDays(); + if (!reqs.style.maxHeight) requestAnimationFrame(fitReqs); // first shown // none yet: what the list is for in its place, and no accounts column // to tally nothing const none = !rs.length; @@ -1039,6 +1040,9 @@ renderActs(rs); return; } + // the list scrolls on its own; WebKit, emptied for a moment, would + // send it back to its top from under the row just picked + const listTop = reqs.scrollTop; reqs.replaceChildren(...rs.map((r) => { const [said, how, tr] = outcome(r); const b = el("button", "rt-req " + how); @@ -1071,6 +1075,7 @@ b.onclick = () => pick(r); return b; })); + if (reqs.scrollTop !== listTop) reqs.scrollTop = listTop; renderActs(rs); } @@ -1631,7 +1636,9 @@ const gHead = el("div", "row-head"), gList = el("div", "list rt-groups"); const pHead = el("div", "row-head"), pList = el("div", "list rt-pools"); gsec.append(gHead, gList, pHead, pList); - more.prepend(gsec); + // after the requests: a request picked in the list plays on the stage, + // so the list sits right under it + more.append(gsec); const ROUTE_OPTS = [["", "Smart"], ["order", "In order"], ["rotate", "In turn"], ["usage", "Least used"]]; const AFF_OPTS = [["", "Auto"], ["session", "Session"], ["turn", "Within a turn"], ["off", "Off"]]; const AFF_HINT = { @@ -2066,6 +2073,18 @@ loadGroups(); new ResizeObserver(() => layout()).observe(stage); + // the list is as tall as leaves the stage in sight above it: the reader + // scrolls the list, not the page, and a request picked plays in view. + // Its top stays where it is as it is sized, so a row just clicked does too + function fitReqs() { + const v = $("#view-routing"); + if (v.hidden || !reqs.offsetParent) return; + const above = reqs.getBoundingClientRect().top - box.getBoundingClientRect().top; + const room = v.clientHeight - above - 28; + reqs.style.maxHeight = Math.round(Math.max(216, Math.min(420, room))) + "px"; + } + new ResizeObserver(fitReqs).observe($("#view-routing")); + new ResizeObserver(fitReqs).observe(box); words(); requestAnimationFrame(frame); poll(); diff --git a/internal/gui/tests/click-scroll.test.cjs b/internal/gui/tests/click-scroll.test.cjs index 6b9e2235..3c19c103 100644 --- a/internal/gui/tests/click-scroll.test.cjs +++ b/internal/gui/tests/click-scroll.test.cjs @@ -107,6 +107,17 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", for (const i of [3, 7, 5]) { const row = () => page.locator(".rt-req").nth(i); await scrollTo(`.rt-req >> nth=${i}`); + // the list scrolls on its own: the reader brings the row into it + // (the click would, before it is measured) + const list = await page.locator(".rt-reqs").boundingBox(); + await page.mouse.move(list.x + list.width / 2, list.y + 20); + for (let k = 0; k < 40; k++) { + const b = await row().boundingBox(); + if (b.y + b.height <= list.y + list.height && b.y >= list.y) break; + await page.mouse.wheel(0, b.y < list.y ? -20 : 20); + await page.waitForTimeout(20); + } + await page.waitForTimeout(300); const was = await row().evaluate((e) => e.getBoundingClientRect().top); await row().click(); await settle(page);