From 36817cf8e501087b0c06189546f66982e09314bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 13 Jul 2026 13:23:57 +0000 Subject: [PATCH] fix(cli): honor text overflow opt-outs --- .../cli/src/commands/layout-audit.browser.js | 9 +++- .../src/commands/layout-audit.browser.test.ts | 43 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/layout-audit.browser.js b/packages/cli/src/commands/layout-audit.browser.js index 1d57956fa1..c21fcea88b 100644 --- a/packages/cli/src/commands/layout-audit.browser.js +++ b/packages/cli/src/commands/layout-audit.browser.js @@ -105,6 +105,10 @@ return !!element.closest("[data-layout-allow-overflow]"); } + function hasTextClipOptOut(element) { + return hasAllowOverflowFlag(element) || element.hasAttribute("data-layout-bleed"); + } + function opacityChain(element) { let opacity = 1; for (let current = element; current; current = current.parentElement) { @@ -394,6 +398,7 @@ } function clippedTextIssue(element, time, tolerance) { + if (hasTextClipOptOut(element)) return null; const style = getComputedStyle(element); if (!clipsOverflow(style)) return null; const overflowX = element.scrollWidth - element.clientWidth; @@ -455,7 +460,7 @@ const containerOverflow = overflowFor(textRect, containerRect, tolerance, verticalTolerance); if ( containerOverflow && - !hasAllowOverflowFlag(element) && + !hasTextClipOptOut(element) && !clippedByAncestor(element, container) ) { const style = elementStyle; @@ -481,7 +486,7 @@ } const canvasOverflow = overflowFor(textRect, rootRect, tolerance); - if (canvasOverflow && !hasAllowOverflowFlag(element)) { + if (canvasOverflow && !hasTextClipOptOut(element)) { issues.push({ code: "canvas_overflow", severity: "info", diff --git a/packages/cli/src/commands/layout-audit.browser.test.ts b/packages/cli/src/commands/layout-audit.browser.test.ts index 1a334a5b68..5daa392dd2 100644 --- a/packages/cli/src/commands/layout-audit.browser.test.ts +++ b/packages/cli/src/commands/layout-audit.browser.test.ts @@ -141,6 +141,49 @@ describe("layout-audit.browser", () => { expect(runAudit()).toEqual([]); }); + it("suppresses intentional ellipsis clipping under overflow opt-outs", () => { + document.body.innerHTML = ` +
+
+
+ Intentional long truncated label +
+
+
+ `; + const headline = document.querySelector("#headline"); + if (!(headline instanceof HTMLElement)) throw new Error("missing headline"); + Object.defineProperties(headline, { + clientWidth: { configurable: true, value: 100 }, + scrollWidth: { configurable: true, value: 240 }, + clientHeight: { configurable: true, value: 20 }, + scrollHeight: { configurable: true, value: 20 }, + }); + installGeometry( + { + root: rect({ left: 0, top: 0, width: 640, height: 360 }), + headline: rect({ left: 40, top: 60, width: 100, height: 20 }), + text: rect({ left: 40, top: 60, width: 240, height: 20 }), + }, + { + headline: { overflow: "hidden", overflowX: "hidden", overflowY: "hidden" }, + }, + ); + + installAuditScript(); + const textOverflowCodes = () => + runAudit() + .map((issue) => issue.code) + .filter((code) => code === "clipped_text" || code === "text_box_overflow"); + + expect(textOverflowCodes()).toEqual(["clipped_text", "text_box_overflow"]); + document.querySelector("#overflow-optout")?.setAttribute("data-layout-allow-overflow", ""); + expect(textOverflowCodes()).toEqual([]); + document.querySelector("#overflow-optout")?.removeAttribute("data-layout-allow-overflow"); + headline.setAttribute("data-layout-bleed", "true"); + expect(textOverflowCodes()).toEqual([]); + }); + it("does not flag glyph-ink vertical spill within the font-metric band on a non-clipping box", () => { // A painted, non-clipping caption-word-like box whose glyph ink (text rect) exceeds its snug // line-height box by a few px vertically — normal typography, nothing is clipped. (fontSize