From 6a8f87275b5d32c5f42cca5d91d3855e33b5cffc Mon Sep 17 00:00:00 2001 From: Kyle McNutt Date: Wed, 4 Dec 2019 14:18:44 -0800 Subject: [PATCH] feat(rule): add color-contrast check for unicode characters. Adding two new config flags to color-contrast so it can check unicode character based icons. ignoreUnicode, defaults to true and retains the behavior of ignoring all unicode characters when doing color contrast. This can be turned off to start checking unicode characters for color contrast. ignoreLength, defaults to false and retains the behavior that single character nodes do not contain enough information to say whether or not they have color contrast issues. This can be turned on to ignore this length check and always check if a node has color contrast issues. Fixes issues described in #1906. --- lib/checks/color/color-contrast.js | 20 ++++++-- lib/checks/color/color-contrast.json | 5 ++ lib/rules/color-contrast-matches.js | 2 +- lib/rules/color-contrast.json | 4 +- test/checks/color/color-contrast.js | 57 ++++++++++++++++++++- test/rule-matches/color-contrast-matches.js | 4 +- 6 files changed, 83 insertions(+), 9 deletions(-) diff --git a/lib/checks/color/color-contrast.js b/lib/checks/color/color-contrast.js index 0bcf0abc9e..22c90bcaa4 100644 --- a/lib/checks/color/color-contrast.js +++ b/lib/checks/color/color-contrast.js @@ -4,6 +4,18 @@ if (!dom.isVisible(node, false)) { return true; } +const visibleText = text.visibleVirtual(virtualNode, false, true); +const ignoreUnicode = !!(options || {}).ignoreUnicode; +const textContainsOnlyUnicode = !text.removeUnicode(visibleText, { + emoji: false, + nonBmp: true, + punctuations: false +}).length; + +if (textContainsOnlyUnicode && ignoreUnicode) { + return true; +} + const noScroll = !!(options || {}).noScroll; const bgNodes = []; const bgColor = color.getBackgroundColor(node, bgNodes, noScroll); @@ -28,11 +40,11 @@ if (bgColor === null) { } const equalRatio = truncatedResult === 1; -const shortTextContent = - text.visibleVirtual(virtualNode, false, true).length === 1; +const shortTextContent = visibleText.length === 1; +const ignoreLength = !!(options || {}).ignoreLength; if (equalRatio) { missing = color.incompleteData.set('bgColor', 'equalRatio'); -} else if (shortTextContent) { +} else if (shortTextContent && !ignoreLength) { // Check that the text content is a single character long missing = 'shortTextContent'; } @@ -55,7 +67,7 @@ if ( fgColor === null || bgColor === null || equalRatio || - (shortTextContent && !cr.isValid) + (shortTextContent && !ignoreLength && !cr.isValid) ) { missing = null; color.incompleteData.clear(); diff --git a/lib/checks/color/color-contrast.json b/lib/checks/color/color-contrast.json index de0635158e..cb3c3f765c 100644 --- a/lib/checks/color/color-contrast.json +++ b/lib/checks/color/color-contrast.json @@ -3,6 +3,11 @@ "evaluate": "color-contrast.js", "metadata": { "impact": "serious", + "options": { + "noScroll": false, + "ignoreUnicode": true, + "ignoreLength": false + }, "messages": { "pass": "Element has sufficient color contrast of ${data.contrastRatio}", "fail": "Element has insufficient color contrast of ${data.contrastRatio} (foreground color: ${data.fgColor}, background color: ${data.bgColor}, font size: ${data.fontSize}, font weight: ${data.fontWeight}). Expected contrast ratio of ${data.expectedContrastRatio}", diff --git a/lib/rules/color-contrast-matches.js b/lib/rules/color-contrast-matches.js index c38e44fe48..9b3bf4e0b1 100644 --- a/lib/rules/color-contrast-matches.js +++ b/lib/rules/color-contrast-matches.js @@ -98,7 +98,7 @@ if ( visibleText === '' || axe.commons.text.removeUnicode(visibleText, { emoji: true, - nonBmp: true, + nonBmp: false, punctuations: true }) === '' ) { diff --git a/lib/rules/color-contrast.json b/lib/rules/color-contrast.json index f09e6f4672..6213e0abee 100644 --- a/lib/rules/color-contrast.json +++ b/lib/rules/color-contrast.json @@ -3,7 +3,9 @@ "matches": "color-contrast-matches.js", "excludeHidden": false, "options": { - "noScroll": false + "noScroll": false, + "ignoreUnicode": true, + "ignoreLength": false }, "tags": ["cat.color", "wcag2aa", "wcag143"], "metadata": { diff --git a/test/checks/color/color-contrast.js b/test/checks/color/color-contrast.js index d7426d2b74..36eaa09b27 100644 --- a/test/checks/color/color-contrast.js +++ b/test/checks/color/color-contrast.js @@ -324,7 +324,7 @@ describe('color-contrast', function() { assert.equal(checkContext._data.messageKey, 'shortTextContent'); }); - it('should return true for a single character text with insufficient contrast', function() { + it('should return true for a single character text with sufficient contrast', function() { var params = checkSetup( '
' + '
X
' + @@ -335,6 +335,61 @@ describe('color-contrast', function() { assert.isTrue(actual); }); + it('should return true when the text only contains nonBmp unicode by default', function() { + var params = checkSetup( + '
' + + '
' + + '
' + ); + + var actual = contrastEvaluate.apply(checkContext, params); + assert.isTrue(actual); + }); + + it('should return true when the text only contains nonBmp unicode when the ignoreUnicode option is false, and there is sufficient contrast', function() { + var params = checkSetup( + '
' + + '
' + + '
', + { + ignoreUnicode: false + } + ); + + var actual = contrastEvaluate.apply(checkContext, params); + assert.isTrue(actual); + }); + + it('should return undefined when the text only contains nonBmp unicode when the ignoreUnicode option is false and the ignoreLength option is default, and there is insufficient contrast', function() { + var params = checkSetup( + '
' + + '
' + + '
', + { + ignoreUnicode: false + } + ); + + var actual = contrastEvaluate.apply(checkContext, params); + assert.isUndefined(actual); + assert.equal(checkContext._data.missingData, 'shortTextContent'); + }); + + it('should return false when the text only contains nonBmp unicode when the ignoreUnicode option is false and the ignoreLength option is true, and there is insufficient contrast', function() { + var params = checkSetup( + '
' + + '
' + + '
', + { + ignoreUnicode: false, + ignoreLength: true + } + ); + + var actual = contrastEvaluate.apply(checkContext, params); + assert.isFalse(actual); + }); + (shadowSupported ? it : xit)( 'returns colors across Shadow DOM boundaries', function() { diff --git a/test/rule-matches/color-contrast-matches.js b/test/rule-matches/color-contrast-matches.js index c0763e0485..b00bb8a361 100644 --- a/test/rule-matches/color-contrast-matches.js +++ b/test/rule-matches/color-contrast-matches.js @@ -57,13 +57,13 @@ describe('color-contrast-matches', function() { assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(target))); }); - it('should not match when text only contains nonBmp unicode', function() { + it('should match when text only contains nonBmp unicode', function() { fixture.innerHTML = '
' + '◓
'; var target = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); - assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(target))); + assert.isTrue(rule.matches(target, axe.utils.getNodeFromTree(target))); }); it('should not match when there is text that is out of the container', function() {