Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
84 changes: 68 additions & 16 deletions lib/checks/label/label-content-name-mismatch-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,72 @@ import {
} from '../../commons/text';

/**
* Check if a given text exists in another
* Check whether the visible label's words appear as a contiguous run of words
* within the accessible name. This implements the comparison at the core of ACT
* rule 2ee8b8's "label in name" algorithm: tokenize on non-text characters, then
* match whole words rather than raw substrings.
*
* @param {String} compare given text to check
* @param {String} compareWith text against which to be compared
* Note: 2ee8b8's parenthetical-content removal and NFKD normalization steps are
* not implemented (see https://github.com/dequelabs/axe-core/issues/5207).
*
* @param {String} label visible label text
* @param {String} name accessible name
* @returns {Boolean}
*/
function isStringContained(compare, compareWith) {
const curatedCompareWith = curateString(compareWith);
const curatedCompare = curateString(compare);
if (!curatedCompareWith || !curatedCompare) {
function isLabelContainedInName(label, name) {
const labelTokens = curateTokens(label);
const nameTokens = curateTokens(name);
if (!labelTokens.length || !nameTokens.length) {
return false;
}
return curatedCompareWith.includes(curatedCompare);
return isContiguousSubsequence(nameTokens, labelTokens);
}

/**
* Curate given text, by removing emoji's, punctuations, unicode and trim whitespace.
* Split text into words, treating non-text characters (emoji, punctuation,
* symbols) as separators by replacing them with a space. Uses `removeUnicode`'s
* explicit unicode ranges (rather than a `\p{…}` property escape) to keep
* working on the browsers axe supports.
*
* @param {String} str given text to curate
* @returns {String}
* @param {String} str given text to tokenize
* @returns {String[]}
*/
function curateString(str) {
const noUnicodeStr = removeUnicode(str, {
function curateTokens(str) {
const separated = removeUnicode(str, {
emoji: true,
nonBmp: true,
punctuations: true
punctuations: true,
replaceWith: ' '
});
return sanitize(noUnicodeStr);
return sanitize(separated).split(/\s+/).filter(Boolean);
}
Comment thread
chutchins25 marked this conversation as resolved.
Outdated

/**
* Whether `needle` appears as a contiguous run within `haystack`.
*
* @param {String[]} haystack
* @param {String[]} needle
* @returns {Boolean}
*/
function isContiguousSubsequence(haystack, needle) {
for (let i = 0; i + needle.length <= haystack.length; i++) {
if (needle.every((word, j) => word === haystack[i + j])) {
return true;
}
}
return false;
}

/**
* Remove hyphens so a hyphenated word collapses into a single word (e.g.
* "non-standard" becomes "nonstandard"). Used to detect when the only
* difference between the label and the name is hyphenation.
*
* @param {String} str
* @returns {String}
*/
function removeHyphens(str) {
return str.replace(/[-‐‑]/g, '');
}
Comment thread
chutchins25 marked this conversation as resolved.
Outdated

function labelContentNameMismatchEvaluate(node, options, virtualNode) {
Expand All @@ -59,7 +97,21 @@ function labelContentNameMismatchEvaluate(node, options, virtualNode) {
return undefined;
}

return isStringContained(visibleText, accText);
if (isLabelContainedInName(visibleText, accText)) {
return true;
}

// ACT rule 2ee8b8 treats hyphenation differences as inapplicable. When the
// label is contained in the name once hyphens are removed rather than treated
// as word separators, the only difference is hyphenation, so return undefined
// (needs review) instead of a violation.
Comment thread
chutchins25 marked this conversation as resolved.
Outdated
if (
isLabelContainedInName(removeHyphens(visibleText), removeHyphens(accText))
) {
return undefined;
Comment thread
chutchins25 marked this conversation as resolved.
}

return false;
}

export default labelContentNameMismatchEvaluate;
13 changes: 7 additions & 6 deletions lib/commons/text/remove-unicode.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,23 @@ import { emojiRegexText } from '../../core/imports';
* @property {Boolean} options.emoji remove emoji unicode
* @property {Boolean} options.nonBmp remove nonBmp unicode
* @property {Boolean} options.punctuations remove punctuations unicode
* @property {String} [options.replaceWith=''] string to substitute for each matched character (e.g. a space to preserve word boundaries)
* @returns {String}
*/
function removeUnicode(str, options) {
const { emoji, nonBmp, punctuations } = options;
const { emoji, nonBmp, punctuations, replaceWith = '' } = options;

if (emoji) {
str = str.replace(emojiRegexText(), '');
str = str.replace(emojiRegexText(), replaceWith);
}
if (nonBmp) {
str = str
.replace(getUnicodeNonBmpRegExp(), '')
.replace(getSupplementaryPrivateUseRegExp(), '')
.replace(getCategoryFormatRegExp(), '');
.replace(getUnicodeNonBmpRegExp(), replaceWith)
.replace(getSupplementaryPrivateUseRegExp(), replaceWith)
.replace(getCategoryFormatRegExp(), replaceWith);
}
if (punctuations) {
str = str.replace(getPunctuationRegExp(), '');
str = str.replace(getPunctuationRegExp(), replaceWith);
Comment thread
chutchins25 marked this conversation as resolved.
Outdated
}

return str;
Expand Down
13 changes: 7 additions & 6 deletions test/act-rules/visible-label-in-accessible-name-2ee8b8.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ require('./act-runner.js')({
title: 'Visible label is part of accessible name',
axeRules: ['label-content-name-mismatch'],
skipTests: [
// See: https://github.com/dequelabs/axe-core/issues/4311
'e9bbdbec137223e2973c6d2896050770c84c26e5',
// See: https://github.com/dequelabs/axe-core/issues/5207
'fab659b02c1edb4f2c8f0bda524b1076abab7df6',
'94a7ce7aea9dbfaa375c459c26d3a5923de84e7a',
'e117393d6711d6bdf32821005219c9d9474dfeb8',
'f5c9811c984987443476760a1c5b91b1067f7e19'
'fab659b02c1edb4f2c8f0bda524b1076abab7df6', // Passed Example 11
'94a7ce7aea9dbfaa375c459c26d3a5923de84e7a', // Passed Example 14
'e117393d6711d6bdf32821005219c9d9474dfeb8', // Failed Example 3
'f5c9811c984987443476760a1c5b91b1067f7e19', // Failed Example 15
// Abbreviation differences are inapplicable under ACT rule 2ee8b8 but cannot
// be reliably detected. See: https://github.com/dequelabs/axe-core/issues/4821
'4c8c38022d15c92158ecaaa647fe8ca2c330f485' // Inapplicable Example 5
]
});
40 changes: 40 additions & 0 deletions test/checks/label/label-content-name-mismatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ describe('label-content-name-mismatch tests', () => {
assert.isTrue(actual);
});

it('treats a lone newline in the visible text as a word separator', () => {
const vNode = queryFixture(
'<button id="target" aria-label="save changes">save\nchanges</button>'
);
const actual = check.evaluate(vNode.actualNode, options, vNode);
assert.isTrue(actual);
});

it('returns true when aria-label and visible text match even though there is an image with alt text', function () {
var vNode = queryFixture(
'<button id="target" aria-label="button label"><img alt="button icon" src="button.png" />button label</button>'
Expand Down Expand Up @@ -224,4 +232,36 @@ describe('label-content-name-mismatch tests', () => {
assert.isFalse(actual);
}
);

it('returns false when the visible words are not a contiguous run within the accessible name', () => {
const vNode = queryFixture(
'<button id="target" aria-label="the big red button">big button</button>'
);
const actual = check.evaluate(vNode.actualNode, options, vNode);
assert.isFalse(actual);
});

it('returns true when the visible words are a contiguous run within the accessible name', () => {
const vNode = queryFixture(
'<button id="target" aria-label="go to next page now">next page</button>'
);
const actual = check.evaluate(vNode.actualNode, options, vNode);
assert.isTrue(actual);
});

it('returns undefined (needs review) when the only difference is hyphenation', () => {
const vNode = queryFixture(
'<a id="target" href="#" aria-label="non-standard">nonstandard</a>'
);
const actual = check.evaluate(vNode.actualNode, options, vNode);
assert.isUndefined(actual);
});

it('returns undefined (needs review) when a hyphenated visible word matches the accessible name', () => {
const vNode = queryFixture(
'<a id="target" href="#" aria-label="email">e-mail</a>'
);
const actual = check.evaluate(vNode.actualNode, options, vNode);
assert.isUndefined(actual);
});
});
24 changes: 24 additions & 0 deletions test/commons/text/unicode.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,28 @@ describe('text.removeUnicode', () => {
});
assert.equal(actual, 'Hello World');
});

it('substitutes matched punctuation with replaceWith when provided', () => {
const actual = axe.commons.text.removeUnicode('non-standard', {
punctuations: true,
replaceWith: ' '
});
assert.equal(actual, 'non standard');
});

it('substitutes matched emoji with replaceWith when provided', () => {
const actual = axe.commons.text.removeUnicode('Sun🌎Earth', {
emoji: true,
replaceWith: ' '
});
assert.equal(actual, 'Sun Earth');
});

it('substitutes matched non BMP characters with replaceWith when provided', () => {
const actual = axe.commons.text.removeUnicode('20000₨100', {
nonBmp: true,
replaceWith: ' '
});
assert.equal(actual, '20000 100');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
<button id="incomplete16" aria-label="toggle capitalization">aA</button>
<button id="incomplete17" aria-label="CJK character"></button>

<a id="incomplete18" href="#" aria-label="non-standard">nonstandard</a>

<!-- inapplicable -->
<a id="inapplicable1" aria-label="OK">Next</a>
<input id="inapplicable2" type="email" aria-label="E-mail" value="Contact" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
["#incomplete14"],
["#incomplete15"],
["#incomplete16"],
["#incomplete17"]
["#incomplete17"],
["#incomplete18"]
]
}
Loading