Skip to content

Commit 3a90bb7

Browse files
authored
fix(target-size): pass for element that has nearby elements that are obscured (#4422)
Had to update how we handled the too many rects break early since it would return an empty array, which when looking at the lengths of the arrays in `getOffset` made it difficult to know which case needed to be handled (returned empty due to too many rects or returned empty because there wasn't any visible rect). Talked to Wilco and we agreed that when we encountered too many rects we could throw and handle the error case in both checks. Closes: #4387
1 parent 08ddcbc commit 3a90bb7

File tree

10 files changed

+81
-16
lines changed

10 files changed

+81
-16
lines changed

lib/checks/mobile/target-offset-evaluate.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,27 @@ export default function targetOffsetEvaluate(node, options, vNode) {
2020
continue;
2121
}
2222
// the offset code works off radius but we want our messaging to reflect diameter
23-
const offset =
24-
roundToSingleDecimal(getOffset(vNode, vNeighbor, minOffset / 2)) * 2;
23+
let offset = null;
24+
try {
25+
offset = getOffset(vNode, vNeighbor, minOffset / 2);
26+
} catch (err) {
27+
if (err.message.startsWith('splitRects')) {
28+
this.data({
29+
messageKey: 'tooManyRects',
30+
closestOffset: 0,
31+
minOffset
32+
});
33+
return undefined;
34+
}
35+
36+
throw err;
37+
}
38+
39+
if (offset === null) {
40+
continue;
41+
}
42+
43+
offset = roundToSingleDecimal(offset) * 2;
2544
if (offset + roundingMargin >= minOffset) {
2645
continue;
2746
}

lib/checks/mobile/target-offset.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"fail": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px instead of at least ${data.minOffset}px.",
1515
"incomplete": {
1616
"default": "Element with negative tabindex has insufficient space to its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px instead of at least ${data.minOffset}px. Is this a target?",
17-
"nonTabbableNeighbor": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px instead of at least ${data.minOffset}px. Is the neighbor a target?"
17+
"nonTabbableNeighbor": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px instead of at least ${data.minOffset}px. Is the neighbor a target?",
18+
"tooManyRects": "Could not get the target size because there are too many overlapping elements"
1819
}
1920
}
2021
}

lib/checks/mobile/target-size-evaluate.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,10 @@ function getLargestUnobscuredArea(vNode, obscuredNodes) {
131131
const obscuringRects = obscuredNodes.map(
132132
({ boundingClientRect: rect }) => rect
133133
);
134-
const unobscuredRects = splitRects(nodeRect, obscuringRects);
135-
if (unobscuredRects.length === 0) {
134+
let unobscuredRects;
135+
try {
136+
unobscuredRects = splitRects(nodeRect, obscuringRects);
137+
} catch (err) {
136138
return null;
137139
}
138140

lib/commons/math/get-offset.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function getOffset(vTarget, vNeighbor, minRadiusNeighbour = 12) {
1818
const neighborRects = getTargetRects(vNeighbor);
1919

2020
if (!targetRects.length || !neighborRects.length) {
21-
return 0;
21+
return null;
2222
}
2323

2424
const targetBoundingBox = targetRects.reduce(getBoundingRect);

lib/commons/math/split-rects.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function splitRects(outerRect, overlapRects) {
1818
// a performance bottleneck
1919
// @see https://github.com/dequelabs/axe-core/issues/4359
2020
if (uniqueRects.length > 4000) {
21-
return [];
21+
throw new Error('splitRects: Too many rects');
2222
}
2323
}
2424
return uniqueRects;

locales/_template.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,8 @@
872872
"fail": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px instead of at least ${data.minOffset}px.",
873873
"incomplete": {
874874
"default": "Element with negative tabindex has insufficient space to its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px instead of at least ${data.minOffset}px. Is this a target?",
875-
"nonTabbableNeighbor": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px instead of at least ${data.minOffset}px. Is the neighbor a target?"
875+
"nonTabbableNeighbor": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px instead of at least ${data.minOffset}px. Is the neighbor a target?",
876+
"tooManyRects": "Could not get the target size because there are too many overlapping elements"
876877
}
877878
},
878879
"target-size": {

test/checks/mobile/target-offset.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,21 @@ describe('target-offset tests', () => {
9898
assert.closeTo(checkContext._data.closestOffset, 24, 0.2);
9999
});
100100

101+
it('ignores obscured widget elements as neighbors', () => {
102+
const checkArgs = checkSetup(`
103+
<div style="position: fixed; bottom: 0">
104+
<a href="#">Go to top</a>
105+
</div>
106+
<div id="target" style="position: fixed; bottom: 0; left: 0; right: 0; background: #eee">
107+
Cookies: <a href="#">Accept all cookies</a>
108+
</div>
109+
`);
110+
111+
assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
112+
assert.equal(checkContext._data.minOffset, 24);
113+
assert.closeTo(checkContext._data.closestOffset, 24, 0.2);
114+
});
115+
101116
it('sets all elements that are too close as related nodes', () => {
102117
const checkArgs = checkSetup(
103118
'<a href="#" id="left" style="' +
@@ -120,7 +135,7 @@ describe('target-offset tests', () => {
120135
assert.deepEqual(relatedIds, ['#left', '#right']);
121136
});
122137

123-
it('returns false if there are too many focusable widgets', () => {
138+
it('returns undefined if there are too many focusable widgets', () => {
124139
let html = '';
125140
for (let i = 0; i < 100; i++) {
126141
html += `
@@ -137,8 +152,9 @@ describe('target-offset tests', () => {
137152
<table id="tab-table">${html}</table>
138153
</div>
139154
`);
140-
assert.isFalse(checkEvaluate.apply(checkContext, checkArgs));
155+
assert.isUndefined(checkEvaluate.apply(checkContext, checkArgs));
141156
assert.deepEqual(checkContext._data, {
157+
messageKey: 'tooManyRects',
142158
closestOffset: 0,
143159
minOffset: 24
144160
});

test/commons/math/get-offset.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,46 @@ describe('getOffset', () => {
3333
assert.closeTo(getOffset(nodeA, nodeB), 63.6, round);
3434
});
3535

36-
it('returns 0 if nodeA is overlapped by nodeB', () => {
36+
it('returns null if nodeA is overlapped by nodeB', () => {
3737
const fixture = fixtureSetup(`
3838
<button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 0">&nbsp;</button>
3939
<button style="width: 30px; height: 30px; margin: 0; padding: 0; position: absolute; top: -10px; left: -10px">&nbsp;</button>
4040
`);
4141
const nodeA = fixture.children[1];
4242
const nodeB = fixture.children[3];
43-
assert.equal(getOffset(nodeA, nodeB), 0);
43+
assert.isNull(getOffset(nodeA, nodeB));
4444
});
4545

46-
it('returns 0 if nodeB is overlapped by nodeA', () => {
46+
it('returns null if nodeB is overlapped by nodeA', () => {
4747
const fixture = fixtureSetup(`
4848
<button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 0">&nbsp;</button>
4949
<button style="width: 30px; height: 30px; margin: 0; padding: 0; position: absolute; top: -10px; left: -10px">&nbsp;</button>
5050
`);
5151
const nodeA = fixture.children[3];
5252
const nodeB = fixture.children[1];
53-
assert.equal(getOffset(nodeA, nodeB), 0);
53+
assert.isNull(getOffset(nodeA, nodeB));
54+
});
55+
56+
it('returns null if nodeA is overlapped by another node', () => {
57+
const fixture = fixtureSetup(`
58+
<button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 0">&nbsp;</button>
59+
<button style="width: 30px; height: 30px; margin: 0; padding: 0; position: absolute; top: 0; left: 20px">&nbsp;</button>
60+
<div style="background: white; width: 30px; height: 30px; margin: 0; padding: 0; position: absolute; top: -10px; left: -10px">&nbsp;</div>
61+
`);
62+
const nodeB = fixture.children[1];
63+
const nodeA = fixture.children[3];
64+
assert.isNull(getOffset(nodeA, nodeB));
65+
});
66+
67+
it('returns null if nodeB is overlapped by another node', () => {
68+
const fixture = fixtureSetup(`
69+
<button style="width: 10px; height: 10px; margin: 0; padding: 0; position: absolute; top: 0; left: 0">&nbsp;</button>
70+
<button style="width: 30px; height: 30px; margin: 0; padding: 0; position: absolute; top: 0; left: 20px">&nbsp;</button>
71+
<div style="background: white; width: 30px; height: 30px; margin: 0; padding: 0; position: absolute; top: -10px; left: -10px">&nbsp;</div>
72+
`);
73+
const nodeA = fixture.children[3];
74+
const nodeB = fixture.children[1];
75+
assert.isNull(getOffset(nodeA, nodeB));
5476
});
5577

5678
it('subtracts minNeighbourRadius from center-to-center calculations', () => {

test/commons/math/split-rects.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ describe('splitRects', () => {
1616
assert.deepEqual(rects[0], rectA);
1717
});
1818

19-
it('returns empty array if there are too many overlapping rects', () => {
19+
it('throws if there are too many overlapping rects', () => {
2020
const rects = [];
2121
for (let i = 0; i < 100; i++) {
2222
rects.push(new DOMRect(i, i, 50, 50));
2323
}
2424
const rectA = new DOMRect(0, 0, 1000, 1000);
25-
assert.lengthOf(splitRects(rectA, rects), 0);
25+
26+
assert.throws(() => {
27+
splitRects(rectA, rects);
28+
}, 'splitRects: Too many rects');
2629
});
2730

2831
describe('with one overlapping rect', () => {

test/integration/full/target-size/too-many-rects.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('target-size too many rects test', () => {
99
elementRef: true
1010
};
1111
const context = {
12+
include: '#incomplete',
1213
// ignore the incomplete table results
1314
exclude: 'table tr'
1415
};

0 commit comments

Comments
 (0)