Lightbox: pin and size to visible viewport when framed

The staff-report iframe is expanded to full content height (no nested
scrollbar), so a modal <dialog> centered in its viewport landed in the
middle of the whole document and sat off-screen unless the parent page
was scrolled there. Cross-origin means the iframe can't read the
parent's scroll position itself.

Tab.tpl now broadcasts the iframe's visible slice (webform-mw-viewport,
rAF-throttled on scroll/resize/load and after each height change), and
AttachmentLightbox pins to that slice and fills its height. Falls back
to a fixed box when no viewport message arrives (older extension), and
standalone mode keeps native centering at a taller 85vh.

Extension bumped to 0.3.1 (template change -> re-copy + cv flush on
prod). PRODUCTION_CUTOVER.md change log updated.
This commit is contained in:
Joel Brock
2026-06-19 08:03:40 -07:00
parent b4f733ee65
commit e4085aa0f3
4 changed files with 146 additions and 12 deletions
@@ -22,7 +22,41 @@
// Add a little headroom so the report's own bottom padding isn't clipped.
var h = Math.max(600, Math.floor(d.height) + 24);
frame.style.height = h + 'px';
// Geometry changed tell the child where its visible slice now is.
scheduleViewport();
}, false);
// The iframe is expanded to full content height, so the report has no
// internal scroll context. A modal opened inside it (the attachment
// lightbox) would center in the full iframe and sit off-screen. Broadcast
// the iframe's currently-visible region (in the child's own content
// coordinates) so the lightbox can pin and size itself to it. The child
// listens for 'webform-mw-viewport'.
var ticking = false;
var raf = window.requestAnimationFrame || function (cb) { return setTimeout(cb, 16); };
function postViewport() {
ticking = false;
if (!frame.contentWindow) return;
var r = frame.getBoundingClientRect();
var winH = window.innerHeight || document.documentElement.clientHeight;
var visibleTop = Math.max(0, r.top);
var visibleBottom = Math.min(winH, r.bottom);
frame.contentWindow.postMessage({
type: 'webform-mw-viewport',
// px from the iframe's content top down to the first visible row.
top: Math.max(0, -r.top),
height: Math.max(0, visibleBottom - visibleTop)
}, APP_ORIGIN || '*');
}
function scheduleViewport() {
if (ticking) return;
ticking = true;
raf(postViewport);
}
window.addEventListener('scroll', scheduleViewport, { passive: true });
window.addEventListener('resize', scheduleViewport);
window.addEventListener('load', scheduleViewport);
scheduleViewport();
})();
</script>
{else}