fix(html-embed): correct stale iframe height and damp the resize loop

Address the non-test code-review findings on the htmlEmbed sandbox change
(test-coverage gaps are tracked in issue #99):

- html-embed-view: track the iframe's reported content height even while a
  fixed height is set, so clearing the height (fixed -> auto) without editing
  the source no longer leaves the frame pinned to the stale value. Derive the
  fixed-height predicate once; seed autoHeight to the default.
- html-embed-view: drop width/border from the iframe inline style (the
  .htmlEmbedFrame CSS class already provides them).
- html-embed-sandbox: coalesce height reports via requestAnimationFrame and
  skip <=1px deltas to damp the self-measure feedback loop; fix the misleading
  bootstrap comment.
- tracker-settings: add an aria-label to the snippet Textarea (a11y).
- CHANGELOG: note the removal of server-side role-based HTML-embed stripping.
This commit is contained in:
claude_code
2026-06-21 03:50:17 +03:00
parent bed3d3d286
commit c596e17a40
4 changed files with 46 additions and 18 deletions

View File

@@ -22,20 +22,42 @@ export function buildSandboxSrcdoc(source: string): string {
const bootstrap = `
<script>
(function () {
function reportHeight() {
var lastSent = -1;
var scheduled = false;
function measure() {
var doc = document.documentElement;
var body = document.body;
var height = Math.max(
return Math.max(
doc ? doc.scrollHeight : 0,
body ? body.scrollHeight : 0
);
}
function flush() {
scheduled = false;
var height = measure();
// Only report when the height actually changed by more than 1px. This
// damps the iframe self-measure feedback loop: content sized to the iframe
// viewport would otherwise oscillate as the parent resizes the frame in
// response to each report.
if (Math.abs(height - lastSent) <= 1) return;
lastSent = height;
parent.postMessage(
{ type: ${JSON.stringify(HTML_EMBED_HEIGHT_MESSAGE)}, height: height },
"*"
);
}
function reportHeight() {
if (scheduled) return;
scheduled = true;
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(flush);
} else {
flush();
}
}
window.addEventListener("load", reportHeight);
// Report immediately too, in case load already fired.
// Report an initial height now (runs during parse, before load/images
// settle); the load handler and ResizeObserver refine it as content changes.
reportHeight();
if (typeof ResizeObserver !== "undefined") {
try {

View File

@@ -62,21 +62,24 @@ export default function HtmlEmbedView(props: NodeViewProps) {
const [draft, setDraft] = useState<string>(source || "");
const [draftHeight, setDraftHeight] = useState<number | "">(height ?? "");
// Auto-resize height tracked in state (used only when no fixed height is set).
const [autoHeight, setAutoHeight] = useState<number>(
typeof height === "number" && Number.isFinite(height)
? height
: DEFAULT_IFRAME_HEIGHT,
);
// True when the author pinned an explicit height; otherwise we auto-resize to
// the iframe's reported content height.
const hasFixedHeight = typeof height === "number" && Number.isFinite(height);
// Auto-resize height tracked in state. Seeded to the default and updated from
// the iframe's postMessage reports (see effect below) regardless of mode, so
// switching a fixed-height embed back to auto immediately reflects the last
// reported content height instead of staying pinned to the old fixed value.
const [autoHeight, setAutoHeight] = useState<number>(DEFAULT_IFRAME_HEIGHT);
const srcdoc = useMemo(() => buildSandboxSrcdoc(source || ""), [source]);
// Auto-resize: accept height messages ONLY from this iframe's own content
// window. The sandboxed srcdoc has an opaque ("null") origin, so we cannot
// match by event.origin — we match by event.source instead. No-op when a
// fixed height is configured.
// match by event.origin — we match by event.source instead. We track the
// reported height even while a fixed height is in effect, so toggling back to
// auto shows the current content height with no iframe reload.
useEffect(() => {
if (typeof height === "number" && Number.isFinite(height)) return;
function onMessage(event: MessageEvent) {
if (event.source !== iframeRef.current?.contentWindow) return;
const data = event.data as { type?: string; height?: number };
@@ -87,12 +90,9 @@ export default function HtmlEmbedView(props: NodeViewProps) {
}
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}, [height]);
}, []);
const effectiveHeight =
typeof height === "number" && Number.isFinite(height)
? clampHeight(height)
: autoHeight;
const effectiveHeight = hasFixedHeight ? clampHeight(height) : autoHeight;
const openEditor = useCallback(() => {
setDraft(source || "");
@@ -157,7 +157,7 @@ export default function HtmlEmbedView(props: NodeViewProps) {
srcDoc={srcdoc}
title={t("HTML embed")}
referrerPolicy="no-referrer"
style={{ width: "100%", border: "none", height: effectiveHeight }}
style={{ height: effectiveHeight }}
/>
) : canEdit ? (
<div className={classes.htmlEmbedPlaceholder} onClick={openEditor}>

View File

@@ -78,6 +78,7 @@ export default function TrackerSettings() {
autosize
minRows={6}
maxRows={20}
aria-label={t("Analytics / tracker")}
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
placeholder={t("<script>...</script>")}