Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rule): Label and Name from Content mismatch WCAG21 (Issue #1149) #1335

Merged
merged 44 commits into from
Feb 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8c80f14
chore(WIP): rewrite accessibleText
WilcoFiers Sep 20, 2018
82e5c74
Merge branch 'develop' into a11y-name
WilcoFiers Sep 26, 2018
9d2451e
chore: More refactoring for accname
WilcoFiers Sep 26, 2018
305c864
chore(WIP): More improvements to accessibleName
WilcoFiers Oct 10, 2018
8501f34
feat: Reimplement accessible name computation
WilcoFiers Oct 10, 2018
dec7220
chore: All accessible name tests passing
WilcoFiers Oct 13, 2018
abb0673
chore(accName): All tests passing
WilcoFiers Oct 13, 2018
4de5489
chore: Add tests
WilcoFiers Nov 9, 2018
a98448b
chore: Test form-control-value
WilcoFiers Nov 23, 2018
e264958
chore: Merge develop
WilcoFiers Nov 23, 2018
1aded4a
chore: Refactor and add docs to accessible-text
WilcoFiers Nov 24, 2018
6e67b52
chore: Add tests for namedFromContents
WilcoFiers Nov 25, 2018
2a5020e
chore: Refactor subtreeText method
WilcoFiers Nov 25, 2018
6ff002b
chore: Refactor native accessible text methods
WilcoFiers Nov 25, 2018
4c6c351
chore: Coverage for text.labelText
WilcoFiers Dec 3, 2018
6917ec9
Merge branch 'develop' into a11y-name
WilcoFiers Jan 7, 2019
73ded40
Merge branch 'develop' into a11y-name
jeeyyy Jan 23, 2019
2244ed2
fix: update to axe.commons.matches usage
jeeyyy Jan 23, 2019
6c35b51
Merge branch 'develop' into a11y-name
jeeyyy Jan 23, 2019
9bddd36
test: fix nativeTextboxValue tests
jeeyyy Jan 23, 2019
3f9a969
test: fix failing tests
jeeyyy Jan 25, 2019
1227102
chore: merge from develop
jeeyyy Jan 25, 2019
dfa1bd7
fix: compute includeHidden as a part of accessibleName fn
jeeyyy Jan 25, 2019
d0f45e7
fix: do not mutate context in accessibleText
jeeyyy Jan 25, 2019
c3a1bdc
Merge branch 'develop' into a11y-name
jeeyyy Jan 25, 2019
ef1c638
feat: rule for label and name from content mismatch
jeeyyy Jan 28, 2019
14b09b6
Merge branch 'develop' into new-rule-lcnm
jeeyyy Jan 28, 2019
7412de8
Merge branch 'develop' into new-rule-lcnm
jeeyyy Feb 1, 2019
d615dbe
fix: refactor based on review and add unicode computation
jeeyyy Feb 6, 2019
ad9aa2d
Merge branch 'develop' into new-rule-lcnm
jeeyyy Feb 6, 2019
ec2af57
Merge branch 'develop' into new-rule-lcnm
jeeyyy Feb 12, 2019
b21e631
Merge branch 'develop' into new-rule-lcnm
jeeyyy Feb 12, 2019
3945006
refactor: update based on code review
jeeyyy Feb 12, 2019
2b07290
test: update test
jeeyyy Feb 12, 2019
6696c5d
Merge branch 'develop' into new-rule-lcnm
jeeyyy Feb 18, 2019
9afed3f
Merge branch 'develop' into new-rule-lcnm
jeeyyy Feb 18, 2019
b18226f
chore: fix linting errors
jeeyyy Feb 18, 2019
866fe88
Merge branch 'develop' into new-rule-lcnm
jeeyyy Feb 19, 2019
5fefd54
refactor: updates based on code review
jeeyyy Feb 19, 2019
56e4680
merge from develop
jeeyyy Feb 19, 2019
fd003e7
Merge branch 'develop' into new-rule-lcnm
jeeyyy Feb 20, 2019
072711d
refactor: unicode and punctuation methods
jeeyyy Feb 20, 2019
eabb7dd
Merge branch 'develop' into new-rule-lcnm
jeeyyy Feb 21, 2019
92489be
test: update tests
jeeyyy Feb 21, 2019
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
1 change: 1 addition & 0 deletions doc/rule-descriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
| image-alt | Ensures <img> elements have alternate text or a role of none or presentation | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true |
| image-redundant-alt | Ensure button and link text is not repeated as image alternative | Minor | cat.text-alternatives, best-practice | true |
| input-image-alt | Ensures <input type="image"> elements have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true |
| label-content-name-mismatch | Ensures that elements labelled through their content must have their visible text as part of their accessible name | Serious | wcag21a, wcag253, experimental | true |
| label-title-only | Ensures that every form element is not solely labeled using the title or aria-describedby attributes | Serious | cat.forms, best-practice | true |
| label | Ensures every form element has a label | Minor, Critical | cat.forms, wcag2a, wcag332, wcag131, section508, section508.22.n | true |
| landmark-banner-is-top-level | Ensures the banner landmark is at top level | Moderate | cat.semantics, best-practice | true |
Expand Down
49 changes: 49 additions & 0 deletions lib/checks/label/label-content-name-mismatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { text } = axe.commons;

const accText = text.accessibleText(node).toLowerCase();
if (text.isHumanInterpretable(accText) < 1) {
return undefined;
}

const visibleText = text
.sanitize(text.visibleVirtual(virtualNode))
.toLowerCase();
if (text.isHumanInterpretable(visibleText) < 1) {
if (isStringContained(visibleText, accText)) {
return true;
}
return undefined;
}

return isStringContained(visibleText, accText);

/**
* Check if a given text exists in another
*
* @param {String} compare given text to check
* @param {String} compareWith text against which to be compared
* @returns {Boolean}
*/
function isStringContained(compare, compareWith) {
const curatedCompareWith = curateString(compareWith);
const curatedCompare = curateString(compare);
if (!curatedCompareWith || !curatedCompare) {
return false;
}
return curatedCompareWith.includes(curatedCompare);
}

/**
* Curate given text, by removing emoji's, punctuations, unicode and trim whitespace.
*
* @param {String} str given text to curate
* @returns {String}
*/
function curateString(str) {
const noUnicodeStr = text.removeUnicode(str, {
emoji: true,
nonBmp: true,
punctuations: true
});
return text.sanitize(noUnicodeStr);
}
11 changes: 11 additions & 0 deletions lib/checks/label/label-content-name-mismatch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "label-content-name-mismatch",
"evaluate": "label-content-name-mismatch.js",
"metadata": {
"impact": "serious",
"messages": {
"pass": "Element contains visible text as part of it's accessible name",
"fail": "Text inside the element is not included in the accessible name"
}
}
}
42 changes: 42 additions & 0 deletions lib/commons/aria/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2357,3 +2357,45 @@ lookupTable.evaluateRoleForElement = {
return out;
}
};

/**
* Reference -> https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#Widget_roles
* The current lookupTable.role['widget'] widget, yeilds
* ->
* [
* "alert", "alertdialog", "button", "checkbox", "dialog", "gridcell", "link", "log", "marquee", "menuitem", "menuitemcheckbox",
* "menuitemradio", "option", "progressbar", "radio", "scrollbar", "searchbox", "slider", "spinbutton", "status", "switch", "tab", "tabpanel",
* "textbox", "timer", "tooltip", "treeitem"
* ]
* There are some differences against specs, hence the below listing was made
*/
lookupTable.rolesOfType = {
widget: [
'button',
'checkbox',
'dialog',
'gridcell',
'heading',
'link',
'log',
'marquee',
'menuitem',
'menuitemcheckbox',
'menuitemradio',
'option',
'progressbar',
'radio',
'scrollbar',
'slider',
'spinbutton',
'status',
'switch',
'tab',
'tabpanel',
'textbox',
'timer',
'tooltip',
'tree',
'treeitem'
]
};
51 changes: 51 additions & 0 deletions lib/commons/text/is-human-interpretable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* global text */

/**
* Determines if a given text is human friendly and interpretable
*
* @method isHumanInterpretable
* @memberof axe.commons.text
* @instance
* @param {String} str text to be validated
* @returns {Number} Between 0 and 1, (0 -> not interpretable, 1 -> interpretable)
*/
text.isHumanInterpretable = function(str) {
/**
* Steps:
* 1) Check for single character edge cases
* a) handle if character is alphanumeric & within the given icon mapping
* eg: x (close), i (info)
*
* 3) handle unicode from astral (non bilingual multi plane) unicode, emoji & punctuations
* eg: Windings font
* eg: '💪'
* eg: I saw a shooting 💫
* eg: ? (help), > (next arrow), < (back arrow), need help ?
*/

if (!str.length) {
return 0;
}

// Step 1
const alphaNumericIconMap = [
'x', // close
'i' // info
];
// Step 1a
if (alphaNumericIconMap.includes(str)) {
return 0;
}

// Step 2
const noUnicodeStr = text.removeUnicode(str, {
emoji: true,
nonBmp: true,
punctuations: true
});
if (!text.sanitize(noUnicodeStr)) {
return 0;
}

return 1;
};
117 changes: 117 additions & 0 deletions lib/commons/text/unicode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* global text */

/**
* Determine if a given string contains unicode characters, specified in options
*
* @method hasUnicode
* @memberof axe.commons.text
* @instance
* @param {String} str string to verify
* @param {Object} options config containing which unicode character sets to verify
* @property {Boolean} options.emoji verify emoji unicode
* @property {Boolean} options.nonBmp verify nonBmp unicode
* @property {Boolean} options.punctuations verify punctuations unicode
* @returns {Boolean}
*/
text.hasUnicode = function hasUnicode(str, options) {
const { emoji, nonBmp, punctuations } = options;
if (emoji) {
return axe.imports.emojiRegexText().test(str);
}
if (nonBmp) {
return getUnicodeNonBmpRegExp().test(str);
}
if (punctuations) {
return getPunctuationRegExp().test(str);
}
return false;
};

/**
* Remove specified type(s) unicode characters
*
* @method removeUnicode
* @memberof axe.commons.text
* @instance
* @param {String} str string to operate on
* @param {Object} options config containing which unicode character sets to remove
* @property {Boolean} options.emoji remove emoji unicode
* @property {Boolean} options.nonBmp remove nonBmp unicode
* @property {Boolean} options.punctuations remove punctuations unicode
* @returns {String}
*/
text.removeUnicode = function removeUnicode(str, options) {
const { emoji, nonBmp, punctuations } = options;

if (emoji) {
str = str.replace(axe.imports.emojiRegexText(), '');
}
if (nonBmp) {
str = str.replace(getUnicodeNonBmpRegExp(), '');
}
if (punctuations) {
str = str.replace(getPunctuationRegExp(), '');
}

return str;
};

/**
* Regex for matching unicode values out of Basic Multilingual Plane (BMP)
* Reference:
* - https://github.com/mathiasbynens/regenerate
* - https://unicode-table.com/
* - https://mathiasbynens.be/notes/javascript-unicode
*
* @returns {RegExp}
*/
function getUnicodeNonBmpRegExp() {
/**
* Regex for matching astral plane unicode
* - http://kourge.net/projects/regexp-unicode-block
*/
return new RegExp(
'[' +
'\u1D00-\u1D7F' + // Phonetic Extensions
'\u1D80-\u1DBF' + // Phonetic Extensions Supplement
'\u1DC0-\u1DFF' + // Combining Diacritical Marks Supplement
// '\u2000-\u206F' + // General punctuation - handled in -> getPunctuationRegExp
'\u20A0-\u20CF' + // Currency symbols
'\u20D0-\u20FF' + // Combining Diacritical Marks for Symbols
'\u2100-\u214F' + // Letter like symbols
'\u2150-\u218F' + // Number forms (eg: Roman numbers)
'\u2190-\u21FF' + // Arrows
'\u2200-\u22FF' + // Mathematical operators
'\u2300-\u23FF' + // Misc Technical
'\u2400-\u243F' + // Control pictures
'\u2440-\u245F' + // OCR
'\u2460-\u24FF' + // Enclosed alpha numerics
'\u2500-\u257F' + // Box Drawing
'\u2580-\u259F' + // Block Elements
'\u25A0-\u25FF' + // Geometric Shapes
'\u2600-\u26FF' + // Misc Symbols
'\u2700-\u27BF' + // Dingbats
']'
);
}

/**
* Get regular expression for matching punctuations
*
* @returns {RegExp}
*/
function getPunctuationRegExp() {
/**
* Reference: http://kunststube.net/encoding/
* US-ASCII
* -> !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
*
* General Punctuation block
* -> \u2000-\u206F
*
* Supplemental Punctuation block
* Reference: https://en.wikipedia.org/wiki/Supplemental_Punctuation
* -> \u2E00-\u2E7F Reference
*/
return /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]/g;
}
3 changes: 2 additions & 1 deletion lib/core/imports/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require('es6-promise').polyfill();
*/
axe.imports = {
axios: require('axios'),
CssSelectorParser: require('css-selector-parser').CssSelectorParser,
doT: require('dot'),
CssSelectorParser: require('css-selector-parser').CssSelectorParser
emojiRegexText: require('emoji-regex')
};
42 changes: 42 additions & 0 deletions lib/rules/label-content-name-mismatch-matches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Applicability:
* Rule applies to any element that has
* a) a semantic role that is `widget` that supports name from content
* b) has visible text content
* c) has accessible name (eg: `aria-label`)
*/
const { aria, text } = axe.commons;

const role = aria.getRole(node);
if (!role) {
return false;
}

const isWidgetType = aria.lookupTable.rolesOfType.widget.includes(role);
if (!isWidgetType) {
return false;
}

const rolesWithNameFromContents = aria.getRolesWithNameFromContents();
if (!rolesWithNameFromContents.includes(role)) {
return false;
}

/**
* if no `aria-label` or `aria-labelledby` attribute - ignore `node`
*/
if (
!text.sanitize(aria.arialabelText(node)) &&
!text.sanitize(aria.arialabelledbyText(node))
) {
return false;
}

/**
* if no `contentText` - ignore `node`
*/
if (!text.sanitize(text.visibleVirtual(virtualNode))) {
return false;
}

return true;
12 changes: 12 additions & 0 deletions lib/rules/label-content-name-mismatch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "label-content-name-mismatch",
"matches": "label-content-name-mismatch-matches.js",
"tags": ["wcag21a", "wcag253", "experimental"],
"metadata": {
"description": "Ensures that elements labelled through their content must have their visible text as part of their accessible name",
"help": "Elements must have their visible text as part of their accessible name"
},
"all": [],
"any": ["label-content-name-mismatch"],
"none": []
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"clone": "~2.1.1",
"css-selector-parser": "^1.3.0",
"dot": "~1.1.2",
"emoji-regex": "7.0.3",
"es6-promise": "^4.2.6",
"eslint": "^5.14.0",
"eslint-config-prettier": "^3.4.0",
Expand Down
Loading