Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions packages/cli/src/commands/layout-audit.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,8 @@
return rects;
}

function textRectFor(element, directOnly) {
const rects = textClientRects(element, directOnly);
function unionRects(rects) {
if (rects.length === 0) return null;

const union = rects.reduce(
(acc, rect) => ({
left: Math.min(acc.left, rect.left),
Expand All @@ -263,14 +261,45 @@
bottom: Number.NEGATIVE_INFINITY,
},
);

return toRect({
...union,
width: union.right - union.left,
height: union.bottom - union.top,
});
}

function visibleTextClientRects(element, directOnly) {
// Range rects stay geometrically present outside an overflow clip. Reduce
// them in viewport coordinates so overlap measures only paintable text.
let rects = textClientRects(element, directOnly).map(toRect);
for (
let ancestor = element.parentElement;
ancestor && rects.length > 0;
ancestor = ancestor.parentElement
) {
const style = getComputedStyle(ancestor);
const clipX = clipsOverflowValue(style.overflowX || style.overflow);
const clipY = clipsOverflowValue(style.overflowY || style.overflow);
if (!clipX && !clipY) continue;
const clip = toRect(ancestor.getBoundingClientRect());
rects = rects
.map((rect) => {
const left = clipX ? Math.max(rect.left, clip.left) : rect.left;
const right = clipX ? Math.min(rect.right, clip.right) : rect.right;
const top = clipY ? Math.max(rect.top, clip.top) : rect.top;
const bottom = clipY ? Math.min(rect.bottom, clip.bottom) : rect.bottom;
if (right - left <= 0.5 || bottom - top <= 0.5) return null;
return toRect({ left, right, top, bottom, width: right - left, height: bottom - top });
})
.filter(Boolean);
}
return rects;
}

function textRectFor(element, directOnly) {
return unionRects(textClientRects(element, directOnly));
}

function parsePx(value) {
const parsed = Number.parseFloat(value);
return Number.isFinite(parsed) ? parsed : 0;
Expand Down Expand Up @@ -317,9 +346,13 @@
return hasBackground || hasImage || hasBorder || hasRadius;
}

function clipsOverflowValue(value) {
return value && value !== "visible" && value !== "clip visible";
}

function clipsOverflow(style) {
return [style.overflowX, style.overflowY, style.overflow].some(
(value) => value && value !== "visible" && value !== "clip visible",
return [style.overflowX, style.overflowY, style.overflow].some((value) =>
clipsOverflowValue(value),
);
}

Expand Down Expand Up @@ -587,8 +620,8 @@
const blocks = [];
for (const element of Array.from(root.querySelectorAll("*"))) {
if (!isSolidTextBlock(element)) continue;
const rects = textClientRects(element, true);
const rect = textRectFor(element, true);
const rects = visibleTextClientRects(element, true);
const rect = unionRects(rects);
if (rect) blocks.push({ element, rect, rects });
}
return blocks;
Expand Down
60 changes: 58 additions & 2 deletions packages/cli/src/commands/layout-audit.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,31 @@ describe("layout-audit.browser content overlap", () => {
});
expect(issues.some((issue) => issue.code === "content_overlap")).toBe(true);
});

it.each(["hidden", "clip", "auto", "scroll"])(
"excludes fully clipped text under overflow:%s",
(overflow) => {
const issues = auditOverflowClippedOverlap({
overflow,
clipRect: rect({ left: 0, top: 0, width: 640, height: 100 }),
aTextRect: rect({ left: 100, top: 180, width: 300, height: 80 }),
bTextRect: rect({ left: 120, top: 190, width: 300, height: 80 }),
});

expect(issues.some((issue) => issue.code === "content_overlap")).toBe(false);
},
);

it("uses the painted fragment area after partial overflow clipping", () => {
const issues = auditOverflowClippedOverlap({
overflow: "hidden",
clipRect: rect({ left: 390, top: 0, width: 250, height: 200 }),
aTextRect: rect({ left: 100, top: 50, width: 300, height: 100 }),
bTextRect: rect({ left: 397, top: 50, width: 50, height: 100 }),
});

expect(issues.some((issue) => issue.code === "content_overlap")).toBe(true);
});
});

describe("contrast-audit.browser clip-path visibility", () => {
Expand Down Expand Up @@ -1721,9 +1746,33 @@ function auditOverlapScene(options: {
return runAudit();
}

function auditOverflowClippedOverlap(options: {
overflow: string;
clipRect: DOMRect;
aTextRect: DOMRect;
bTextRect: DOMRect;
}): ReturnType<typeof runAudit> {
document.body.innerHTML = `
<div id="root" data-composition-id="main" data-width="1920" data-height="1080">
<div id="clip"><div id="a">Block A copy</div></div>
<div id="b">Block B copy</div>
</div>
`;
const textRects = { a: [options.aTextRect], b: [options.bTextRect] };
installOverlapStyles(
{ a: "rgb(0, 0, 0)", b: "rgb(0, 0, 0)" },
{ a: "none", b: "none" },
{ clip: options.overflow },
);
installOverlapGeometry(textRects, { clip: options.clipRect });
installAuditScript();
return runAudit();
}

function installOverlapStyles(
colors: Record<string, string>,
clipPaths: Record<string, string>,
overflows: Record<string, string> = {},
): void {
vi.spyOn(window, "getComputedStyle").mockImplementation((element) => {
const id = (element as Element).id;
Expand All @@ -1733,6 +1782,9 @@ function installOverlapStyles(
opacity: "1",
color: colors[id] ?? "rgb(0, 0, 0)",
clipPath: clipPaths[id] ?? "none",
overflow: overflows[id] ?? "visible",
overflowX: overflows[id] ?? "visible",
overflowY: overflows[id] ?? "visible",
} as unknown as CSSStyleDeclaration;
});

Expand All @@ -1745,10 +1797,14 @@ function installOverlapStyles(
};
}

function installOverlapGeometry(textRects: Record<string, DOMRect[]>): void {
function installOverlapGeometry(
textRects: Record<string, DOMRect[]>,
elementRects: Record<string, DOMRect> = {},
): void {
for (const element of Array.from(document.querySelectorAll("*"))) {
vi.spyOn(element, "getBoundingClientRect").mockReturnValue(
boundingTextRect(textRects[element.id]) ??
elementRects[element.id] ??
boundingTextRect(textRects[element.id]) ??
rect({ left: 0, top: 0, width: 1920, height: 1080 }),
);
}
Expand Down
Loading