eslint-plugin-jsx-a11y
Advanced tools
Comparing version 1.4.0 to 1.4.1
@@ -0,5 +1,9 @@ | ||
1.4.1 / 2016-06-10 | ||
================== | ||
- [fix] Handle spread props in `aria-unsupported-elements` and `role-supports-aria-props` when reporting. | ||
1.4.0 / 2016-06-10 | ||
================== | ||
- [dependency] Integrate `jsx-ast-utils` (extracted JSX core module) | ||
1. [jsx-ast-utils](https://github.com/evcohen/jsx-ast-utils) | ||
- [dependency] Integrate [jsx-ast-utils](https://github.com/evcohen/jsx-ast-utils) | ||
- [fix] Better error reporting for aria-unsupported-elements indicating which prop to remove. | ||
@@ -6,0 +10,0 @@ |
'use strict'; | ||
/* eslint-disable global-require */ | ||
module.exports = { | ||
@@ -4,0 +6,0 @@ rules: { |
@@ -1,11 +0,3 @@ | ||
/** | ||
* @fileoverview Enforce all aria-* properties are valid. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var _ARIA = require('../util/attributes/ARIA'); | ||
@@ -21,2 +13,11 @@ | ||
/** | ||
* @fileoverview Enforce all aria-* properties are valid. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var errorMessage = function errorMessage(name) { | ||
@@ -23,0 +24,0 @@ var dictionary = Object.keys(_ARIA2.default).map(function (aria) { |
@@ -1,11 +0,3 @@ | ||
/** | ||
* @fileoverview Enforce ARIA state and property values are valid. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var _ARIA = require('../util/attributes/ARIA'); | ||
@@ -19,2 +11,11 @@ | ||
/** | ||
* @fileoverview Enforce ARIA state and property values are valid. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var errorMessage = function errorMessage(name, type, permittedValues) { | ||
@@ -44,3 +45,3 @@ switch (type) { | ||
case 'tristate': | ||
return typeof value === 'boolean' || value == 'mixed'; | ||
return typeof value === 'boolean' || value === 'mixed'; | ||
case 'integer': | ||
@@ -47,0 +48,0 @@ case 'number': |
@@ -1,11 +0,3 @@ | ||
/** | ||
* @fileoverview Enforce aria role attribute is valid. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var _role = require('../util/attributes/role'); | ||
@@ -19,2 +11,11 @@ | ||
/** | ||
* @fileoverview Enforce aria role attribute is valid. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var errorMessage = 'Elements with ARIA roles must use a valid, non-abstract ARIA role.'; | ||
@@ -33,3 +34,4 @@ | ||
// If value is undefined, then the role attribute will be dropped in the DOM. | ||
// If value is null, then getLiteralAttributeValue is telling us that the value isn't in the form of a literal. | ||
// If value is null, then getLiteralAttributeValue is telling us that the | ||
// value isn't in the form of a literal. | ||
if (value === undefined || value === null) { | ||
@@ -43,4 +45,4 @@ return; | ||
}); | ||
var isValid = normalizedValues.every(function (value) { | ||
return validRoles.indexOf(value) > -1; | ||
var isValid = normalizedValues.every(function (val) { | ||
return validRoles.indexOf(val) > -1; | ||
}); | ||
@@ -47,0 +49,0 @@ |
@@ -1,11 +0,3 @@ | ||
/** | ||
* @fileoverview Enforce that elements that do not support ARIA roles, states and properties do not have those attributes. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var _DOM = require('../util/attributes/DOM'); | ||
@@ -25,4 +17,12 @@ | ||
return 'This element does not support ARIA roles, states and properties. Try removing the prop \'' + invalidProp + '\'.'; | ||
}; | ||
}; /** | ||
* @fileoverview Enforce that elements that do not support ARIA roles, | ||
* states and properties do not have those attributes. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
module.exports = function (context) { | ||
@@ -43,2 +43,6 @@ return { | ||
node.attributes.forEach(function (prop) { | ||
if (prop.type === 'JSXSpreadAttribute') { | ||
return; | ||
} | ||
if (invalidAttributes.indexOf(prop.name.name.toUpperCase()) > -1) { | ||
@@ -45,0 +49,0 @@ context.report({ |
@@ -1,7 +0,10 @@ | ||
/** | ||
* @fileoverview Enforce links may not point to just #. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var errorMessage = 'Links must not point to "#". ' + 'Use a more descriptive href or use a button instead.'; /** | ||
* @fileoverview Enforce links may not point to just #. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
@@ -11,6 +14,2 @@ // Rule Definition | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var errorMessage = 'Links must not point to "#". Use a more descriptive href or use a button instead.'; | ||
module.exports = function (context) { | ||
@@ -41,10 +40,10 @@ return { | ||
module.exports.schema = [{ | ||
'oneOf': [{ 'type': 'string' }, { | ||
'type': 'array', | ||
'items': { | ||
'type': 'string' | ||
oneOf: [{ type: 'string' }, { | ||
type: 'array', | ||
items: { | ||
type: 'string' | ||
}, | ||
'minItems': 1, | ||
'uniqueItems': true | ||
minItems: 1, | ||
uniqueItems: true | ||
}] | ||
}]; |
@@ -1,11 +0,3 @@ | ||
/** | ||
* @fileoverview Enforce img tag uses alt attribute. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
@@ -58,13 +50,20 @@ | ||
}; | ||
}; | ||
}; /** | ||
* @fileoverview Enforce img tag uses alt attribute. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
module.exports.schema = [{ | ||
'oneOf': [{ 'type': 'string' }, { | ||
'type': 'array', | ||
'items': { | ||
'type': 'string' | ||
oneOf: [{ type: 'string' }, { | ||
type: 'array', | ||
items: { | ||
type: 'string' | ||
}, | ||
'minItems': 1, | ||
'uniqueItems': true | ||
minItems: 1, | ||
uniqueItems: true | ||
}] | ||
}]; |
@@ -1,11 +0,3 @@ | ||
/** | ||
* @fileoverview Enforce img alt attribute does not have the word image, picture, or photo. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
@@ -19,5 +11,14 @@ | ||
/** | ||
* @fileoverview Enforce img alt attribute does not have the word image, picture, or photo. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var REDUNDANT_WORDS = ['image', 'photo', 'picture']; | ||
var errorMessage = 'Redundant alt attribute. Screen-readers already announce `img` tags as an image. ' + 'You don\'t need to use the words `image`, `photo,` or `picture` in the alt prop.'; | ||
var errorMessage = 'Redundant alt attribute. Screen-readers already announce ' + '`img` tags as an image. You don\'t need to use the words `image`, ' + '`photo,` or `picture` in the alt prop.'; | ||
@@ -24,0 +25,0 @@ var validTypes = ['LITERAL', 'TEMPLATELITERAL']; |
@@ -1,7 +0,10 @@ | ||
/** | ||
* @fileoverview Enforce label tags have htmlFor attribute. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var errorMessage = 'Form controls using a label to identify them must be ' + 'programmatically associated with the control using htmlFor'; /** | ||
* @fileoverview Enforce label tags have htmlFor attribute. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
@@ -11,6 +14,2 @@ // Rule Definition | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var errorMessage = 'Form controls using a label to identify them must be ' + 'programmatically associated with the control using htmlFor'; | ||
module.exports = function (context) { | ||
@@ -42,10 +41,10 @@ return { | ||
module.exports.schema = [{ | ||
'oneOf': [{ 'type': 'string' }, { | ||
'type': 'array', | ||
'items': { | ||
'type': 'string' | ||
oneOf: [{ type: 'string' }, { | ||
type: 'array', | ||
items: { | ||
type: 'string' | ||
}, | ||
'minItems': 1, | ||
'uniqueItems': true | ||
minItems: 1, | ||
uniqueItems: true | ||
}] | ||
}]; |
@@ -1,8 +0,11 @@ | ||
/** | ||
* @fileoverview Enforce onmouseover/onmouseout are | ||
* accompanied by onfocus/onblur. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var mouseOverErrorMessage = 'onMouseOver must be accompanied by onFocus for accessibility.'; /** | ||
* @fileoverview Enforce onmouseover/onmouseout are | ||
* accompanied by onfocus/onblur. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
@@ -12,5 +15,2 @@ // Rule Definition | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var mouseOverErrorMessage = 'onMouseOver must be accompanied by onFocus for accessibility.'; | ||
var mouseOutErrorMessage = 'onMouseOut must be accompanied by onBlur for accessibility.'; | ||
@@ -17,0 +17,0 @@ |
@@ -1,7 +0,10 @@ | ||
/** | ||
* @fileoverview Enforce no accesskey attribute on element. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var errorMessage = 'No access key attribute allowed. Inconsistencies ' + 'between keyboard shortcuts and keyboard comments used by screenreader ' + 'and keyboard only users create a11y complications.'; /** | ||
* @fileoverview Enforce no accesskey attribute on element. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
@@ -11,6 +14,2 @@ // Rule Definition | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var errorMessage = 'No access key attribute allowed. Inconsistencies ' + 'between keyboard shortcuts and keyboard comments used by screenreader ' + 'and keyboard only users create a11y complications.'; | ||
module.exports = function (context) { | ||
@@ -17,0 +16,0 @@ return { |
@@ -1,7 +0,10 @@ | ||
/** | ||
* @fileoverview Enforce usage of onBlur over onChange for accessibility. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var errorMessage = 'onBlur must be used instead of onchange, ' + 'unless absolutely necessary and it causes no negative consequences ' + 'for keyboard only or screen reader users.'; /** | ||
* @fileoverview Enforce usage of onBlur over onChange for accessibility. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
@@ -11,6 +14,2 @@ // Rule Definition | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var errorMessage = 'onBlur must be used instead of onchange, ' + 'unless absolutely necessary and it causes no negative consequences ' + 'for keyboard only or screen reader users.'; | ||
module.exports = function (context) { | ||
@@ -17,0 +16,0 @@ return { |
@@ -1,5 +0,1 @@ | ||
/** | ||
* @fileoverview Enforce that elements with onClick handlers must be focusable. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
@@ -27,2 +23,7 @@ | ||
/** | ||
* @fileoverview Enforce that elements with onClick handlers must be focusable. | ||
* @author Ethan Cohen | ||
*/ | ||
var errorMessage = 'Elements with onClick handlers must be focusable. ' + 'Either set the tabIndex property to a valid value (usually 0), or use ' + 'an element type which is inherently focusable such as `button`.'; | ||
@@ -29,0 +30,0 @@ |
@@ -1,6 +0,1 @@ | ||
/** | ||
* @fileoverview Enforce non-interactive elements with | ||
* click handlers use role attribute. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
@@ -24,3 +19,7 @@ | ||
var errorMessage = 'Visible, non-interactive elements with click handlers must ' + 'have role attribute.'; | ||
var errorMessage = 'Visible, non-interactive elements with click handlers must ' + 'have role attribute.'; /** | ||
* @fileoverview Enforce non-interactive elements with | ||
* click handlers use role attribute. | ||
* @author Ethan Cohen | ||
*/ | ||
@@ -27,0 +26,0 @@ module.exports = function (context) { |
@@ -1,11 +0,3 @@ | ||
/** | ||
* @fileoverview Enforce that elements with ARIA roles must have all required attributes for that role. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var _role = require('../util/attributes/role'); | ||
@@ -19,2 +11,12 @@ | ||
/** | ||
* @fileoverview Enforce that elements with ARIA roles must | ||
* have all required attributes for that role. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var errorMessage = function errorMessage(role, requiredProps) { | ||
@@ -35,3 +37,4 @@ return 'Elements with the ARIA role "' + role + '" must have the following ' + ('attributes defined: ' + String(requiredProps).toLowerCase()); | ||
// If value is undefined, then the role attribute will be dropped in the DOM. | ||
// If value is null, then getLiteralAttributeValue is telling us that the value isn't in the form of a literal. | ||
// If value is null, then getLiteralAttributeValue is telling us | ||
// that the value isn't in the form of a literal. | ||
if (value === undefined || value === null) { | ||
@@ -42,4 +45,4 @@ return; | ||
var normalizedValues = String(value).toUpperCase().split(' '); | ||
var validRoles = normalizedValues.filter(function (value) { | ||
return Object.keys(_role2.default).indexOf(value) > -1; | ||
var validRoles = normalizedValues.filter(function (val) { | ||
return Object.keys(_role2.default).indexOf(val) > -1; | ||
}); | ||
@@ -46,0 +49,0 @@ |
@@ -1,12 +0,3 @@ | ||
/** | ||
* @fileoverview Enforce that elements with explicit or implicit roles defined contain only | ||
* `aria-*` properties supported by that `role`. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var _role = require('../util/attributes/role'); | ||
@@ -28,2 +19,12 @@ | ||
/** | ||
* @fileoverview Enforce that elements with explicit or implicit roles defined contain only | ||
* `aria-*` properties supported by that `role`. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
// Rule Definition | ||
// ---------------------------------------------------------------------------- | ||
var errorMessage = function errorMessage(attr, role, tag, isImplicit) { | ||
@@ -60,2 +61,6 @@ if (isImplicit) { | ||
node.attributes.forEach(function (prop) { | ||
if (prop.type === 'JSXSpreadAttribute') { | ||
return; | ||
} | ||
if (invalidAriaPropsForRole.indexOf(prop.name.name.toUpperCase()) > -1) { | ||
@@ -62,0 +67,0 @@ context.report({ |
@@ -1,7 +0,10 @@ | ||
/** | ||
* @fileoverview Enforce tabIndex value is not greater than zero. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var errorMessage = 'Avoid positive integer values for tabIndex.'; /** | ||
* @fileoverview Enforce tabIndex value is not greater than zero. | ||
* @author Ethan Cohen | ||
*/ | ||
// ---------------------------------------------------------------------------- | ||
@@ -11,6 +14,2 @@ // Rule Definition | ||
var _jsxAstUtils = require('jsx-ast-utils'); | ||
var errorMessage = 'Avoid positive integer values for tabIndex.'; | ||
module.exports = function (context) { | ||
@@ -17,0 +16,0 @@ return { |
@@ -26,3 +26,6 @@ 'use strict'; | ||
var distances = dictionary.reduce(function (suggestions, dictionaryWord) { | ||
suggestions[dictionaryWord] = (0, _damerauLevenshtein2.default)(word.toUpperCase(), dictionaryWord.toUpperCase()).steps; | ||
var distance = (0, _damerauLevenshtein2.default)(word.toUpperCase(), dictionaryWord.toUpperCase()); | ||
var steps = distance.steps; | ||
suggestions[dictionaryWord] = steps; // eslint-disable-line | ||
return suggestions; | ||
@@ -29,0 +32,0 @@ }, {}); |
@@ -20,3 +20,3 @@ 'use strict'; | ||
if (hidden && hidden.toUpperCase() == 'HIDDEN') { | ||
if (hidden && hidden.toUpperCase() === 'HIDDEN') { | ||
return true; | ||
@@ -23,0 +23,0 @@ } |
{ | ||
"name": "eslint-plugin-jsx-a11y", | ||
"version": "1.4.0", | ||
"version": "1.4.1", | ||
"description": "A static analysis linter of jsx and their accessibility with screen readers.", | ||
@@ -23,3 +23,3 @@ "keywords": [ | ||
"coveralls": "cat ./reports/coverage/lcov.info | coveralls", | ||
"lint": "eslint --config .eslintrc.js .", | ||
"lint": "eslint --config .eslintrc src tests", | ||
"pretest": "npm run lint", | ||
@@ -35,2 +35,4 @@ "test": "istanbul cover --dir reports/coverage node_modules/mocha/bin/_mocha tests/**/*.js -- --compilers js:babel-core/register --reporter dot" | ||
"eslint": "^2.2.0", | ||
"eslint-config-airbnb-base": "^3.0.1", | ||
"eslint-plugin-import": "^1.8.1", | ||
"istanbul": "^1.0.0-alpha.2", | ||
@@ -37,0 +39,0 @@ "mocha": "^2.4.5", |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
/* eslint-disable global-require */ | ||
@@ -20,3 +20,3 @@ module.exports = { | ||
'role-supports-aria-props': require('./rules/role-supports-aria-props'), | ||
'tabindex-no-positive': require('./rules/tabindex-no-positive') | ||
'tabindex-no-positive': require('./rules/tabindex-no-positive'), | ||
}, | ||
@@ -27,4 +27,4 @@ configs: { | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}, | ||
@@ -47,6 +47,6 @@ rules: { | ||
'jsx-a11y/role-supports-aria-props': 2, | ||
'jsx-a11y/tabindex-no-positive': 2 | ||
} | ||
} | ||
} | ||
'jsx-a11y/tabindex-no-positive': 2, | ||
}, | ||
}, | ||
}, | ||
}; |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -42,10 +41,10 @@ // ---------------------------------------------------------------------------- | ||
node: attribute, | ||
message: errorMessage(name) | ||
message: errorMessage(name), | ||
}); | ||
} | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -22,3 +21,4 @@ // ---------------------------------------------------------------------------- | ||
case 'tokenlist': | ||
return `The value for ${name} must be a list of one or more tokens from the following: ${permittedValues}.`; | ||
return `The value for ${name} must be a list of one or more \ | ||
tokens from the following: ${permittedValues}.`; | ||
case 'boolean': | ||
@@ -40,3 +40,3 @@ case 'string': | ||
case 'tristate': | ||
return typeof value === 'boolean' || value == 'mixed'; | ||
return typeof value === 'boolean' || value === 'mixed'; | ||
case 'integer': | ||
@@ -49,3 +49,4 @@ case 'number': | ||
case 'tokenlist': | ||
return typeof value === 'string' && value.split(' ').every(token => permittedValues.indexOf(token.toLowerCase()) > -1); | ||
return typeof value === 'string' && | ||
value.split(' ').every(token => permittedValues.indexOf(token.toLowerCase()) > -1); | ||
default: | ||
@@ -79,3 +80,4 @@ return false; | ||
const isValid = validityCheck(value, permittedType, permittedValues) || (allowUndefined && value === undefined); | ||
const isValid = validityCheck(value, permittedType, permittedValues) || | ||
(allowUndefined && value === undefined); | ||
@@ -88,9 +90,9 @@ if (isValid) { | ||
node: attribute, | ||
message: errorMessage(name, permittedType, permittedValues) | ||
message: errorMessage(name, permittedType, permittedValues), | ||
}); | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -27,3 +26,4 @@ // ---------------------------------------------------------------------------- | ||
// If value is undefined, then the role attribute will be dropped in the DOM. | ||
// If value is null, then getLiteralAttributeValue is telling us that the value isn't in the form of a literal. | ||
// If value is null, then getLiteralAttributeValue is telling us that the | ||
// value isn't in the form of a literal. | ||
if (value === undefined || value === null) { | ||
@@ -35,3 +35,3 @@ return; | ||
const validRoles = Object.keys(roles).filter(role => roles[role].abstract === false); | ||
const isValid = normalizedValues.every(value => validRoles.indexOf(value) > -1); | ||
const isValid = normalizedValues.every(val => validRoles.indexOf(val) > -1); | ||
@@ -44,9 +44,9 @@ if (isValid === true) { | ||
node: attribute, | ||
message: errorMessage | ||
message: errorMessage, | ||
}); | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
/** | ||
* @fileoverview Enforce that elements that do not support ARIA roles, states and properties do not have those attributes. | ||
* @fileoverview Enforce that elements that do not support ARIA roles, | ||
* states and properties do not have those attributes. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
@@ -16,3 +16,4 @@ // ---------------------------------------------------------------------------- | ||
const errorMessage = invalidProp => | ||
`This element does not support ARIA roles, states and properties. Try removing the prop '${invalidProp}'.`; | ||
`This element does not support ARIA roles, states and properties. \ | ||
Try removing the prop '${invalidProp}'.`; | ||
@@ -33,14 +34,18 @@ module.exports = context => ({ | ||
node.attributes.forEach(prop => { | ||
if (prop.type === 'JSXSpreadAttribute') { | ||
return; | ||
} | ||
if (invalidAttributes.indexOf(prop.name.name.toUpperCase()) > -1) { | ||
context.report({ | ||
node, | ||
message: errorMessage(prop.name.name) | ||
message: errorMessage(prop.name.name), | ||
}); | ||
} | ||
}); | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -14,7 +13,8 @@ // ---------------------------------------------------------------------------- | ||
const errorMessage = 'Links must not point to "#". Use a more descriptive href or use a button instead.'; | ||
const errorMessage = 'Links must not point to "#". ' + | ||
'Use a more descriptive href or use a button instead.'; | ||
module.exports = context => ({ | ||
JSXOpeningElement: node => { | ||
const typeCheck = [ 'a' ].concat(context.options[0]); | ||
const typeCheck = ['a'].concat(context.options[0]); | ||
const nodeType = elementType(node); | ||
@@ -33,6 +33,6 @@ | ||
node, | ||
message: errorMessage | ||
message: errorMessage, | ||
}); | ||
} | ||
} | ||
}, | ||
}); | ||
@@ -42,14 +42,14 @@ | ||
{ | ||
'oneOf': [ | ||
{ 'type': 'string' }, | ||
oneOf: [ | ||
{ type: 'string' }, | ||
{ | ||
'type': 'array', | ||
'items': { | ||
'type': 'string' | ||
type: 'array', | ||
items: { | ||
type: 'string', | ||
}, | ||
'minItems': 1, | ||
'uniqueItems': true | ||
} | ||
] | ||
} | ||
minItems: 1, | ||
uniqueItems: true, | ||
}, | ||
], | ||
}, | ||
]; |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -16,3 +15,3 @@ // ---------------------------------------------------------------------------- | ||
JSXOpeningElement: node => { | ||
const typeCheck = [ 'img' ].concat(context.options[0]); | ||
const typeCheck = ['img'].concat(context.options[0]); | ||
const nodeType = elementType(node); | ||
@@ -27,3 +26,4 @@ | ||
const roleValue = getPropValue(roleProp); | ||
const isPresentation = roleProp && typeof roleValue === 'string' && roleValue.toLowerCase() === 'presentation'; | ||
const isPresentation = roleProp && typeof roleValue === 'string' | ||
&& roleValue.toLowerCase() === 'presentation'; | ||
@@ -40,3 +40,3 @@ if (isPresentation) { | ||
node, | ||
message: `${nodeType} elements must have an alt prop or use role="presentation".` | ||
message: `${nodeType} elements must have an alt prop or use role="presentation".`, | ||
}); | ||
@@ -58,5 +58,6 @@ return; | ||
message: | ||
`Invalid alt value for ${nodeType}. Use alt="" or role="presentation" for presentational images.` | ||
`Invalid alt value for ${nodeType}. \ | ||
Use alt="" or role="presentation" for presentational images.`, | ||
}); | ||
} | ||
}, | ||
}); | ||
@@ -66,14 +67,14 @@ | ||
{ | ||
'oneOf': [ | ||
{ 'type': 'string' }, | ||
oneOf: [ | ||
{ type: 'string' }, | ||
{ | ||
'type': 'array', | ||
'items': { | ||
'type': 'string' | ||
type: 'array', | ||
items: { | ||
type: 'string', | ||
}, | ||
'minItems': 1, | ||
'uniqueItems': true | ||
} | ||
] | ||
} | ||
minItems: 1, | ||
uniqueItems: true, | ||
}, | ||
], | ||
}, | ||
]; |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -18,11 +17,12 @@ // ---------------------------------------------------------------------------- | ||
'photo', | ||
'picture' | ||
'picture', | ||
]; | ||
const errorMessage = 'Redundant alt attribute. Screen-readers already announce `img` tags as an image. ' + | ||
'You don\'t need to use the words `image`, `photo,` or `picture` in the alt prop.'; | ||
const errorMessage = 'Redundant alt attribute. Screen-readers already announce ' + | ||
'`img` tags as an image. You don\'t need to use the words `image`, ' + | ||
'`photo,` or `picture` in the alt prop.'; | ||
const validTypes = [ | ||
'LITERAL', | ||
'TEMPLATELITERAL' | ||
'TEMPLATELITERAL', | ||
]; | ||
@@ -44,3 +44,4 @@ | ||
// Only check literals, as we should not enforce variable names :P | ||
const normalizedType = altProp.value && altProp.value.type.toUpperCase() === 'JSXEXPRESSIONCONTAINER' ? | ||
const normalizedType = altProp.value && | ||
altProp.value.type.toUpperCase() === 'JSXEXPRESSIONCONTAINER' ? | ||
altProp.value.expression.type.toUpperCase() : | ||
@@ -63,3 +64,3 @@ altProp.value.type.toUpperCase(); | ||
node, | ||
message: errorMessage | ||
message: errorMessage, | ||
}); | ||
@@ -70,7 +71,7 @@ } | ||
} | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -19,3 +18,3 @@ // ---------------------------------------------------------------------------- | ||
JSXOpeningElement: node => { | ||
const typeCheck = [ 'label' ].concat(context.options[0]); | ||
const typeCheck = ['label'].concat(context.options[0]); | ||
const nodeType = elementType(node); | ||
@@ -35,6 +34,6 @@ | ||
node, | ||
message: errorMessage | ||
message: errorMessage, | ||
}); | ||
} | ||
} | ||
}, | ||
}); | ||
@@ -44,14 +43,14 @@ | ||
{ | ||
'oneOf': [ | ||
{ 'type': 'string' }, | ||
oneOf: [ | ||
{ type: 'string' }, | ||
{ | ||
'type': 'array', | ||
'items': { | ||
'type': 'string' | ||
type: 'array', | ||
items: { | ||
type: 'string', | ||
}, | ||
'minItems': 1, | ||
'uniqueItems': true | ||
} | ||
] | ||
} | ||
minItems: 1, | ||
uniqueItems: true, | ||
}, | ||
], | ||
}, | ||
]; |
@@ -6,3 +6,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -34,3 +33,3 @@ // ---------------------------------------------------------------------------- | ||
node, | ||
message: mouseOverErrorMessage | ||
message: mouseOverErrorMessage, | ||
}); | ||
@@ -50,11 +49,11 @@ } | ||
node, | ||
message: mouseOutErrorMessage | ||
message: mouseOutErrorMessage, | ||
}); | ||
} | ||
} | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -26,10 +25,10 @@ // ---------------------------------------------------------------------------- | ||
node, | ||
message: errorMessage | ||
message: errorMessage, | ||
}); | ||
} | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -26,10 +25,10 @@ // ---------------------------------------------------------------------------- | ||
node, | ||
message: errorMessage | ||
message: errorMessage, | ||
}); | ||
} | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -40,9 +39,9 @@ import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader'; | ||
node, | ||
message: errorMessage | ||
message: errorMessage, | ||
}); | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
@@ -6,3 +6,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -40,9 +39,9 @@ import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader'; | ||
node, | ||
message: errorMessage | ||
message: errorMessage, | ||
}); | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
/** | ||
* @fileoverview Enforce that elements with ARIA roles must have all required attributes for that role. | ||
* @fileoverview Enforce that elements with ARIA roles must | ||
* have all required attributes for that role. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
@@ -29,3 +29,4 @@ // ---------------------------------------------------------------------------- | ||
// If value is undefined, then the role attribute will be dropped in the DOM. | ||
// If value is null, then getLiteralAttributeValue is telling us that the value isn't in the form of a literal. | ||
// If value is null, then getLiteralAttributeValue is telling us | ||
// that the value isn't in the form of a literal. | ||
if (value === undefined || value === null) { | ||
@@ -36,3 +37,4 @@ return; | ||
const normalizedValues = String(value).toUpperCase().split(' '); | ||
const validRoles = normalizedValues.filter(value => Object.keys(validRoleTypes).indexOf(value) > -1); | ||
const validRoles = normalizedValues | ||
.filter(val => Object.keys(validRoleTypes).indexOf(val) > -1); | ||
@@ -43,3 +45,4 @@ validRoles.forEach(role => { | ||
if (requiredProps.length > 0) { | ||
const hasRequiredProps = requiredProps.every(prop => getProp(attribute.parent.attributes, prop)); | ||
const hasRequiredProps = requiredProps | ||
.every(prop => getProp(attribute.parent.attributes, prop)); | ||
@@ -49,3 +52,3 @@ if (hasRequiredProps === false) { | ||
node: attribute, | ||
message: errorMessage(role.toLowerCase(), requiredProps) | ||
message: errorMessage(role.toLowerCase(), requiredProps), | ||
}); | ||
@@ -55,8 +58,7 @@ } | ||
}); | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
@@ -6,3 +6,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -20,3 +19,4 @@ // ---------------------------------------------------------------------------- | ||
if (isImplicit) { | ||
return `The attribute ${attr} is not supported by the role ${role}. This role is implicit on the element ${tag}.`; | ||
return `The attribute ${attr} is not supported by the role ${role}. \ | ||
This role is implicit on the element ${tag}.`; | ||
} | ||
@@ -44,17 +44,22 @@ | ||
const propertySet = ROLES[roleValue.toUpperCase()].props; | ||
const invalidAriaPropsForRole = Object.keys(ARIA).filter(attribute => propertySet.indexOf(attribute) === -1); | ||
const invalidAriaPropsForRole = Object.keys(ARIA) | ||
.filter(attribute => propertySet.indexOf(attribute) === -1); | ||
node.attributes.forEach(prop => { | ||
if (prop.type === 'JSXSpreadAttribute') { | ||
return; | ||
} | ||
if (invalidAriaPropsForRole.indexOf(prop.name.name.toUpperCase()) > -1) { | ||
context.report({ | ||
node, | ||
message: errorMessage(prop.name.name, roleValue, type, isImplicit) | ||
message: errorMessage(prop.name.name, roleValue, type, isImplicit), | ||
}); | ||
} | ||
}); | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
'use strict'; | ||
@@ -35,9 +34,9 @@ // ---------------------------------------------------------------------------- | ||
node: attribute, | ||
message: errorMessage | ||
message: errorMessage, | ||
}); | ||
} | ||
}, | ||
}); | ||
module.exports.schema = [ | ||
{ type: 'object' } | ||
{ type: 'object' }, | ||
]; |
@@ -1,6 +0,3 @@ | ||
'use strict'; | ||
import editDistance from 'damerau-levenshtein'; | ||
// Minimum edit distance to be considered a good suggestion. | ||
@@ -15,3 +12,5 @@ const THRESHOLD = 2; | ||
const distances = dictionary.reduce((suggestions, dictionaryWord) => { | ||
suggestions[dictionaryWord] = editDistance(word.toUpperCase(), dictionaryWord.toUpperCase()).steps; | ||
const distance = editDistance(word.toUpperCase(), dictionaryWord.toUpperCase()); | ||
const { steps } = distance; | ||
suggestions[dictionaryWord] = steps; // eslint-disable-line | ||
return suggestions; | ||
@@ -18,0 +17,0 @@ }, {}); |
@@ -1,3 +0,1 @@ | ||
'use strict'; | ||
import { getPropValue, getLiteralPropValue } from 'jsx-ast-utils'; | ||
@@ -4,0 +2,0 @@ |
@@ -39,4 +39,2 @@ import A from './a'; | ||
export default { | ||
@@ -79,3 +77,3 @@ A, | ||
THEAD, | ||
UL | ||
UL, | ||
}; |
@@ -1,3 +0,1 @@ | ||
'use strict'; | ||
import { getProp, getPropValue, getLiteralPropValue } from 'jsx-ast-utils'; | ||
@@ -16,3 +14,3 @@ | ||
if (hidden && hidden.toUpperCase() == 'HIDDEN') { | ||
if (hidden && hidden.toUpperCase() === 'HIDDEN') { | ||
return true; | ||
@@ -19,0 +17,0 @@ } |
@@ -1,3 +0,1 @@ | ||
'use strict'; | ||
import { getProp, getPropValue, getLiteralPropValue } from 'jsx-ast-utils'; | ||
@@ -23,3 +21,3 @@ import getTabIndex from './getTabIndex'; | ||
select: () => true, | ||
textarea: () => true | ||
textarea: () => true, | ||
}; | ||
@@ -26,0 +24,0 @@ |
/* eslint-env mocha */ | ||
'use strict'; | ||
import plugin from '../src'; | ||
@@ -18,3 +16,3 @@ | ||
plugin.rules[ruleName], | ||
require(path.join('../src/rules', ruleName)) | ||
require(path.join('../src/rules', ruleName)) // eslint-disable-line global-require | ||
); | ||
@@ -21,0 +19,0 @@ }); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -21,4 +19,4 @@ // Requirements | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -40,3 +38,3 @@ | ||
type: 'JSXAttribute', | ||
message: `${message} Did you mean to use ${suggestions}?` | ||
message: `${message} Did you mean to use ${suggestions}?`, | ||
}; | ||
@@ -47,3 +45,3 @@ } | ||
type: 'JSXAttribute', | ||
message | ||
message, | ||
}; | ||
@@ -55,3 +53,3 @@ }; | ||
code: `<div ${prop.toLowerCase()}="foobar" />`, | ||
parserOptions | ||
parserOptions, | ||
})); | ||
@@ -68,9 +66,17 @@ | ||
{ code: '<div fooaria-hidden="true"></div>', parserOptions }, | ||
{ code: '<Bar baz />', parserOptions } | ||
{ code: '<Bar baz />', parserOptions }, | ||
].concat(basicValidityTests), | ||
invalid: [ | ||
{ code: '<div aria-="foobar" />', errors: [ errorMessage('aria-') ], parserOptions }, | ||
{ code: '<div aria-labeledby="foobar" />', errors: [ errorMessage('aria-labeledby') ], parserOptions }, | ||
{ code: '<div aria-skldjfaria-klajsd="foobar" />', errors: [ errorMessage('aria-skldjfaria-klajsd') ], parserOptions } | ||
] | ||
{ code: '<div aria-="foobar" />', errors: [errorMessage('aria-')], parserOptions }, | ||
{ | ||
code: '<div aria-labeledby="foobar" />', | ||
errors: [errorMessage('aria-labeledby')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div aria-skldjfaria-klajsd="foobar" />', | ||
errors: [errorMessage('aria-skldjfaria-klajsd')], | ||
parserOptions, | ||
}, | ||
], | ||
}); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -19,4 +17,4 @@ // Requirements | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -41,3 +39,4 @@ | ||
case 'tokenlist': | ||
return `The value for ${name} must be a list of one or more tokens from the following: ${permittedValues}.`; | ||
return `The value for ${name} must be a list of one or more \ | ||
tokens from the following: ${permittedValues}.`; | ||
case 'boolean': | ||
@@ -146,66 +145,130 @@ case 'string': | ||
{ code: '<div aria-relevant={foo} />', parserOptions }, | ||
{ code: '<div aria-relevant={foo.bar} />', parserOptions } | ||
{ code: '<div aria-relevant={foo.bar} />', parserOptions }, | ||
], | ||
invalid: [ | ||
// BOOLEAN | ||
{ code: '<div aria-hidden={undefined} />', errors: [ errorMessage('aria-hidden') ], parserOptions }, | ||
{ code: '<div aria-hidden="yes" />', errors: [ errorMessage('aria-hidden') ], parserOptions }, | ||
{ code: '<div aria-hidden="no" />', errors: [ errorMessage('aria-hidden') ], parserOptions }, | ||
{ code: '<div aria-hidden={1234} />', errors: [ errorMessage('aria-hidden') ], parserOptions }, | ||
{ code: '<div aria-hidden={`${abc}`} />', errors: [ errorMessage('aria-hidden') ], parserOptions }, | ||
{ | ||
code: '<div aria-hidden={undefined} />', | ||
errors: [errorMessage('aria-hidden')], | ||
parserOptions, | ||
}, | ||
{ code: '<div aria-hidden="yes" />', errors: [errorMessage('aria-hidden')], parserOptions }, | ||
{ code: '<div aria-hidden="no" />', errors: [errorMessage('aria-hidden')], parserOptions }, | ||
{ code: '<div aria-hidden={1234} />', errors: [errorMessage('aria-hidden')], parserOptions }, | ||
{ | ||
code: '<div aria-hidden={`${abc}`} />', | ||
errors: [errorMessage('aria-hidden')], | ||
parserOptions, | ||
}, | ||
// STRING | ||
{ code: '<div aria-label={undefined} />', errors: [ errorMessage('aria-label') ], parserOptions }, | ||
{ code: '<div aria-label />', errors: [ errorMessage('aria-label') ], parserOptions }, | ||
{ code: '<div aria-label={true} />', errors: [ errorMessage('aria-label') ], parserOptions }, | ||
{ code: '<div aria-label={false} />', errors: [ errorMessage('aria-label') ], parserOptions }, | ||
{ code: '<div aria-label={1234} />', errors: [ errorMessage('aria-label') ], parserOptions }, | ||
{ code: '<div aria-label={!true} />', errors: [ errorMessage('aria-label') ], parserOptions }, | ||
{ code: '<div aria-label={undefined} />', errors: [errorMessage('aria-label')], parserOptions }, | ||
{ code: '<div aria-label />', errors: [errorMessage('aria-label')], parserOptions }, | ||
{ code: '<div aria-label={true} />', errors: [errorMessage('aria-label')], parserOptions }, | ||
{ code: '<div aria-label={false} />', errors: [errorMessage('aria-label')], parserOptions }, | ||
{ code: '<div aria-label={1234} />', errors: [errorMessage('aria-label')], parserOptions }, | ||
{ code: '<div aria-label={!true} />', errors: [errorMessage('aria-label')], parserOptions }, | ||
// TRISTATE | ||
{ code: '<div aria-checked={undefined} />', errors: [ errorMessage('aria-checked') ], parserOptions }, | ||
{ code: '<div aria-checked="yes" />', errors: [ errorMessage('aria-checked') ], parserOptions }, | ||
{ code: '<div aria-checked="no" />', errors: [ errorMessage('aria-checked') ], parserOptions }, | ||
{ code: '<div aria-checked={1234} />', errors: [ errorMessage('aria-checked') ], parserOptions }, | ||
{ code: '<div aria-checked={`${abc}`} />', errors: [ errorMessage('aria-checked') ], parserOptions }, | ||
{ | ||
code: '<div aria-checked={undefined} />', | ||
errors: [errorMessage('aria-checked')], | ||
parserOptions, | ||
}, | ||
{ code: '<div aria-checked="yes" />', errors: [errorMessage('aria-checked')], parserOptions }, | ||
{ code: '<div aria-checked="no" />', errors: [errorMessage('aria-checked')], parserOptions }, | ||
{ code: '<div aria-checked={1234} />', errors: [errorMessage('aria-checked')], parserOptions }, | ||
{ | ||
code: '<div aria-checked={`${abc}`} />', | ||
errors: [errorMessage('aria-checked')], | ||
parserOptions, | ||
}, | ||
// INTEGER | ||
{ code: '<div aria-level={undefined} />', errors: [ errorMessage('aria-level') ], parserOptions }, | ||
{ code: '<div aria-level="yes" />', errors: [ errorMessage('aria-level') ], parserOptions }, | ||
{ code: '<div aria-level="no" />', errors: [ errorMessage('aria-level') ], parserOptions }, | ||
{ code: '<div aria-level={`abc`} />', errors: [ errorMessage('aria-level') ], parserOptions }, | ||
{ code: '<div aria-level={true} />', errors: [ errorMessage('aria-level') ], parserOptions }, | ||
{ code: '<div aria-level />', errors: [ errorMessage('aria-level') ], parserOptions }, | ||
{ code: '<div aria-level={"false"} />', errors: [ errorMessage('aria-level') ], parserOptions }, | ||
{ code: '<div aria-level={!"false"} />', errors: [ errorMessage('aria-level') ], parserOptions }, | ||
{ code: '<div aria-level={undefined} />', errors: [errorMessage('aria-level')], parserOptions }, | ||
{ code: '<div aria-level="yes" />', errors: [errorMessage('aria-level')], parserOptions }, | ||
{ code: '<div aria-level="no" />', errors: [errorMessage('aria-level')], parserOptions }, | ||
{ code: '<div aria-level={`abc`} />', errors: [errorMessage('aria-level')], parserOptions }, | ||
{ code: '<div aria-level={true} />', errors: [errorMessage('aria-level')], parserOptions }, | ||
{ code: '<div aria-level />', errors: [errorMessage('aria-level')], parserOptions }, | ||
{ code: '<div aria-level={"false"} />', errors: [errorMessage('aria-level')], parserOptions }, | ||
{ code: '<div aria-level={!"false"} />', errors: [errorMessage('aria-level')], parserOptions }, | ||
// NUMBER | ||
{ code: '<div aria-valuemax={undefined} />', errors: [ errorMessage('aria-valuemax') ], parserOptions }, | ||
{ code: '<div aria-valuemax="yes" />', errors: [ errorMessage('aria-valuemax') ], parserOptions }, | ||
{ code: '<div aria-valuemax="no" />', errors: [ errorMessage('aria-valuemax') ], parserOptions }, | ||
{ code: '<div aria-valuemax={`abc`} />', errors: [ errorMessage('aria-valuemax') ], parserOptions }, | ||
{ code: '<div aria-valuemax={true} />', errors: [ errorMessage('aria-valuemax') ], parserOptions }, | ||
{ code: '<div aria-valuemax />', errors: [ errorMessage('aria-valuemax') ], parserOptions }, | ||
{ code: '<div aria-valuemax={"false"} />', errors: [ errorMessage('aria-valuemax') ], parserOptions }, | ||
{ code: '<div aria-valuemax={!"false"} />', errors: [ errorMessage('aria-valuemax') ], parserOptions }, | ||
{ | ||
code: '<div aria-valuemax={undefined} />', | ||
errors: [errorMessage('aria-valuemax')], | ||
parserOptions, | ||
}, | ||
{ code: '<div aria-valuemax="yes" />', errors: [errorMessage('aria-valuemax')], parserOptions }, | ||
{ code: '<div aria-valuemax="no" />', errors: [errorMessage('aria-valuemax')], parserOptions }, | ||
{ | ||
code: '<div aria-valuemax={`abc`} />', | ||
errors: [errorMessage('aria-valuemax')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div aria-valuemax={true} />', | ||
errors: [errorMessage('aria-valuemax')], | ||
parserOptions, | ||
}, | ||
{ code: '<div aria-valuemax />', errors: [errorMessage('aria-valuemax')], parserOptions }, | ||
{ | ||
code: '<div aria-valuemax={"false"} />', | ||
errors: [errorMessage('aria-valuemax')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div aria-valuemax={!"false"} />', | ||
errors: [errorMessage('aria-valuemax')], | ||
parserOptions, | ||
}, | ||
// TOKEN | ||
{ code: '<div aria-sort="" />', errors: [ errorMessage('aria-sort') ], parserOptions }, | ||
{ code: '<div aria-sort="descnding" />', errors: [ errorMessage('aria-sort') ], parserOptions }, | ||
{ code: '<div aria-sort />', errors: [ errorMessage('aria-sort') ], parserOptions }, | ||
{ code: '<div aria-sort={undefined} />', errors: [ errorMessage('aria-sort') ], parserOptions }, | ||
{ code: '<div aria-sort={true} />', errors: [ errorMessage('aria-sort') ], parserOptions }, | ||
{ code: '<div aria-sort={"false"} />', errors: [ errorMessage('aria-sort') ], parserOptions }, | ||
{ code: '<div aria-sort="ascending descending" />', errors: [ errorMessage('aria-sort') ], parserOptions }, | ||
{ code: '<div aria-sort="" />', errors: [errorMessage('aria-sort')], parserOptions }, | ||
{ code: '<div aria-sort="descnding" />', errors: [errorMessage('aria-sort')], parserOptions }, | ||
{ code: '<div aria-sort />', errors: [errorMessage('aria-sort')], parserOptions }, | ||
{ code: '<div aria-sort={undefined} />', errors: [errorMessage('aria-sort')], parserOptions }, | ||
{ code: '<div aria-sort={true} />', errors: [errorMessage('aria-sort')], parserOptions }, | ||
{ code: '<div aria-sort={"false"} />', errors: [errorMessage('aria-sort')], parserOptions }, | ||
{ | ||
code: '<div aria-sort="ascending descending" />', | ||
errors: [errorMessage('aria-sort')], | ||
parserOptions, | ||
}, | ||
// TOKENLIST | ||
{ code: '<div aria-relevant="" />', errors: [ errorMessage('aria-relevant') ], parserOptions }, | ||
{ code: '<div aria-relevant="foobar" />', errors: [ errorMessage('aria-relevant') ], parserOptions }, | ||
{ code: '<div aria-relevant />', errors: [ errorMessage('aria-relevant') ], parserOptions }, | ||
{ code: '<div aria-relevant={undefined} />', errors: [ errorMessage('aria-relevant') ], parserOptions }, | ||
{ code: '<div aria-relevant={true} />', errors: [ errorMessage('aria-relevant') ], parserOptions }, | ||
{ code: '<div aria-relevant={"false"} />', errors: [ errorMessage('aria-relevant') ], parserOptions }, | ||
{ code: '<div aria-relevant="additions removalss" />', errors: [ errorMessage('aria-relevant') ], parserOptions }, | ||
{ code: '<div aria-relevant="additions removalss " />', errors: [ errorMessage('aria-relevant') ], parserOptions } | ||
] | ||
{ code: '<div aria-relevant="" />', errors: [errorMessage('aria-relevant')], parserOptions }, | ||
{ | ||
code: '<div aria-relevant="foobar" />', | ||
errors: [errorMessage('aria-relevant')], | ||
parserOptions, | ||
}, | ||
{ code: '<div aria-relevant />', errors: [errorMessage('aria-relevant')], parserOptions }, | ||
{ | ||
code: '<div aria-relevant={undefined} />', | ||
errors: [errorMessage('aria-relevant')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div aria-relevant={true} />', | ||
errors: [errorMessage('aria-relevant')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div aria-relevant={"false"} />', | ||
errors: [errorMessage('aria-relevant')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div aria-relevant="additions removalss" />', | ||
errors: [errorMessage('aria-relevant')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div aria-relevant="additions removalss " />', | ||
errors: [errorMessage('aria-relevant')], | ||
parserOptions, | ||
}, | ||
], | ||
}); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -15,2 +13,4 @@ // Requirements | ||
import { RuleTester } from 'eslint'; | ||
import ROLES from '../../../src/util/attributes/role'; | ||
import assign from 'object-assign'; | ||
@@ -20,4 +20,4 @@ const parserOptions = { | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -33,7 +33,5 @@ | ||
message: 'Elements with ARIA roles must use a valid, non-abstract ARIA role.', | ||
type: 'JSXAttribute' | ||
type: 'JSXAttribute', | ||
}; | ||
import ROLES from '../../../src/util/attributes/role'; | ||
const validRoles = Object.keys(ROLES).filter(role => ROLES[role].abstract === false); | ||
@@ -44,3 +42,3 @@ const invalidRoles = Object.keys(ROLES).filter(role => ROLES[role].abstract === true); | ||
code: `<div role="${role.toLowerCase()}" />`, | ||
parserOptions | ||
parserOptions, | ||
})); | ||
@@ -50,4 +48,5 @@ | ||
const invalidTests = createTests(invalidRoles).map(test => { | ||
test.errors = [ errorMessage ]; | ||
return test; | ||
const invalidTest = assign({}, test); | ||
invalidTest.errors = [errorMessage]; | ||
return invalidTest; | ||
}); | ||
@@ -66,16 +65,16 @@ | ||
{ code: '<div role="doc-appendix doc-bibliography" />', parserOptions }, | ||
{ code: '<Bar baz />', parserOptions } | ||
{ code: '<Bar baz />', parserOptions }, | ||
].concat(validTests), | ||
invalid: [ | ||
{ code: '<div role="foobar" />', errors: [ errorMessage ], parserOptions }, | ||
{ code: '<div role="datepicker"></div>', errors: [ errorMessage ], parserOptions }, | ||
{ code: '<div role="range"></div>', errors: [ errorMessage ], parserOptions }, | ||
{ code: '<div role=""></div>', errors: [ errorMessage ], parserOptions }, | ||
{ code: '<div role="tabpanel row foobar"></div>', errors: [ errorMessage ], parserOptions }, | ||
{ code: '<div role="tabpanel row range"></div>', errors: [ errorMessage ], parserOptions }, | ||
{ code: '<div role="doc-endnotes range"></div>', errors: [ errorMessage ], parserOptions }, | ||
{ code: '<div role />', errors: [ errorMessage ], parserOptions }, | ||
{ code: '<div role={null}></div>', errors: [ errorMessage ], parserOptions } | ||
].concat(invalidTests) | ||
{ code: '<div role="foobar" />', errors: [errorMessage], parserOptions }, | ||
{ code: '<div role="datepicker"></div>', errors: [errorMessage], parserOptions }, | ||
{ code: '<div role="range"></div>', errors: [errorMessage], parserOptions }, | ||
{ code: '<div role=""></div>', errors: [errorMessage], parserOptions }, | ||
{ code: '<div role="tabpanel row foobar"></div>', errors: [errorMessage], parserOptions }, | ||
{ code: '<div role="tabpanel row range"></div>', errors: [errorMessage], parserOptions }, | ||
{ code: '<div role="doc-endnotes range"></div>', errors: [errorMessage], parserOptions }, | ||
{ code: '<div role />', errors: [errorMessage], parserOptions }, | ||
{ code: '<div role={null}></div>', errors: [errorMessage], parserOptions }, | ||
].concat(invalidTests), | ||
}); |
/** | ||
* @fileoverview Enforce that elements that do not support ARIA roles, states and properties do not have those attributes. | ||
* @fileoverview Enforce that elements that do not support ARIA roles, | ||
* states and properties do not have those attributes. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -18,4 +17,4 @@ // Requirements | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -32,4 +31,5 @@ | ||
const errorMessage = invalidProp => ({ | ||
message: `This element does not support ARIA roles, states and properties. Try removing the prop '${invalidProp}'.`, | ||
type: 'JSXOpeningElement' | ||
message: `This element does not support ARIA roles, states and properties. \ | ||
Try removing the prop '${invalidProp}'.`, | ||
type: 'JSXOpeningElement', | ||
}); | ||
@@ -44,3 +44,3 @@ | ||
code: `<${element} ${role} />`, | ||
parserOptions | ||
parserOptions, | ||
}; | ||
@@ -55,3 +55,3 @@ }); | ||
code: `<${element} ${aria} />`, | ||
parserOptions | ||
parserOptions, | ||
}; | ||
@@ -64,5 +64,5 @@ }); | ||
.map(reservedElem => ({ | ||
code: `<${reservedElem} role />`, | ||
errors: [ errorMessage('role') ], | ||
parserOptions | ||
code: `<${reservedElem} role {...props} />`, | ||
errors: [errorMessage('role')], | ||
parserOptions, | ||
})); | ||
@@ -73,5 +73,5 @@ | ||
.map(reservedElem => ({ | ||
code: `<${reservedElem} aria-hidden />`, | ||
errors: [ errorMessage('aria-hidden') ], | ||
parserOptions | ||
code: `<${reservedElem} aria-hidden {...props} />`, | ||
errors: [errorMessage('aria-hidden')], | ||
parserOptions, | ||
})); | ||
@@ -81,3 +81,3 @@ | ||
valid: roleValidityTests.concat(ariaValidityTests), | ||
invalid: invalidRoleValidityTests.concat(invalidAriaValidityTests) | ||
invalid: invalidRoleValidityTests.concat(invalidAriaValidityTests), | ||
}); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -19,4 +17,4 @@ // Requirements | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -32,7 +30,7 @@ | ||
message: 'Links must not point to "#". Use a more descriptive href or use a button instead.', | ||
type: 'JSXOpeningElement' | ||
type: 'JSXOpeningElement', | ||
}; | ||
const string = [ 'Link' ]; | ||
const array = [ [ 'Anchor', 'Link' ] ]; | ||
const string = ['Link']; | ||
const array = [['Anchor', 'Link']]; | ||
@@ -95,23 +93,38 @@ ruleTester.run('href-no-hash', rule, { | ||
{ code: '<Link href={"foo"}/>', options: array, parserOptions }, | ||
{ code: '<Link href="#foo" />', options: array, parserOptions } | ||
{ code: '<Link href="#foo" />', options: array, parserOptions }, | ||
], | ||
invalid: [ | ||
// DEFAULT ELEMENT 'a' TESTS | ||
{ code: '<a href="#" />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<a href={"#"} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<a href={`#${undefined}`} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<a href="#" />', errors: [expectedError], parserOptions }, | ||
{ code: '<a href={"#"} />', errors: [expectedError], parserOptions }, | ||
{ code: '<a href={`#${undefined}`} />', errors: [expectedError], parserOptions }, | ||
// CUSTOM ELEMENT TEST FOR STRING OPTION | ||
{ code: '<Link href="#" />', errors: [ expectedError ], options: string, parserOptions }, | ||
{ code: '<Link href={"#"} />', errors: [ expectedError ], options: string, parserOptions }, | ||
{ code: '<Link href={`#${undefined}`} />', errors: [ expectedError ], options: string, parserOptions }, | ||
{ code: '<Link href="#" />', errors: [expectedError], options: string, parserOptions }, | ||
{ code: '<Link href={"#"} />', errors: [expectedError], options: string, parserOptions }, | ||
{ | ||
code: '<Link href={`#${undefined}`} />', | ||
errors: [expectedError], | ||
options: string, | ||
parserOptions, | ||
}, | ||
// CUSTOM ELEMENT TEST FOR ARRAY OPTION | ||
{ code: '<Link href="#" />', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Link href={"#"} />', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Link href={`#${undefined}`} />', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Anchor href="#" />', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Anchor href={"#"} />', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Anchor href={`#${undefined}`} />', errors: [ expectedError ], options: array, parserOptions } | ||
] | ||
{ code: '<Link href="#" />', errors: [expectedError], options: array, parserOptions }, | ||
{ code: '<Link href={"#"} />', errors: [expectedError], options: array, parserOptions }, | ||
{ | ||
code: '<Link href={`#${undefined}`} />', | ||
errors: [expectedError], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ code: '<Anchor href="#" />', errors: [expectedError], options: array, parserOptions }, | ||
{ code: '<Anchor href={"#"} />', errors: [expectedError], options: array, parserOptions }, | ||
{ | ||
code: '<Anchor href={`#${undefined}`} />', | ||
errors: [expectedError], | ||
options: array, | ||
parserOptions, | ||
}, | ||
], | ||
}); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -19,4 +17,4 @@ // Requirements | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -32,12 +30,13 @@ | ||
message: `${type} elements must have an alt prop or use role="presentation".`, | ||
type: 'JSXOpeningElement' | ||
type: 'JSXOpeningElement', | ||
}); | ||
const altValueError = type => ({ | ||
message: `Invalid alt value for ${type}. Use alt="" or role="presentation" for presentational images.`, | ||
type: 'JSXOpeningElement' | ||
message: `Invalid alt value for ${type}. \ | ||
Use alt="" or role="presentation" for presentational images.`, | ||
type: 'JSXOpeningElement', | ||
}); | ||
const string = [ 'Avatar' ]; | ||
const array = [ [ 'Thumbnail', 'Image' ] ]; | ||
const string = ['Avatar']; | ||
const array = [['Thumbnail', 'Image']]; | ||
@@ -127,13 +126,13 @@ | ||
{ code: '<IMAGE />', options: array, parserOptions }, | ||
{ code: '<Image alt={alt || "foo" } />', options: array, parserOptions } | ||
{ code: '<Image alt={alt || "foo" } />', options: array, parserOptions }, | ||
], | ||
invalid: [ | ||
// DEFAULT ELEMENT 'img' TESTS | ||
{ code: '<img />;', errors: [ missingPropError('img') ], parserOptions }, | ||
{ code: '<img alt />;', errors: [ altValueError('img') ], parserOptions }, | ||
{ code: '<img alt={undefined} />;', errors: [ altValueError('img') ], parserOptions }, | ||
{ code: '<img src="xyz" />', errors: [ missingPropError('img') ], parserOptions }, | ||
{ code: '<img role />', errors: [ missingPropError('img') ], parserOptions }, | ||
{ code: '<img {...this.props} />', errors: [ missingPropError('img') ], parserOptions }, | ||
{ code: '<img alt={false || false} />', errors: [ altValueError('img') ], parserOptions }, | ||
{ code: '<img />;', errors: [missingPropError('img')], parserOptions }, | ||
{ code: '<img alt />;', errors: [altValueError('img')], parserOptions }, | ||
{ code: '<img alt={undefined} />;', errors: [altValueError('img')], parserOptions }, | ||
{ code: '<img src="xyz" />', errors: [missingPropError('img')], parserOptions }, | ||
{ code: '<img role />', errors: [missingPropError('img')], parserOptions }, | ||
{ code: '<img {...this.props} />', errors: [missingPropError('img')], parserOptions }, | ||
{ code: '<img alt={false || false} />', errors: [altValueError('img')], parserOptions }, | ||
@@ -143,33 +142,77 @@ // CUSTOM ELEMENT TESTS FOR STRING OPTION | ||
code: '<Avatar />;', | ||
errors: [ missingPropError('Avatar') ], | ||
errors: [missingPropError('Avatar')], | ||
options: string, | ||
parserOptions | ||
parserOptions, | ||
}, | ||
{ code: '<Avatar alt />;', errors: [ altValueError('Avatar') ], options: string, parserOptions }, | ||
{ code: '<Avatar alt={undefined} />;', errors: [ altValueError('Avatar') ], options: string, parserOptions }, | ||
{ code: '<Avatar src="xyz" />', errors: [ missingPropError('Avatar') ], options: string, parserOptions }, | ||
{ code: '<Avatar {...this.props} />', errors: [ missingPropError('Avatar') ], options: string, parserOptions }, | ||
{ code: '<Avatar alt />;', errors: [altValueError('Avatar')], options: string, parserOptions }, | ||
{ | ||
code: '<Avatar alt={undefined} />;', | ||
errors: [altValueError('Avatar')], | ||
options: string, | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<Avatar src="xyz" />', | ||
errors: [missingPropError('Avatar')], | ||
options: string, parserOptions, | ||
}, | ||
{ | ||
code: '<Avatar {...this.props} />', | ||
errors: [missingPropError('Avatar')], | ||
options: string, | ||
parserOptions, | ||
}, | ||
// CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS | ||
{ code: '<Thumbnail />;', errors: [ missingPropError('Thumbnail') ], options: array, parserOptions }, | ||
{ code: '<Thumbnail alt />;', errors: [ altValueError('Thumbnail') ], options: array, parserOptions }, | ||
{ | ||
code: '<Thumbnail />;', | ||
errors: [missingPropError('Thumbnail')], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<Thumbnail alt />;', | ||
errors: [altValueError('Thumbnail')], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<Thumbnail alt={undefined} />;', | ||
errors: [ altValueError('Thumbnail') ], | ||
errors: [altValueError('Thumbnail')], | ||
options: array, | ||
parserOptions | ||
parserOptions, | ||
}, | ||
{ code: '<Thumbnail src="xyz" />', errors: [ missingPropError('Thumbnail') ], options: array, parserOptions }, | ||
{ | ||
code: '<Thumbnail src="xyz" />', | ||
errors: [missingPropError('Thumbnail')], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<Thumbnail {...this.props} />', | ||
errors: [ missingPropError('Thumbnail') ], | ||
errors: [missingPropError('Thumbnail')], | ||
options: array, | ||
parserOptions | ||
parserOptions, | ||
}, | ||
{ code: '<Image />;', errors: [ missingPropError('Image') ], options: array, parserOptions }, | ||
{ code: '<Image alt />;', errors: [ altValueError('Image') ], options: array, parserOptions }, | ||
{ code: '<Image alt={undefined} />;', errors: [ altValueError('Image') ], options: array, parserOptions }, | ||
{ code: '<Image src="xyz" />', errors: [ missingPropError('Image') ], options: array, parserOptions }, | ||
{ code: '<Image {...this.props} />', errors: [ missingPropError('Image') ], options: array, parserOptions } | ||
] | ||
{ code: '<Image />;', errors: [missingPropError('Image')], options: array, parserOptions }, | ||
{ code: '<Image alt />;', errors: [altValueError('Image')], options: array, parserOptions }, | ||
{ | ||
code: '<Image alt={undefined} />;', | ||
errors: [altValueError('Image')], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<Image src="xyz" />', | ||
errors: [missingPropError('Image')], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<Image {...this.props} />', | ||
errors: [missingPropError('Image')], | ||
options: array, | ||
parserOptions, | ||
}, | ||
], | ||
}); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -19,4 +17,4 @@ // Requirements | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -33,3 +31,3 @@ | ||
'You don\'t need to use the words `image`, `photo,` or `picture` in the alt prop.', | ||
type: 'JSXOpeningElement' | ||
type: 'JSXOpeningElement', | ||
}; | ||
@@ -62,25 +60,61 @@ | ||
{ code: '<UX.Layout>test</UX.Layout>', parserOptions }, | ||
{ code: '<img alt={imageAlt} />', parserOptions } | ||
{ code: '<img alt={imageAlt} />', parserOptions }, | ||
], | ||
invalid: [ | ||
{ code: '<img alt="Photo of friend." />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt="Picture of friend." />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt="Image of friend." />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt="PhOtO of friend." />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt={"photo"} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt="piCTUre of friend." />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt="imAGE of friend." />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt="photo of cool person" aria-hidden={false} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt="picture of cool person" aria-hidden={false} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt="image of cool person" aria-hidden={false} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt="photo" {...this.props} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt="image" {...this.props} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt="picture" {...this.props} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt={`picture doing ${things}`} {...this.props} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt={`photo doing ${things}`} {...this.props} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt={`image doing ${things}`} {...this.props} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt={`picture doing ${picture}`} {...this.props} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt={`photo doing ${photo}`} {...this.props} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<img alt={`image doing ${image}`} {...this.props} />', errors: [ expectedError ], parserOptions } | ||
] | ||
{ code: '<img alt="Photo of friend." />;', errors: [expectedError], parserOptions }, | ||
{ code: '<img alt="Picture of friend." />;', errors: [expectedError], parserOptions }, | ||
{ code: '<img alt="Image of friend." />;', errors: [expectedError], parserOptions }, | ||
{ code: '<img alt="PhOtO of friend." />;', errors: [expectedError], parserOptions }, | ||
{ code: '<img alt={"photo"} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<img alt="piCTUre of friend." />;', errors: [expectedError], parserOptions }, | ||
{ code: '<img alt="imAGE of friend." />;', errors: [expectedError], parserOptions }, | ||
{ | ||
code: '<img alt="photo of cool person" aria-hidden={false} />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<img alt="picture of cool person" aria-hidden={false} />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<img alt="image of cool person" aria-hidden={false} />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ code: '<img alt="photo" {...this.props} />', errors: [expectedError], parserOptions }, | ||
{ code: '<img alt="image" {...this.props} />', errors: [expectedError], parserOptions }, | ||
{ code: '<img alt="picture" {...this.props} />', errors: [expectedError], parserOptions }, | ||
{ | ||
code: '<img alt={`picture doing ${things}`} {...this.props} />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<img alt={`photo doing ${things}`} {...this.props} />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<img alt={`image doing ${things}`} {...this.props} />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<img alt={`picture doing ${picture}`} {...this.props} />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<img alt={`photo doing ${photo}`} {...this.props} />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<img alt={`image doing ${image}`} {...this.props} />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
], | ||
}); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -16,7 +14,7 @@ // Requirements | ||
const parserOptions = { | ||
const parserOptions = { | ||
ecmaVersion: 6, | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -33,7 +31,7 @@ | ||
'programmatically associated with the control using htmlFor', | ||
type: 'JSXOpeningElement' | ||
type: 'JSXOpeningElement', | ||
}; | ||
const string = [ 'Label' ]; | ||
const array = [ [ 'Label', 'Descriptor' ] ]; | ||
const string = ['Label']; | ||
const array = [['Label', 'Descriptor']]; | ||
@@ -73,31 +71,81 @@ ruleTester.run('label-has-for', rule, { | ||
{ code: '<div />', options: array, parserOptions }, | ||
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>', options: array, parserOptions } | ||
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>', options: array, parserOptions }, | ||
], | ||
invalid: [ | ||
// DEFAULT ELEMENT 'label' TESTS | ||
{ code: '<label id="foo" />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<label htmlFor={undefined} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<label htmlFor={`${undefined}`} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<label>First Name</label>', errors: [ expectedError ], parserOptions }, | ||
{ code: '<label {...props}>Foo</label>', errors: [ expectedError ], parserOptions }, | ||
{ code: '<label id="foo" />', errors: [expectedError], parserOptions }, | ||
{ code: '<label htmlFor={undefined} />', errors: [expectedError], parserOptions }, | ||
{ code: '<label htmlFor={`${undefined}`} />', errors: [expectedError], parserOptions }, | ||
{ code: '<label>First Name</label>', errors: [expectedError], parserOptions }, | ||
{ code: '<label {...props}>Foo</label>', errors: [expectedError], parserOptions }, | ||
// CUSTOM ELEMENT STRING OPTION TESTS | ||
{ code: '<Label id="foo" />', errors: [ expectedError ], options: string, parserOptions }, | ||
{ code: '<Label htmlFor={undefined} />', errors: [ expectedError ], options: string, parserOptions }, | ||
{ code: '<Label htmlFor={`${undefined}`} />', errors: [ expectedError ], options: string, parserOptions }, | ||
{ code: '<Label>First Name</Label>', errors: [ expectedError ], options: string, parserOptions }, | ||
{ code: '<Label {...props}>Foo</Label>', errors: [ expectedError ], options: string, parserOptions }, | ||
{ code: '<Label id="foo" />', errors: [expectedError], options: string, parserOptions }, | ||
{ | ||
code: '<Label htmlFor={undefined} />', | ||
errors: [expectedError], | ||
options: string, | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<Label htmlFor={`${undefined}`} />', | ||
errors: [expectedError], | ||
options: string, | ||
parserOptions, | ||
}, | ||
{ code: '<Label>First Name</Label>', errors: [expectedError], options: string, parserOptions }, | ||
{ | ||
code: '<Label {...props}>Foo</Label>', | ||
errors: [expectedError], | ||
options: string, | ||
parserOptions, | ||
}, | ||
// CUSTOM ELEMENT ARRAY OPTION TESTS | ||
{ code: '<Label id="foo" />', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Label htmlFor={undefined} />', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Label htmlFor={`${undefined}`} />', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Label>First Name</Label>', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Label {...props}>Foo</Label>', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Descriptor id="foo" />', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Descriptor htmlFor={undefined} />', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Descriptor htmlFor={`${undefined}`} />', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Descriptor>First Name</Descriptor>', errors: [ expectedError ], options: array, parserOptions }, | ||
{ code: '<Descriptor {...props}>Foo</Descriptor>', errors: [ expectedError ], options: array, parserOptions } | ||
] | ||
{ code: '<Label id="foo" />', errors: [expectedError], options: array, parserOptions }, | ||
{ | ||
code: '<Label htmlFor={undefined} />', | ||
errors: [expectedError], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<Label htmlFor={`${undefined}`} />', | ||
errors: [expectedError], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ code: '<Label>First Name</Label>', errors: [expectedError], options: array, parserOptions }, | ||
{ | ||
code: '<Label {...props}>Foo</Label>', | ||
errors: [expectedError], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ code: '<Descriptor id="foo" />', errors: [expectedError], options: array, parserOptions }, | ||
{ | ||
code: '<Descriptor htmlFor={undefined} />', | ||
errors: [expectedError], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<Descriptor htmlFor={`${undefined}`} />', | ||
errors: [expectedError], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<Descriptor>First Name</Descriptor>', | ||
errors: [expectedError], | ||
options: array, | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<Descriptor {...props}>Foo</Descriptor>', | ||
errors: [expectedError], | ||
options: array, | ||
parserOptions, | ||
}, | ||
], | ||
}); |
@@ -7,4 +7,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -20,4 +18,4 @@ // Requirements | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -33,7 +31,7 @@ | ||
message: 'onMouseOver must be accompanied by onFocus for accessibility.', | ||
type: 'JSXOpeningElement' | ||
type: 'JSXOpeningElement', | ||
}; | ||
const mouseOutError = { | ||
message: 'onMouseOut must be accompanied by onBlur for accessibility.', | ||
type: 'JSXOpeningElement' | ||
type: 'JSXOpeningElement', | ||
}; | ||
@@ -44,5 +42,11 @@ | ||
{ code: '<div onMouseOver={() => void 0} onFocus={() => void 0} />;', parserOptions }, | ||
{ code: '<div onMouseOver={() => void 0} onFocus={() => void 0} {...props} />;', parserOptions }, | ||
{ | ||
code: '<div onMouseOver={() => void 0} onFocus={() => void 0} {...props} />;', | ||
parserOptions, | ||
}, | ||
{ code: '<div onMouseOver={handleMouseOver} onFocus={handleFocus} />;', parserOptions }, | ||
{ code: '<div onMouseOver={handleMouseOver} onFocus={handleFocus} {...props} />;', parserOptions }, | ||
{ | ||
code: '<div onMouseOver={handleMouseOver} onFocus={handleFocus} {...props} />;', | ||
parserOptions, | ||
}, | ||
{ code: '<div />;', parserOptions }, | ||
@@ -52,12 +56,28 @@ { code: '<div onMouseOut={() => void 0} onBlur={() => void 0} />', parserOptions }, | ||
{ code: '<div onMouseOut={handleMouseOut} onBlur={handleOnBlur} />', parserOptions }, | ||
{ code: '<div onMouseOut={handleMouseOut} onBlur={handleOnBlur} {...props} />', parserOptions } | ||
{ code: '<div onMouseOut={handleMouseOut} onBlur={handleOnBlur} {...props} />', parserOptions }, | ||
], | ||
invalid: [ | ||
{ code: '<div onMouseOver={() => void 0} />;', errors: [ mouseOverError ], parserOptions }, | ||
{ code: '<div onMouseOut={() => void 0} />', errors: [ mouseOutError ], parserOptions }, | ||
{ code: '<div onMouseOver={() => void 0} onFocus={undefined} />;', errors: [ mouseOverError ], parserOptions }, | ||
{ code: '<div onMouseOut={() => void 0} onBlur={undefined} />', errors: [ mouseOutError ], parserOptions }, | ||
{ code: '<div onMouseOver={() => void 0} {...props} />', errors: [ mouseOverError ], parserOptions }, | ||
{ code: '<div onMouseOut={() => void 0} {...props} />', errors: [ mouseOutError ], parserOptions } | ||
] | ||
{ code: '<div onMouseOver={() => void 0} />;', errors: [mouseOverError], parserOptions }, | ||
{ code: '<div onMouseOut={() => void 0} />', errors: [mouseOutError], parserOptions }, | ||
{ | ||
code: '<div onMouseOver={() => void 0} onFocus={undefined} />;', | ||
errors: [mouseOverError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div onMouseOut={() => void 0} onBlur={undefined} />', | ||
errors: [mouseOutError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div onMouseOver={() => void 0} {...props} />', | ||
errors: [mouseOverError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div onMouseOut={() => void 0} {...props} />', | ||
errors: [mouseOutError], | ||
parserOptions, | ||
}, | ||
], | ||
}); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -19,4 +17,4 @@ // Requirements | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -34,3 +32,3 @@ | ||
'and keyboard only users create a11y complications.', | ||
type: 'JSXOpeningElement' | ||
type: 'JSXOpeningElement', | ||
}; | ||
@@ -44,15 +42,19 @@ | ||
{ code: '<div accessKey={`${undefined}`} />', parserOptions }, | ||
{ code: '<div accessKey={`${undefined}${undefined}`} />', parserOptions } | ||
{ code: '<div accessKey={`${undefined}${undefined}`} />', parserOptions }, | ||
], | ||
invalid: [ | ||
{ code: '<div accesskey="h" />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div accessKey="h" />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div accessKey="h" {...props} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div acCesSKeY="y" />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div accessKey={"y"} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div accessKey={`${y}`} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div accessKey={`${undefined}y${undefined}`} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div accessKey={`This is ${bad}`} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div accessKey={accessKey} />', errors: [ expectedError ], parserOptions } | ||
] | ||
{ code: '<div accesskey="h" />', errors: [expectedError], parserOptions }, | ||
{ code: '<div accessKey="h" />', errors: [expectedError], parserOptions }, | ||
{ code: '<div accessKey="h" {...props} />', errors: [expectedError], parserOptions }, | ||
{ code: '<div acCesSKeY="y" />', errors: [expectedError], parserOptions }, | ||
{ code: '<div accessKey={"y"} />', errors: [expectedError], parserOptions }, | ||
{ code: '<div accessKey={`${y}`} />', errors: [expectedError], parserOptions }, | ||
{ | ||
code: '<div accessKey={`${undefined}y${undefined}`} />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ code: '<div accessKey={`This is ${bad}`} />', errors: [expectedError], parserOptions }, | ||
{ code: '<div accessKey={accessKey} />', errors: [expectedError], parserOptions }, | ||
], | ||
}); |
@@ -6,3 +6,2 @@ /** | ||
'use strict'; | ||
@@ -16,7 +15,7 @@ // ----------------------------------------------------------------------------- | ||
const parserOptions = { | ||
const parserOptions = { | ||
ecmaVersion: 6, | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -33,3 +32,3 @@ | ||
'causes no negative consequences for keyboard only or screen reader users.', | ||
type: 'JSXOpeningElement' | ||
type: 'JSXOpeningElement', | ||
}; | ||
@@ -43,10 +42,10 @@ | ||
{ code: '<div onBlur={() => {}} onChange={() => {}} />;', parserOptions }, | ||
{ code: '<div {...props} />', parserOptions } | ||
{ code: '<div {...props} />', parserOptions }, | ||
], | ||
invalid: [ | ||
{ code: '<div onChange={() => {}} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div onChange={handleOnChange} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<input onChange={() => {}} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<input onChange={() => {}} {...props} />', errors: [ expectedError ], parserOptions } | ||
] | ||
{ code: '<div onChange={() => {}} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<div onChange={handleOnChange} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<input onChange={() => {}} />', errors: [expectedError], parserOptions }, | ||
{ code: '<input onChange={() => {}} {...props} />', errors: [expectedError], parserOptions }, | ||
], | ||
}); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -16,7 +14,7 @@ // Requirements | ||
const parserOptions = { | ||
const parserOptions = { | ||
ecmaVersion: 6, | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -30,7 +28,7 @@ | ||
const expectedError = { | ||
const expectedError = { | ||
message: 'Elements with onClick handlers must be focusable. ' + | ||
'Either set the tabIndex property to a valid value (usually 0), ' + | ||
'or use an element type which is inherently focusable such as `button`.', | ||
type: 'JSXOpeningElement' | ||
type: 'JSXOpeningElement', | ||
}; | ||
@@ -71,28 +69,71 @@ | ||
{ code: '<span onClick="doSomething();" tabIndex="-1">Click me too!</span>', parserOptions }, | ||
{ code: '<a href="javascript:void(0);" onClick="doSomething();">Click ALL the things!</a>', parserOptions }, | ||
{ | ||
code: '<a href="javascript:void(0);" onClick="doSomething();">Click ALL the things!</a>', | ||
parserOptions, | ||
}, | ||
{ code: '<Foo.Bar onClick={() => void 0} aria-hidden={false} />;', parserOptions }, | ||
{ code: '<Input onClick={() => void 0} type="hidden" />;', parserOptions } | ||
{ code: '<Input onClick={() => void 0} type="hidden" />;', parserOptions }, | ||
], | ||
invalid: [ | ||
{ code: '<span onClick="submitForm();">Submit</span>', errors: [ expectedError ], parserOptions }, | ||
{ code: '<span onClick="submitForm();" tabIndex={undefined}>Submit</span>', errors: [ expectedError ], parserOptions }, | ||
{ code: '<span onClick="submitForm();" tabIndex="bad">Submit</span>', errors: [ expectedError ], parserOptions }, | ||
{ code: '<a onClick="showNextPage();">Next page</a>', errors: [ expectedError ], parserOptions }, | ||
{ code: '<a onClick="showNextPage();" tabIndex={undefined}>Next page</a>', errors: [ expectedError ], parserOptions }, | ||
{ code: '<a onClick="showNextPage();" tabIndex="bad">Next page</a>', errors: [ expectedError ], parserOptions }, | ||
{ code: '<a onClick={() => void 0} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<area onClick={() => void 0} className="foo" />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div onClick={() => void 0} tabIndex={undefined} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div onClick={() => void 0} tabIndex="bad" />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div onClick={() => void 0} role={undefined} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div onClick={() => void 0} aria-hidden={false} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div onClick={() => void 0} {...props} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<section onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<main onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<article onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<header onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<footer onClick={() => void 0} />;', errors: [ expectedError ], parserOptions } | ||
] | ||
{ code: '<span onClick="submitForm();">Submit</span>', errors: [expectedError], parserOptions }, | ||
{ | ||
code: '<span onClick="submitForm();" tabIndex={undefined}>Submit</span>', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<span onClick="submitForm();" tabIndex="bad">Submit</span>', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ code: '<a onClick="showNextPage();">Next page</a>', errors: [expectedError], parserOptions }, | ||
{ | ||
code: '<a onClick="showNextPage();" tabIndex={undefined}>Next page</a>', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<a onClick="showNextPage();" tabIndex="bad">Next page</a>', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<a onClick={() => void 0} />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<area onClick={() => void 0} className="foo" />', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ code: '<div onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
{ | ||
code: '<div onClick={() => void 0} tabIndex={undefined} />;', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div onClick={() => void 0} tabIndex="bad" />;', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div onClick={() => void 0} role={undefined} />;', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div onClick={() => void 0} aria-hidden={false} />;', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ code: '<div onClick={() => void 0} {...props} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<section onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<main onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<article onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<header onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<footer onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
], | ||
}); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -16,7 +14,7 @@ // Requirements | ||
const parserOptions = { | ||
const parserOptions = { | ||
ecmaVersion: 6, | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -30,6 +28,6 @@ | ||
const expectedError = { | ||
const expectedError = { | ||
message: 'Visible, non-interactive elements with click ' + | ||
'handlers must have role attribute.', | ||
type: 'JSXOpeningElement' | ||
type: 'JSXOpeningElement', | ||
}; | ||
@@ -48,3 +46,6 @@ | ||
{ code: '<div onClick={() => void 0} role="button" aria-hidden={false} />;', parserOptions }, | ||
{ code: '<div onClick={() => void 0} role="button" aria-hidden={undefined} />;', parserOptions }, | ||
{ | ||
code: '<div onClick={() => void 0} role="button" aria-hidden={undefined} />;', | ||
parserOptions, | ||
}, | ||
{ code: '<input type="text" onClick={() => void 0} />', parserOptions }, | ||
@@ -62,16 +63,24 @@ { code: '<input onClick={() => void 0} />', parserOptions }, | ||
{ code: '<TestComponent onClick={doFoo} />', parserOptions }, | ||
{ code: '<Button onClick={doFoo} />', parserOptions } | ||
{ code: '<Button onClick={doFoo} />', parserOptions }, | ||
], | ||
invalid: [ | ||
{ code: '<div onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div onClick={() => void 0} role={undefined} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div onClick={() => void 0} {...props} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<section onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<main onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<article onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<header onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<footer onClick={() => void 0} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div onClick={() => void 0} aria-hidden={false} />;', errors: [ expectedError ], parserOptions }, | ||
{ code: '<a onClick={() => void 0} />', errors: [ expectedError ], parserOptions } | ||
] | ||
{ code: '<div onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
{ | ||
code: '<div onClick={() => void 0} role={undefined} />;', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ code: '<div onClick={() => void 0} {...props} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<section onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<main onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<article onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<header onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
{ code: '<footer onClick={() => void 0} />;', errors: [expectedError], parserOptions }, | ||
{ | ||
code: '<div onClick={() => void 0} aria-hidden={false} />;', | ||
errors: [expectedError], | ||
parserOptions, | ||
}, | ||
{ code: '<a onClick={() => void 0} />', errors: [expectedError], parserOptions }, | ||
], | ||
}); |
/** | ||
* @fileoverview Enforce that elements with ARIA roles must have all required attributes for that role. | ||
* @fileoverview Enforce that elements with ARIA roles must | ||
* have all required attributes for that role. | ||
* @author Ethan Cohen | ||
*/ | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -18,4 +17,4 @@ // Requirements | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -31,9 +30,13 @@ | ||
const errorMessage = role => ({ | ||
message: `Elements with the ARIA role "${role}" must have the following ` + | ||
`attributes defined: ${validRoleTypes[role.toUpperCase()].requiredProps.toString().toLowerCase()}`, | ||
type: 'JSXAttribute' | ||
}); | ||
const errorMessage = role => { | ||
const requiredProps = validRoleTypes[role.toUpperCase()].requiredProps.toString().toLowerCase(); | ||
return { | ||
message: `Elements with the ARIA role "${role}" must have the following ` + | ||
`attributes defined: ${requiredProps}`, | ||
type: 'JSXAttribute', | ||
}; | ||
}; | ||
// Create basic test cases using all valid role types. | ||
@@ -46,3 +49,3 @@ const basicValidityTests = Object.keys(validRoleTypes).map(role => { | ||
code: `<div role="${role.toLowerCase()}" ${propChain} />`, | ||
parserOptions | ||
parserOptions, | ||
}; | ||
@@ -60,4 +63,7 @@ }); | ||
{ code: '<div role="tabpanel row" />', parserOptions }, | ||
{ code: '<span role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0"></span>', parserOptions }, | ||
{ code: '<Bar baz />', parserOptions } | ||
{ | ||
code: '<span role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0"></span>', | ||
parserOptions, | ||
}, | ||
{ code: '<Bar baz />', parserOptions }, | ||
].concat(basicValidityTests), | ||
@@ -67,37 +73,93 @@ | ||
// SLIDER | ||
{ code: '<div role="slider" />', errors: [ errorMessage('slider') ], parserOptions }, | ||
{ code: '<div role="slider" aria-valuemax />', errors: [ errorMessage('slider') ], parserOptions }, | ||
{ code: '<div role="slider" aria-valuemax aria-valuemin />', errors: [ errorMessage('slider') ], parserOptions }, | ||
{ code: '<div role="slider" aria-valuemax aria-valuenow />', errors: [ errorMessage('slider') ], parserOptions }, | ||
{ code: '<div role="slider" aria-valuemin aria-valuenow />', errors: [ errorMessage('slider') ], parserOptions }, | ||
{ code: '<div role="slider" />', errors: [errorMessage('slider')], parserOptions }, | ||
{ | ||
code: '<div role="slider" aria-valuemax />', | ||
errors: [errorMessage('slider')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div role="slider" aria-valuemax aria-valuemin />', | ||
errors: [errorMessage('slider')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div role="slider" aria-valuemax aria-valuenow />', | ||
errors: [errorMessage('slider')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div role="slider" aria-valuemin aria-valuenow />', | ||
errors: [errorMessage('slider')], | ||
parserOptions, | ||
}, | ||
// SPINBUTTON | ||
{ code: '<div role="spinbutton" />', errors: [ errorMessage('spinbutton') ], parserOptions }, | ||
{ code: '<div role="spinbutton" aria-valuemax />', errors: [ errorMessage('spinbutton') ], parserOptions }, | ||
{ code: '<div role="spinbutton" aria-valuemax aria-valuemin />', errors: [ errorMessage('spinbutton') ], parserOptions }, | ||
{ code: '<div role="spinbutton" aria-valuemax aria-valuenow />', errors: [ errorMessage('spinbutton') ], parserOptions }, | ||
{ code: '<div role="spinbutton" aria-valuemin aria-valuenow />', errors: [ errorMessage('spinbutton') ], parserOptions }, | ||
{ code: '<div role="spinbutton" />', errors: [errorMessage('spinbutton')], parserOptions }, | ||
{ | ||
code: '<div role="spinbutton" aria-valuemax />', | ||
errors: [errorMessage('spinbutton')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div role="spinbutton" aria-valuemax aria-valuemin />', | ||
errors: [errorMessage('spinbutton')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div role="spinbutton" aria-valuemax aria-valuenow />', | ||
errors: [errorMessage('spinbutton')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div role="spinbutton" aria-valuemin aria-valuenow />', | ||
errors: [errorMessage('spinbutton')], | ||
parserOptions, | ||
}, | ||
// CHECKBOX | ||
{ code: '<div role="checkbox" />', errors: [ errorMessage('checkbox') ], parserOptions }, | ||
{ code: '<div role="checkbox" checked />', errors: [ errorMessage('checkbox') ], parserOptions }, | ||
{ code: '<div role="checkbox" aria-chcked />', errors: [ errorMessage('checkbox') ], parserOptions }, | ||
{ code: '<div role="checkbox" />', errors: [errorMessage('checkbox')], parserOptions }, | ||
{ code: '<div role="checkbox" checked />', errors: [errorMessage('checkbox')], parserOptions }, | ||
{ | ||
code: '<div role="checkbox" aria-chcked />', | ||
errors: [errorMessage('checkbox')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<span role="checkbox" aria-labelledby="foo" tabindex="0"></span>', | ||
errors: [ errorMessage('checkbox') ], | ||
parserOptions | ||
errors: [errorMessage('checkbox')], | ||
parserOptions, | ||
}, | ||
// COMBOBOX | ||
{ code: '<div role="combobox" />', errors: [ errorMessage('combobox') ], parserOptions }, | ||
{ code: '<div role="combobox" expanded />', errors: [ errorMessage('combobox') ], parserOptions }, | ||
{ code: '<div role="combobox" aria-expandd />', errors: [ errorMessage('combobox') ], parserOptions }, | ||
{ code: '<div role="combobox" />', errors: [errorMessage('combobox')], parserOptions }, | ||
{ code: '<div role="combobox" expanded />', errors: [errorMessage('combobox')], parserOptions }, | ||
{ | ||
code: '<div role="combobox" aria-expandd />', | ||
errors: [errorMessage('combobox')], | ||
parserOptions, | ||
}, | ||
// SCROLLBAR | ||
{ code: '<div role="scrollbar" />', errors: [ errorMessage('scrollbar') ], parserOptions }, | ||
{ code: '<div role="scrollbar" aria-valuemax />', errors: [ errorMessage('scrollbar') ], parserOptions }, | ||
{ code: '<div role="scrollbar" aria-valuemax aria-valuemin />', errors: [ errorMessage('scrollbar') ], parserOptions }, | ||
{ code: '<div role="scrollbar" aria-valuemax aria-valuenow />', errors: [ errorMessage('scrollbar') ], parserOptions }, | ||
{ code: '<div role="scrollbar" aria-valuemin aria-valuenow />', errors: [ errorMessage('scrollbar') ], parserOptions } | ||
] | ||
{ code: '<div role="scrollbar" />', errors: [errorMessage('scrollbar')], parserOptions }, | ||
{ | ||
code: '<div role="scrollbar" aria-valuemax />', | ||
errors: [errorMessage('scrollbar')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div role="scrollbar" aria-valuemax aria-valuemin />', | ||
errors: [errorMessage('scrollbar')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div role="scrollbar" aria-valuemax aria-valuenow />', | ||
errors: [errorMessage('scrollbar')], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<div role="scrollbar" aria-valuemin aria-valuenow />', | ||
errors: [errorMessage('scrollbar')], | ||
parserOptions, | ||
}, | ||
], | ||
}); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -15,2 +13,4 @@ // Requirements | ||
import { RuleTester } from 'eslint'; | ||
import ROLES from '../../../src/util/attributes/role'; | ||
import ARIA from '../../../src/util/attributes/ARIA'; | ||
@@ -20,4 +20,4 @@ const parserOptions = { | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -33,3 +33,4 @@ | ||
if (isImplicit) { | ||
return `The attribute ${attr} is not supported by the role ${role}. This role is implicit on the element ${tag}.`; | ||
return `The attribute ${attr} is not supported by the role ${role}. \ | ||
This role is implicit on the element ${tag}.`; | ||
} | ||
@@ -42,8 +43,5 @@ | ||
message: generateErrorMessage(attr, role, tag, isImplicit), | ||
type: 'JSXOpeningElement' | ||
type: 'JSXOpeningElement', | ||
}); | ||
import ROLES from '../../../src/util/attributes/role'; | ||
import ARIA from '../../../src/util/attributes/ARIA'; | ||
const nonAbstractRoles = Object.keys(ROLES).filter(role => ROLES[role].abstract === false); | ||
@@ -53,22 +51,24 @@ | ||
const validPropsForRole = ROLES[role.toUpperCase()].props; | ||
const invalidPropsForRole = Object.keys(ARIA).filter(attribute => validPropsForRole.indexOf(attribute) === -1); | ||
const invalidPropsForRole = Object.keys(ARIA) | ||
.filter(attribute => validPropsForRole.indexOf(attribute) === -1); | ||
const normalRole = role.toLowerCase(); | ||
tests[0] = tests[0].concat(validPropsForRole.map(prop => ({ | ||
const allTests = []; | ||
allTests[0] = tests[0].concat(validPropsForRole.map(prop => ({ | ||
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`, | ||
parserOptions | ||
parserOptions, | ||
}))); | ||
tests[1] = tests[1].concat(invalidPropsForRole.map(prop => ({ | ||
allTests[1] = tests[1].concat(invalidPropsForRole.map(prop => ({ | ||
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`, | ||
parserOptions, | ||
errors: [ errorMessage(prop.toLowerCase(), normalRole, 'div', false) ] | ||
errors: [errorMessage(prop.toLowerCase(), normalRole, 'div', false)], | ||
}))); | ||
return tests; | ||
return allTests; | ||
}, [[], []]); | ||
}, [ [], [] ]); | ||
const [validTests, invalidTests] = createTests(nonAbstractRoles); | ||
const [ validTests, invalidTests ] = createTests(nonAbstractRoles); | ||
ruleTester.run('role-supports-aria-props', rule, { | ||
@@ -79,2 +79,3 @@ valid: [ | ||
{ code: '<div id="main" />', parserOptions }, | ||
{ code: '<div role="presentation" {...props} />', parserOptions }, | ||
{ code: '<Foo.Bar baz={true} />', parserOptions }, | ||
@@ -150,18 +151,18 @@ | ||
// IMG TESTS - implicit role is `presentation` | ||
{ code: '<img alt="" aria-atomic />', parserOptions }, | ||
{ code: '<img alt="" aria-busy />', parserOptions }, | ||
{ code: '<img alt="" aria-controls />', parserOptions }, | ||
{ code: '<img alt="" aria-describedby />', parserOptions }, | ||
{ code: '<img alt="" aria-disabled />', parserOptions }, | ||
{ code: '<img alt="" aria-dropeffect />', parserOptions }, | ||
{ code: '<img alt="" aria-flowto />', parserOptions }, | ||
{ code: '<img alt="" aria-grabbed />', parserOptions }, | ||
{ code: '<img alt="" aria-haspopup />', parserOptions }, | ||
{ code: '<img alt="" aria-hidden />', parserOptions }, | ||
{ code: '<img alt="" aria-invalid />', parserOptions }, | ||
{ code: '<img alt="" aria-label />', parserOptions }, | ||
{ code: '<img alt="" aria-labelledby />', parserOptions }, | ||
{ code: '<img alt="" aria-live />', parserOptions }, | ||
{ code: '<img alt="" aria-owns />', parserOptions }, | ||
{ code: '<img alt="" aria-relevant />', parserOptions }, | ||
{ code: '<img alt="" aria-atomic />', parserOptions }, | ||
{ code: '<img alt="" aria-busy />', parserOptions }, | ||
{ code: '<img alt="" aria-controls />', parserOptions }, | ||
{ code: '<img alt="" aria-describedby />', parserOptions }, | ||
{ code: '<img alt="" aria-disabled />', parserOptions }, | ||
{ code: '<img alt="" aria-dropeffect />', parserOptions }, | ||
{ code: '<img alt="" aria-flowto />', parserOptions }, | ||
{ code: '<img alt="" aria-grabbed />', parserOptions }, | ||
{ code: '<img alt="" aria-haspopup />', parserOptions }, | ||
{ code: '<img alt="" aria-hidden />', parserOptions }, | ||
{ code: '<img alt="" aria-invalid />', parserOptions }, | ||
{ code: '<img alt="" aria-label />', parserOptions }, | ||
{ code: '<img alt="" aria-labelledby />', parserOptions }, | ||
{ code: '<img alt="" aria-live />', parserOptions }, | ||
{ code: '<img alt="" aria-owns />', parserOptions }, | ||
{ code: '<img alt="" aria-relevant />', parserOptions }, | ||
@@ -433,3 +434,3 @@ // this will have role of `img` | ||
{ code: '<thead aria-expanded />', parserOptions }, | ||
{ code: '<ul aria-expanded />', parserOptions } | ||
{ code: '<ul aria-expanded />', parserOptions }, | ||
@@ -442,31 +443,31 @@ ].concat(validTests), | ||
code: '<a href="#" aria-checked />', | ||
errors: [ errorMessage('aria-checked', 'link', 'a', true) ], | ||
parserOptions | ||
errors: [errorMessage('aria-checked', 'link', 'a', true)], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<area href="#" aria-checked />', | ||
errors: [ errorMessage('aria-checked', 'link', 'area', true) ], | ||
parserOptions | ||
errors: [errorMessage('aria-checked', 'link', 'area', true)], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<link href="#" aria-checked />', | ||
errors: [ errorMessage('aria-checked', 'link', 'link', true) ], | ||
parserOptions | ||
errors: [errorMessage('aria-checked', 'link', 'link', true)], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<img alt="" aria-checked />', | ||
errors: [ errorMessage('aria-checked', 'presentation', 'img', true) ], | ||
parserOptions | ||
errors: [errorMessage('aria-checked', 'presentation', 'img', true)], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<menu type="toolbar" aria-checked />', | ||
errors: [ errorMessage('aria-checked', 'toolbar', 'menu', true) ], | ||
parserOptions | ||
errors: [errorMessage('aria-checked', 'toolbar', 'menu', true)], | ||
parserOptions, | ||
}, | ||
{ | ||
code: '<aside aria-checked />', | ||
errors: [ errorMessage('aria-checked', 'complementary', 'aside', true) ], | ||
parserOptions | ||
} | ||
].concat(invalidTests) | ||
errors: [errorMessage('aria-checked', 'complementary', 'aside', true)], | ||
parserOptions, | ||
}, | ||
].concat(invalidTests), | ||
}); |
@@ -6,4 +6,2 @@ /** | ||
'use strict'; | ||
// ----------------------------------------------------------------------------- | ||
@@ -19,4 +17,4 @@ // Requirements | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
jsx: true, | ||
}, | ||
}; | ||
@@ -32,3 +30,3 @@ | ||
message: 'Avoid positive integer values for tabIndex.', | ||
type: 'JSXAttribute' | ||
type: 'JSXAttribute', | ||
}; | ||
@@ -55,12 +53,12 @@ | ||
{ code: '<div tabIndex={-5.5} />', parserOptions }, | ||
{ code: '<div tabIndex={-5} />', parserOptions } | ||
{ code: '<div tabIndex={-5} />', parserOptions }, | ||
], | ||
invalid: [ | ||
{ code: '<div tabIndex="1" />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div tabIndex={1} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div tabIndex={"1"} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div tabIndex={`1`} />', errors: [ expectedError ], parserOptions }, | ||
{ code: '<div tabIndex={1.589} />', errors: [ expectedError ], parserOptions } | ||
] | ||
{ code: '<div tabIndex="1" />', errors: [expectedError], parserOptions }, | ||
{ code: '<div tabIndex={1} />', errors: [expectedError], parserOptions }, | ||
{ code: '<div tabIndex={"1"} />', errors: [expectedError], parserOptions }, | ||
{ code: '<div tabIndex={`1`} />', errors: [expectedError], parserOptions }, | ||
{ code: '<div tabIndex={1.589} />', errors: [expectedError], parserOptions }, | ||
], | ||
}); |
/* eslint-env mocha */ | ||
'use strict'; | ||
import assert from 'assert'; | ||
@@ -26,4 +24,4 @@ import getSuggestion from '../../../src/util/getSuggestion'; | ||
const word = 'fo'; | ||
const dictionary = [ 'foo', 'bar', 'baz' ]; | ||
const expected = [ 'foo' ]; | ||
const dictionary = ['foo', 'bar', 'baz']; | ||
const expected = ['foo']; | ||
const actual = getSuggestion(word, dictionary); | ||
@@ -36,4 +34,4 @@ | ||
const word = 'theer'; | ||
const dictionary = [ 'there', 'their', 'foo', 'bar' ]; | ||
const expected = [ 'their', 'there' ]; | ||
const dictionary = ['there', 'their', 'foo', 'bar']; | ||
const expected = ['their', 'there']; | ||
const actual = getSuggestion(word, dictionary); | ||
@@ -44,7 +42,8 @@ | ||
it('should return one correct suggestion given real word and a dictionary and a limit of 1', () => { | ||
it('should return correct # of suggestions given the limit argument', () => { | ||
const word = 'theer'; | ||
const dictionary = [ 'there', 'their', 'foo', 'bar' ]; | ||
const expected = [ 'their' ]; | ||
const actual = getSuggestion(word, dictionary, 1); | ||
const dictionary = ['there', 'their', 'foo', 'bar']; | ||
const limit = 1; | ||
const expected = 1; | ||
const actual = getSuggestion(word, dictionary, limit).length; | ||
@@ -51,0 +50,0 @@ assert.deepEqual(expected, actual); |
336418
10698
11