diff --git a/internal/gui/assets/app.js b/internal/gui/assets/app.js index 5d1777e8..c761bad9 100644 --- a/internal/gui/assets/app.js +++ b/internal/gui/assets/app.js @@ -373,6 +373,7 @@ function renderAgents() { } fold.append(inner); const more = el("button", "agent-more"); + more.dataset.unrolls = ""; // it goes down with the rows it opens const label = el("span", "", ""); const chev = el("span", "chev"); chev.append(svg(CHEV, 10, 1.8)); @@ -6310,6 +6311,10 @@ function savePrefs(body) { // filter, a day, a toggle leaves the page where it was. When what it does // leaves the page shorter under it (a list emptied), the view keeps room // at its foot for it to stay, room that goes as the reader scrolls back. +// A control under what it unrolls (data-unrolls: "Show 7 more") is the +// exception: it goes down with what it opens, and what's held is the +// part it is in, so the rows open downwards rather than the page riding +// up past them. // // Code moves a view only in answer to a click that asks to go somewhere, and // shows that with the reader's event: scrollOnPurpose(e). Called without one @@ -6402,7 +6407,8 @@ addEventListener("click", (e) => { const v = e.target.closest?.(".view"); if (!v || v.hidden) { held = null; return; } const chain = []; - for (let n = e.target; n && n !== v; n = n.parentElement) { + const from = e.target.closest?.("[data-unrolls]")?.parentElement || e.target; + for (let n = from; n && n !== v; n = n.parentElement) { for (const m of [n, n.previousElementSibling, n.nextElementSibling]) if (m instanceof HTMLElement && m.offsetParent) chain.push([m, onScreen(m, v)]); } held = chain.length ? { v, chain, until: performance.now() + 4000 } : null; diff --git a/internal/gui/tests/README.md b/internal/gui/tests/README.md index d7f6ebf3..a3ed5b90 100644 --- a/internal/gui/tests/README.md +++ b/internal/gui/tests/README.md @@ -9,7 +9,8 @@ and dismissal. It does not start the backend or read local user configuration. (see "where the reader is" in `app.js`): on the Routing page, Live and a day picked in turn with the list scrolled to its end; a click whose handler sets scrollTop, grows the page above, redraws itself, or scrolls from a timer or a -shared helper; a click that asks to go somewhere with `scrollOnPurpose(e)`; +shared helper; a control under what it unrolls (`data-unrolls`) going down +with it; a click that asks to go somewhere with `scrollOnPurpose(e)`; the room kept at the foot going as the reader scrolls back; and the wheel. With Node.js and Playwright available: diff --git a/internal/gui/tests/click-scroll.test.cjs b/internal/gui/tests/click-scroll.test.cjs index 0bee4b72..3fe196a6 100644 --- a/internal/gui/tests/click-scroll.test.cjs +++ b/internal/gui/tests/click-scroll.test.cjs @@ -133,6 +133,30 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", } }); + await t.test("a control under what it unrolls goes down with it", async () => { + await reset(); + await page.locator(view).evaluate((v) => { + const s = document.createElement("div"); + s.style.cssText = "flex:none;height:1600px"; + const part = document.createElement("div"); + part.id = "part"; part.style.flex = "none"; + const head = document.createElement("div"); + head.id = "head"; head.textContent = "head"; + const fold = document.createElement("div"); + const b = document.createElement("button"); + b.id = "more"; b.textContent = "more"; b.dataset.unrolls = ""; + b.onclick = () => { fold.style.height = "300px"; }; + part.append(head, fold, b); + v.append(s, part, Object.assign(document.createElement("div"), { style: "flex:none;height:600px" })); + }); + await scrollTo("#more"); + const head = await top(page, "#head"), more = await top(page, "#more"); + await page.locator("#more").click(); + await settle(page); + assert(Math.abs((await top(page, "#head")) - head) <= 1, "what's above the fold must stay put"); + assert(Math.abs((await top(page, "#more")) - more - 300) <= 1, "the control must go down with the rows"); + }); + await t.test("a click that asks to go somewhere does", async () => { await reset(); await page.locator(view).evaluate((v) => {