From 5dbf0697cee5e48a5bfbac48f34a4f4698b6155f Mon Sep 17 00:00:00 2001 From: Louis King Date: Sat, 4 Jul 2026 10:38:08 +0100 Subject: [PATCH] fix(web): path-node popover follows page scroll The popover used position: fixed with viewport-relative coordinates, so it stayed pinned to the viewport while the page scrolled away beneath it. Switch to absolute positioning anchored to the document (adding scrollX/scrollY) and read the badge's live rect on each reposition so it tracks the anchor after the async node fetch resolves. Co-Authored-By: Claude Opus 4.8 --- .../static/js/spa/pages/packet-group-detail.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/meshcore_hub/web/static/js/spa/pages/packet-group-detail.js b/src/meshcore_hub/web/static/js/spa/pages/packet-group-detail.js index 1044c48..2bb76f2 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/packet-group-detail.js +++ b/src/meshcore_hub/web/static/js/spa/pages/packet-group-detail.js @@ -126,8 +126,9 @@ export async function render(container, params, router) { return tagName || n.name || truncateKey(n.public_key, 12); } - function positionPopover(rect) { + function positionPopover(badgeEl) { if (!popoverEl) return; + const rect = badgeEl.getBoundingClientRect(); const margin = 8; const pw = popoverEl.offsetWidth || 256; const ph = popoverEl.offsetHeight || 0; @@ -137,8 +138,9 @@ export async function render(container, params, router) { if (top + ph + margin > window.innerHeight && rect.top - ph - 4 > margin) { top = rect.top - ph - 4; } - popoverEl.style.left = `${left}px`; - popoverEl.style.top = `${top}px`; + // Anchor to the document (position: absolute) so the popover scrolls with the page. + popoverEl.style.left = `${left + window.scrollX}px`; + popoverEl.style.top = `${top + window.scrollY}px`; } function popoverShell(hashLabel, body) { @@ -153,15 +155,15 @@ export async function render(container, params, router) { async function openPathPopover(e, ph) { e.preventDefault(); e.stopPropagation(); - const rect = e.currentTarget.getBoundingClientRect(); + const badgeEl = e.currentTarget; closePopover(); popoverEl = document.createElement('div'); - popoverEl.className = 'path-node-popover fixed z-[1000] w-64 max-w-[90vw] max-h-[60vh] overflow-y-auto bg-base-100 rounded-box shadow-lg border border-base-300'; + popoverEl.className = 'path-node-popover absolute z-[1000] w-64 max-w-[90vw] max-h-[60vh] overflow-y-auto bg-base-100 rounded-box shadow-lg border border-base-300'; document.body.appendChild(popoverEl); litRender(popoverShell(ph, html`
${loading()}
`), popoverEl); - positionPopover(rect); + positionPopover(badgeEl); const onDocClick = (ev) => { if (popoverEl && !popoverEl.contains(ev.target)) closePopover(); }; const onKey = (ev) => { if (ev.key === 'Escape') closePopover(); }; @@ -194,11 +196,11 @@ export async function render(container, params, router) { ` : nothing} `; litRender(popoverShell(ph, body), popoverEl); - positionPopover(rect); + positionPopover(badgeEl); } catch (err) { if (isAbortError(err) || !popoverEl) return; litRender(popoverShell(ph, html`
${warningBadge(err.message)}
`), popoverEl); - positionPopover(rect); + positionPopover(badgeEl); } }