diff --git a/lib/checks/mobile/target-offset-evaluate.js b/lib/checks/mobile/target-offset-evaluate.js index e799cb68f1..8e45fc8164 100644 --- a/lib/checks/mobile/target-offset-evaluate.js +++ b/lib/checks/mobile/target-offset-evaluate.js @@ -1,11 +1,18 @@ import { findNearbyElms, isFocusable, isInTabOrder } from '../../commons/dom'; import { getRoleType } from '../../commons/aria'; -import { getOffset } from '../../commons/math'; +import { getOffset, rectHasMinimumSize } from '../../commons/math'; const roundingMargin = 0.05; export default function targetOffsetEvaluate(node, options, vNode) { const minOffset = options?.minOffset || 24; + // Bail early to avoid hitting very expensive calculations. + // Targets are so large they are unlikely to fail. + if (rectHasMinimumSize(minOffset * 10, vNode.boundingClientRect)) { + this.data({ messageKey: 'large', minOffset }); + return true; + } + const closeNeighbors = []; let closestOffset = minOffset; for (const vNeighbor of findNearbyElms(vNode, minOffset)) { diff --git a/lib/checks/mobile/target-offset.json b/lib/checks/mobile/target-offset.json index 2aebbdf9f4..c06ee11d7a 100644 --- a/lib/checks/mobile/target-offset.json +++ b/lib/checks/mobile/target-offset.json @@ -7,7 +7,10 @@ "metadata": { "impact": "serious", "messages": { - "pass": "Target has sufficient space from its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px which is at least ${data.minOffset}px.", + "pass": { + "default": "Target has sufficient space from its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px which is at least ${data.minOffset}px.", + "large": "Target far exceeds the minimum size of ${data.minOffset}px." + }, "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.", "incomplete": { "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?", diff --git a/lib/checks/mobile/target-size-evaluate.js b/lib/checks/mobile/target-size-evaluate.js index 3d692606e3..6ee45bc9c5 100644 --- a/lib/checks/mobile/target-size-evaluate.js +++ b/lib/checks/mobile/target-size-evaluate.js @@ -13,6 +13,13 @@ import { export default function targetSizeEvaluate(node, options, vNode) { const minSize = options?.minSize || 24; const nodeRect = vNode.boundingClientRect; + // Bail early to avoid hitting very expensive calculations. + // Targets are so large they are unlikely to fail. + if (rectHasMinimumSize(minSize * 10, nodeRect)) { + this.data({ messageKey: 'large', minSize }); + return true; + } + const hasMinimumSize = rectHasMinimumSize.bind(null, minSize); const nearbyElms = findNearbyElms(vNode); const overflowingContent = filterOverflowingContent(vNode, nearbyElms); diff --git a/lib/checks/mobile/target-size.json b/lib/checks/mobile/target-size.json index 649d075ae8..302e4f985e 100644 --- a/lib/checks/mobile/target-size.json +++ b/lib/checks/mobile/target-size.json @@ -9,7 +9,8 @@ "messages": { "pass": { "default": "Control has sufficient size (${data.width}px by ${data.height}px, should be at least ${data.minSize}px by ${data.minSize}px)", - "obscured": "Control is ignored because it is fully obscured and thus not clickable" + "obscured": "Control is ignored because it is fully obscured and thus not clickable", + "large": "Target far exceeds the minimum size of ${data.minSize}px." }, "fail": { "default": "Target has insufficient size (${data.width}px by ${data.height}px, should be at least ${data.minSize}px by ${data.minSize}px)", diff --git a/locales/_template.json b/locales/_template.json index ef56f9abdf..a911c60144 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -865,7 +865,10 @@ "fail": "${data} on tag disables zooming on mobile devices" }, "target-offset": { - "pass": "Target has sufficient space from its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px which is at least ${data.minOffset}px.", + "pass": { + "default": "Target has sufficient space from its closest neighbors. Safe clickable space has a diameter of ${data.closestOffset}px which is at least ${data.minOffset}px.", + "large": "Target far exceeds the minimum size of ${data.minOffset}px." + }, "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.", "incomplete": { "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,7 +878,8 @@ "target-size": { "pass": { "default": "Control has sufficient size (${data.width}px by ${data.height}px, should be at least ${data.minSize}px by ${data.minSize}px)", - "obscured": "Control is ignored because it is fully obscured and thus not clickable" + "obscured": "Control is ignored because it is fully obscured and thus not clickable", + "large": "Target far exceeds the minimum size of ${data.minSize}px." }, "fail": { "default": "Target has insufficient size (${data.width}px by ${data.height}px, should be at least ${data.minSize}px by ${data.minSize}px)", diff --git a/test/checks/mobile/target-offset.js b/test/checks/mobile/target-offset.js index 47ff219834..367968f448 100644 --- a/test/checks/mobile/target-offset.js +++ b/test/checks/mobile/target-offset.js @@ -125,21 +125,15 @@ describe('target-offset tests', () => { for (let i = 0; i < 100; i++) { html += ` - Hello - Hello - Hello - Hello - Hello - Hello - Hello - - - + A + + + `; } const checkArgs = checkSetup(` -
+
${html}
`); @@ -196,5 +190,22 @@ describe('target-offset tests', () => { }); assert.deepEqual(relatedIds, ['#left', '#right']); }); + + it('returns true if the target is 10x the minOffset', () => { + const checkArgs = checkSetup( + 'x' + + 'x' + + 'x' + ); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); + assert.equal(checkContext._data.minOffset, 24); + assert.equal(checkContext._data.messageKey, 'large'); + }); }); }); diff --git a/test/checks/mobile/target-size.js b/test/checks/mobile/target-size.js index 6d34482246..688f809753 100644 --- a/test/checks/mobile/target-size.js +++ b/test/checks/mobile/target-size.js @@ -58,6 +58,16 @@ describe('target-size tests', function () { }); }); + it('returns true for very large targets', function () { + var checkArgs = checkSetup( + '' + ); + assert.isTrue(check.evaluate.apply(checkContext, checkArgs)); + assert.deepEqual(checkContext._data, { messageKey: 'large', minSize: 24 }); + }); + describe('when fully obscured', function () { it('returns true, regardless of size', function () { var checkArgs = checkSetup( @@ -172,21 +182,15 @@ describe('target-size tests', function () { for (let i = 0; i < 100; i++) { html += ` - Hello - Hello - Hello - Hello - Hello - Hello - Hello - - - + A + + + `; } const checkArgs = checkSetup(` -
+
${html}
`); diff --git a/test/integration/full/target-size/too-many-rects.html b/test/integration/full/target-size/too-many-rects.html index 4ce85fd174..0a90cd4782 100644 --- a/test/integration/full/target-size/too-many-rects.html +++ b/test/integration/full/target-size/too-many-rects.html @@ -22,7 +22,12 @@
-
+
@@ -32,16 +37,10 @@ for (let i = 0; i < 100; i++) { html += ` - Hello - Hello - Hello - Hello - Hello - Hello - Hello - - - + A + + + `; } document.querySelector('#tab-table').innerHTML = html;