Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(color-contrast): handle text that is outside overflow: hidden ancestor #4357

Merged
merged 4 commits into from
Mar 12, 2024
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
8 changes: 7 additions & 1 deletion lib/commons/dom/get-visible-child-text-rects.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@ const getVisibleChildTextRects = memoize(
* @see https://github.com/dequelabs/axe-core/issues/2178
* @see https://github.com/dequelabs/axe-core/issues/2483
* @see https://github.com/dequelabs/axe-core/issues/2681
*
* also need to resize the nodeRect to fit within the bounds of any overflow: hidden ancestors.
*
* @see https://github.com/dequelabs/axe-core/issues/4253
*/
return clientRects.length ? clientRects : [nodeRect];
return clientRects.length
? clientRects
: filterHiddenRects([nodeRect], overflowHiddenNodes);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to leave this in as a failsafe though it technically shouldn't be needed anymore

}
);
export default getVisibleChildTextRects;
Expand Down
24 changes: 17 additions & 7 deletions lib/rules/color-contrast-matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import {
findUpVirtual,
visuallyOverlaps,
getRootNode,
isInert
isInert,
getOverflowHiddenAncestors
} from '../commons/dom';
import {
visibleVirtual,
removeUnicode,
sanitize,
isIconLigature
} from '../commons/text';
import { rectsOverlap } from '../commons/math';
import { isDisabled } from '../commons/forms';
import { getNodeFromTree, querySelectorAll, tokenList } from '../core/utils';

Expand Down Expand Up @@ -147,14 +149,22 @@ function colorContrastMatches(node, virtualNode) {
}
}

const rects = range.getClientRects();
for (let index = 0; index < rects.length; index++) {
const rects = Array.from(range.getClientRects());
const clippingAncestors = getOverflowHiddenAncestors(virtualNode);
return rects.some(rect => {
//check to see if the rectangle impinges
if (visuallyOverlaps(rects[index], node)) {
return true;
const overlaps = visuallyOverlaps(rect, node);

if (!clippingAncestors.length) {
return overlaps;
}
}
return false;

const withinOverflow = clippingAncestors.some(overflowNode => {
return rectsOverlap(rect, overflowNode.boundingClientRect);
});

return overlaps && withinOverflow;
});
}

export default colorContrastMatches;
Expand Down
16 changes: 16 additions & 0 deletions test/commons/dom/get-visible-child-text-rects.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,20 @@ describe('dom.getVisibleChildTextRects', () => {

assert.lengthOf(actual, 2);
});

it('changes nodeRect size if all text rects got outside ancestor overflow', () => {
fixtureSetup(`
<div style="overflow: hidden; width: 50px;">
<div style="overflow: hidden; width: 25px">
<div id="target" style="padding-left: 65px;">Hello World</div>
</div>
</div>
`);
const node = fixture.querySelector('#target');
const actual = getVisibleChildTextRects(node);
const rect = getClientRects(node)[0];
const expected = new DOMRect(rect.left, rect.top, 25, rect.height);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as addressing the issue this makes sense. But this example is odd. This element has no visible text, yet axe is deciding it does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking and looking into it, I don't believe this is the correct solution. color-contrast-matches uses a function to determine if an element has visible text children, if it doesn't it moves it to inapplicable. So text nodes that fully start outside an overflow hidden ancestor are ignored. I think text nodes that are causing this bug should also be ignored in that case.


assertRectsEqual(actual, [expected]);
});
});
6 changes: 6 additions & 0 deletions test/integration/rules/color-contrast/color-contrast.html
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,9 @@
>
Hello world
</div>

<div style="overflow: hidden; width: 50px">
<div style="overflow: hidden; width: 25px">
<div id="ignore13" style="padding-left: 65px">Hello World</div>
</div>
</div>
13 changes: 13 additions & 0 deletions test/rule-matches/color-contrast-matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,19 @@ describe('color-contrast-matches', function () {
assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(target)));
});

it('should not match text outside overflow', () => {
fixture.innerHTML = `
<div style="overflow: hidden; width: 50px;">
<div style="overflow: hidden; width: 25px">
<div id="target" style="padding-left: 65px;">Hello World</div>
</div>
</div>
`;
var target = fixture.querySelector('#target');
axe.testUtils.flatTreeSetup(fixture);
assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(target)));
});

if (shadowSupport) {
it('should match a descendant of an element across a shadow boundary', function () {
fixture.innerHTML =
Expand Down
Loading