fix: keep checking ancestor opacity after resolving paint opacity

This commit is contained in:
Marco Beretta 2026-06-24 17:20:54 +02:00
parent 206c34528c
commit f865d14b50
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
2 changed files with 31 additions and 5 deletions

View file

@ -466,6 +466,12 @@ describe('isMonochromeSvg', () => {
expect(isMonochromeSvg(svg)).toBe(true);
});
it('ignores a background rect in an opacity-zero group despite its own fill-opacity', () => {
const svg =
'<svg viewBox="0 0 24 24"><g opacity="0"><rect width="24" height="24" fill="#fff" fill-opacity="1" /></g><path fill="#000" d="M4 4h16v16H4z" /></svg>';
expect(isMonochromeSvg(svg)).toBe(true);
});
it('rejects a full-size percentage background rect', () => {
const svg =
'<svg viewBox="0 0 24 24"><rect x="0" y="0" width="100%" height="100%" fill="#eee" /><path fill="#222" /></svg>';
@ -527,6 +533,20 @@ describe('isMonochromeSvg', () => {
});
});
describe('opacity-zero ancestors hide their descendants', () => {
it('ignores a colored paint inside an opacity-zero group despite its own fill-opacity', () => {
const svg =
'<svg viewBox="0 0 24 24"><g opacity="0"><path fill="#f00" fill-opacity="1" d="M0 0h24v24H0z" /></g><path fill="#333" d="M4 4h16v16H4z" /></svg>';
expect(isMonochromeSvg(svg)).toBe(true);
});
it('still counts a paint whose fill-opacity is one under a visible group', () => {
const svg =
'<svg viewBox="0 0 24 24"><g opacity="1"><path fill="#f00" fill-opacity="1" d="M4 4h16v16H4z" /></g></svg>';
expect(isMonochromeSvg(svg)).toBe(false);
});
});
describe('embedded raster/foreign content (not tintable)', () => {
it('rejects an SVG wrapping an embedded raster image (href)', () => {
const svg = '<svg><image href="data:image/png;base64,abc" width="24" height="24" /></svg>';

View file

@ -513,9 +513,11 @@ function isHidden(el: Element, root: Element, rules: StyleRule[]): boolean {
}
/**
* True when a paint is fully transparent through opacity: the element (or an
* ancestor group) has `opacity:0`, or the inherited paint-specific opacity (e.g.
* `fill-opacity`) resolves to zero.
* True when a paint is fully transparent through opacity: the element or any
* ancestor group has `opacity:0` (group opacity is not inherited and composites
* the whole subtree), or the nearest paint-specific opacity (e.g. `fill-opacity`)
* resolves to zero. The nearest paint-specific opacity wins, but the walk keeps
* checking ancestors for `opacity:0` even after a nonzero one is found.
*/
function paintInvisible(
el: Element,
@ -524,15 +526,19 @@ function paintInvisible(
paintProp: string,
): boolean {
const opacityProp = PAINT_OPACITY.get(paintProp);
let paintOpacityResolved = false;
let current: Element | null = el;
while (current != null) {
if (styleNumber(current, rules, 'opacity') === 0) {
return true;
}
if (opacityProp != null) {
if (opacityProp != null && !paintOpacityResolved) {
const value = styleNumber(current, rules, opacityProp);
if (value != null) {
return value === 0;
if (value === 0) {
return true;
}
paintOpacityResolved = true;
}
}
if (current === root) {