From 38f4738ecaaf601a77c38fca1b69461405c12c6e Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Tue, 9 Jun 2026 17:14:54 -0700 Subject: [PATCH] Staff report: fix infinite iframe-grow loop when framed in Civi The framed report posts its content height to the parent so the Civi tab can resize the iframe to fit. Two pieces interacted badly: - The root layout sets html.h-full and body.min-h-full, so documentElement and body heights track the iframe's viewport height. - The parent template sets iframe.height = postedHeight + 24 every time a height message arrives. The combination produced an unbounded feedback loop: parent grows the iframe by 24px, viewport grows, document height grows, ResizeObserver fires, we post the new height, parent grows by another 24px. The outer CiviCRM page scrollbar visibly shrank each cycle. Fix on the report side (no extension change needed): when framed, override html height to auto and body min-height to 0 so the document decouples from the viewport. Observe body (the actual content), measure body.scrollHeight, and skip posting when the value is unchanged. Original styles are restored on unmount so route changes back to the standalone view still work. --- components/StaffReportView.tsx | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/components/StaffReportView.tsx b/components/StaffReportView.tsx index 3c610e7..2ff795d 100644 --- a/components/StaffReportView.tsx +++ b/components/StaffReportView.tsx @@ -76,22 +76,39 @@ export function StaffReportView({ // When embedded, post our content height to the parent so the Civi tab's // iframe can resize to fit (no nested scrollbars). The receiving script // lives in the WebForm-mw Civi extension's tab template. + // + // The root layout sets `html.h-full` and `body.min-h-full`, which tie + // document height to the iframe's viewport height. Combined with the + // parent setting `iframe.height = postedHeight + 24` on every message, + // that creates an unbounded feedback loop (viewport grows -> measured + // height grows -> parent grows the iframe -> repeat). Inside the iframe + // we decouple html/body from the viewport, measure `body.scrollHeight` + // (the actual content), observe the body, and skip duplicate posts. useEffect(() => { if (!framed || typeof window === "undefined") return; if (window.parent === window) return; + const html = document.documentElement; + const body = document.body; + const prevHtmlHeight = html.style.height; + const prevBodyMinHeight = body.style.minHeight; + html.style.height = "auto"; + body.style.minHeight = "0"; + let lastHeight = -1; const post = () => { - window.parent.postMessage( - { type: "webform-mw-height", height: document.documentElement.scrollHeight }, - "*", - ); + const h = body.scrollHeight; + if (h === lastHeight) return; + lastHeight = h; + window.parent.postMessage({ type: "webform-mw-height", height: h }, "*"); }; post(); const ro = new ResizeObserver(post); - ro.observe(document.documentElement); + ro.observe(body); window.addEventListener("load", post); return () => { ro.disconnect(); window.removeEventListener("load", post); + html.style.height = prevHtmlHeight; + body.style.minHeight = prevBodyMinHeight; }; }, [framed, load]);