Product
axe-core
Product Version
4.8.2
Latest Version
Issue Description
Expectation
The target-offset check uses a get-target-rects helper to break down a "target" into a collection of rects. Part of that work involves checking for any other elements that might be obscuring that target. That check is meant to treat children of the target as part of the target, but the way it does so isn't compatible with a target that contains shadow dom children - it assumes that parentElement.contains(childElementWithinShadow) will return true, but it doesn't. We already have a shadow-aware version of contains (at /lib/core/utils/contains.js) which this should use instead. We should audit for other uses of the wrong contains as part of fixing this issue. Bonus points for a new eslint no-restricted-syntax config that warns us if we accidentally try to use the native one directly again in the future.
This causes getTargetRects to treat the child of the target as obscuring the target. In the motivating case, this ends up treating the target as completely obscured, which causes getTargetRects to return an empty array.
This in turn triggers a separate but related issue in target-offset - when a target's neighbor has an empty target rect array, it treats that neighbor as being 0 distance away from the target. It should treat fully-obscured neighbors as omitted from consideration for the check, not as being 0 distance away.
Actual
In the repro snippet below, axe.run({runOnly: 'target-size'}) emits a false positive violation for a case that ought to pass the SC's spacing exception.
How to Reproduce
<script>
const shadowTemplate = document.createElement('template')
shadowTemplate.innerHTML = '<div id="shadow-container"><slot></slot></div>';
class ShadowOpenWebComponent extends HTMLElement {
connectedCallback() {
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(shadowTemplate.content.cloneNode(true));
}
}
customElements.define('shadow-open', ShadowOpenWebComponent);
</script>
<style>
#container {
font-size: 16px;
}
#title {
background-color: #cff;
}
#content {
background-color: #cfc;
margin-top: 10px;
}
</style>
<div id="container">
<div id="title">
<!-- getTargetRects will incorrectly return [] for this element... -->
<a id="title-link" href="#target">
<shadow-open>
<!-- ...because it doesn't consider this div to be "contained by" #title-link, so thinks it's obscuring it instead -->
<div>Title</div>
</shadow-open>
</a>
</div>
<div id="content">
<!-- target-offset check will incorrectly consider this element to have offset distance 0 from #title-link -->
<a id="content-link" href="#target">
<div>Content</div>
</a>
</div>
</div>
Additional context
Product
axe-core
Product Version
4.8.2
Latest Version
Issue Description
Expectation
The
target-offsetcheck uses aget-target-rectshelper to break down a "target" into a collection of rects. Part of that work involves checking for any other elements that might be obscuring that target. That check is meant to treat children of the target as part of the target, but the way it does so isn't compatible with a target that contains shadow dom children - it assumes thatparentElement.contains(childElementWithinShadow)will return true, but it doesn't. We already have a shadow-aware version ofcontains(at/lib/core/utils/contains.js) which this should use instead. We should audit for other uses of the wrongcontainsas part of fixing this issue. Bonus points for a new eslintno-restricted-syntaxconfig that warns us if we accidentally try to use the native one directly again in the future.This causes
getTargetRectsto treat the child of the target as obscuring the target. In the motivating case, this ends up treating the target as completely obscured, which causesgetTargetRectsto return an empty array.This in turn triggers a separate but related issue in
target-offset- when a target's neighbor has an empty target rect array, it treats that neighbor as being 0 distance away from the target. It should treat fully-obscured neighbors as omitted from consideration for the check, not as being 0 distance away.Actual
In the repro snippet below,
axe.run({runOnly: 'target-size'})emits a false positive violation for a case that ought to pass the SC's spacing exception.How to Reproduce
Additional context