` ` div>` elements:
+ 'advise', 'amplify', 'apply', 'arrange', 'ask',
+ 'boost', 'build',
+ 'call', 'click', 'close', 'commit', 'consult', 'compile', 'collect', 'contribute', 'create', 'cut',
+ 'decrease', 'delete', 'divide', 'drink',
+ 'eat', 'earn', 'enable', 'enter', 'execute', 'exit', 'expand', 'explain',
+ 'finish', 'forecast', 'fix',
+ 'generate',
+ 'handle', 'help', 'hire', 'hit',
+ 'improve', 'increase',
+ 'join', 'jump',
+ 'leave', 'let\'/s', 'list', 'listen',
+ 'magnify', 'make', 'manage', 'minimize', 'move',
+ 'ok', 'open', 'organise', 'oversee',
+ 'play', 'push',
+ 'read', 'reduce', 'run',
+ 'save', 'send', 'shout', 'sing', 'submit', 'support',
+ 'talk', 'trim',
+ 'visit', 'volunteer', 'vote',
+ 'watch', 'win', 'write',
+
+This rule takes one optional object argument of type object:
+
+```json
+{
+ "rules": {
+ "jsx-a11y/heading-has-content": [ 2, {
+ "components": [ "Apply" ],
+ }],
+ }
+}
+```
+For the `components` option, these strings determine which JSX elements (**always including** `
`) should be checked for having call to action verbs content. This is a good use case when you have a wrapper component that simply renders a `button` element (like in React):
+
+
+```js
+// Apply.js
+const Apply = props => {
+ return (
+
{ props.children }
+ );
+}
+
+...
+
+// CreateForm.js (for example)
+...
+return (
+
Apply
+);
+```
+
+### Succeed
+```jsx
+
+
// empty div is allowed
+
orange
// no action word within div element
+
orange
// any word within div that has the two attributes
+
advise
// any of the action words e.g.: advise within div that has the two attributes
+
advise // If custom element is an action word then the text within it should also be an action word.
+```
+
+### Fail
+```jsx
+// when a call to action verb is between div elements:
+
+//both attributes are missing and/or wrong
+
submit
// both attributes are missing
+
apply
// both attributes are undefined
+
apply
// both attributes values are wrong
+
apply
// both attributes values are wrong
+
apply
// wrong attribute value and missing attribute
+
apply
// wrong attribute value and missing attribute
+
+// tabindex is missing or wrong
+
apply
+
apply
+
apply
+
apply
+
apply
+
+// role is missing or wrong
+
apply
+
apply
+
apply
+
apply
+
apply
+
+// custom element name is an action verb and the text in between too
+
apply
+```
+
+
+## Accessibility guidelines
+- [WCAG 2.4.7](https://www.w3.org/TR/UNDERSTANDING-WCAG20/navigation-mechanisms-focus-visible.html)
+
+### Resources
+- [axe-core, focus-order-semantics](https://dequeuniversity.com/rules/axe/3.2/focus-order-semantics)
diff --git a/docs/rules/div-has-content.md b/docs/rules/div-has-content.md
new file mode 100644
index 000000000..2896a4ab1
--- /dev/null
+++ b/docs/rules/div-has-content.md
@@ -0,0 +1,21 @@
+# div-has-content
+
+Write a useful explanation here!
+
+### References
+
+ 1.
+
+## Rule details
+
+This rule takes no arguments.
+
+### Succeed
+```jsx
+
+```
+
+### Fail
+```jsx
+
+```
diff --git a/package.json b/package.json
index cebde0124..9ff5bcd94 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "eslint-plugin-jsx-a11y",
+ "name": "eslint-plugin-jsx-a11y-div-seventyone",
"version": "6.4.1",
"description": "Static AST checker for accessibility rules on JSX elements.",
"keywords": [
@@ -23,7 +23,7 @@
"lint:fix": "npm run lint -- --fix",
"lint": "eslint --config .eslintrc src __tests__ __mocks__ scripts",
"prepublish": "safe-publish-latest && not-in-publish || (npm run lint && npm run flow && npm run jest && npm run build)",
- "pretest": "npm run lint:fix && npm run flow",
+ "pretest": "npm run lint:fix",
"test": "npm run jest",
"posttest": "aud --production",
"test:ci": "npm run jest -- --ci --runInBand",
@@ -69,7 +69,8 @@
"has": "^1.0.3",
"jsx-ast-utils": "^3.2.0",
"language-tags": "^1.0.5",
- "minimatch": "^3.0.4"
+ "minimatch": "^3.0.4",
+ "proptypes": "^1.1.0"
},
"peerDependencies": {
"eslint": "^3 || ^4 || ^5 || ^6 || ^7"
diff --git a/src/index.js b/src/index.js
index ee4a6ef64..03c3a5e40 100644
--- a/src/index.js
+++ b/src/index.js
@@ -15,6 +15,8 @@ module.exports = {
'click-events-have-key-events': require('./rules/click-events-have-key-events'),
'control-has-associated-label': require('./rules/control-has-associated-label'),
'heading-has-content': require('./rules/heading-has-content'),
+ 'div-has-content': require('./rules/div-has-content'),
+ 'div-has-apply': require('./rules/div-has-apply'),
'html-has-lang': require('./rules/html-has-lang'),
'iframe-has-title': require('./rules/iframe-has-title'),
'img-redundant-alt': require('./rules/img-redundant-alt'),
@@ -91,6 +93,8 @@ module.exports = {
},
],
'jsx-a11y/heading-has-content': 'error',
+ 'jsx-a11y/div-has-content': 'error',
+ 'jsx-a11y/div-has-apply': 'error',
'jsx-a11y/html-has-lang': 'error',
'jsx-a11y/iframe-has-title': 'error',
'jsx-a11y/img-redundant-alt': 'error',
@@ -245,6 +249,8 @@ module.exports = {
],
}],
'jsx-a11y/heading-has-content': 'error',
+ 'jsx-a11y/div-has-content': 'error',
+ 'jsx-a11y/div-has-apply': 'error',
'jsx-a11y/html-has-lang': 'error',
'jsx-a11y/iframe-has-title': 'error',
'jsx-a11y/img-redundant-alt': 'error',
diff --git a/src/rules/div-has-apply.js b/src/rules/div-has-apply.js
new file mode 100644
index 000000000..7ff955b32
--- /dev/null
+++ b/src/rules/div-has-apply.js
@@ -0,0 +1,108 @@
+/**
+ * @fileoverview Discourage use of div when text is an action word
+ * @author Felicia Kovacs
+ */
+
+// ----------------------------------------------------------------------------
+// Rule Definition
+// ----------------------------------------------------------------------------
+
+import {
+ elementType,
+ getProp,
+ getPropValue,
+} from 'jsx-ast-utils';
+import { generateObjSchema, arraySchema } from '../util/schemas';
+
+// random list of action verbs in alphabetical order
+const actionVerbs = [
+ 'advise', 'amplify', 'apply', 'arrange', 'ask',
+ 'boost', 'build',
+ 'call', 'click', 'close', 'commit', 'consult', 'compile', 'collect', 'contribute', 'create', 'cut',
+ 'decrease', 'delete', 'divide', 'drink',
+ 'eat', 'earn', 'enable', 'enter', 'execute', 'exit', 'expand', 'explain',
+ 'finish', 'forecast', 'fix',
+ 'generate',
+ 'handle', 'help', 'hire', 'hit',
+ 'improve', 'increase',
+ 'join', 'jump',
+ 'leave', 'let\'/s', 'list', 'listen',
+ 'magnify', 'make', 'manage', 'minimize', 'move',
+ 'ok', 'open', 'organise', 'oversee',
+ 'play', 'push',
+ 'read', 'reduce', 'run',
+ 'save', 'send', 'shout', 'sing', 'submit', 'support',
+ 'talk', 'trim',
+ 'visit', 'volunteer', 'vote',
+ 'watch', 'win', 'write',
+];
+const schema = generateObjSchema({ components: arraySchema });
+
+module.exports = {
+ meta: {
+ docs: {},
+ schema: [schema],
+ },
+
+ create: (context) => ({
+ JSXOpeningElement: (node) => {
+ const TextChildValue = node.parent.children.find((child) => child.type === 'Literal' || child.type === 'JSXText' || child.type === 'Unknown'); // text within div elements
+ const options = context.options[0] || {}; // returns e.g.: [object Object]
+ const componentOptions = options.components || []; // returns e.g.: Apply - comming from .eslintrc.js file
+ const typeCheck = ['div'].concat(componentOptions); // returns e.g.: the string div, Apply
+ const nodeType = elementType(node); // returns e.g.: Apply
+
+ // Only check 'div*' elements and custom types.
+ // for example, is the Apply custom component present in div,Apply
+ // answers the question: is the current node, which is Apply is defined in the componentOptions in the eslintrc.json file?
+ if (typeCheck.indexOf(nodeType) === -1) {
+ return;
+ }
+
+ if ((actionVerbs.includes(nodeType.toLowerCase()) || nodeType.toLowerCase() === 'button') === true) {
+ if (actionVerbs.includes(TextChildValue && TextChildValue.value.toLowerCase()) === false) {
+ context.report({
+ node,
+ message: 'If custom element is an action word then the text within it should also be an action word. Action verbs should be contained preferably within a native HTML button element(see first rule of ARIA) or within a div element that has tabIndex="0" attribute and role="button" aria role. Refer to https://w3c.github.io/aria-practices/examples/button/button.html and https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#accessibility_concerns',
+ });
+ return;
+ }
+ }
+
+ if (actionVerbs.includes(TextChildValue && TextChildValue.value.toLowerCase()) === false) {
+ return;
+ }
+
+ const tabindexProp = getProp(node.attributes, 'tabIndex');
+ const roleProp = getProp(node.attributes, 'role');
+ const tabindexValue = getPropValue(tabindexProp);
+ const roleValue = getPropValue(roleProp);
+ // Missing and/ or incorrect tabindex and role attributes
+ if (((tabindexProp === undefined) && (roleProp === undefined)) || ((tabindexValue !== '0') && (roleValue !== 'button'))
+ || ((tabindexProp === undefined) && (roleValue !== 'button')) || ((tabindexValue !== '0') && (roleProp === undefined))) {
+ context.report({
+ node,
+ message: 'Missing and/or incorrect attributes. Action verbs should be contained preferably within a native HTML button element(see first rule of ARIA) or within a div element that has tabIndex="0" attribute and role="button" aria role. Refer to https://w3c.github.io/aria-practices/examples/button/button.html and https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#accessibility_concerns',
+ });
+ return;
+ }
+
+ // Missing and/or incorrect tabindex attribute
+ if ((tabindexValue !== '0') || (tabindexProp === undefined)) {
+ context.report({
+ node,
+ message: 'Missing or incorrect role attribute value. Action verbs should be contained preferably within a native HTML button element(see first rule of ARIA) or within a div element that has tabIndex="0" attribute and role="button" aria role. Refer to https://w3c.github.io/aria-practices/examples/button/button.html and https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#accessibility_concerns',
+ });
+ return;
+ }
+
+ // Missing and/or incorrect role attribute
+ if ((roleValue !== 'button') || (roleProp === undefined)) {
+ context.report({
+ node,
+ message: 'Missing or incorrect role value. Action verbs should be contained preferably within a native HTML button element(see first rule of ARIA) or within a div element that has tabIndex="0" attribute and role="button" aria role. Refer to https://w3c.github.io/aria-practices/examples/button/button.html and https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#accessibility_concerns',
+ });
+ }
+ },
+ }),
+};
diff --git a/src/rules/div-has-content.js b/src/rules/div-has-content.js
new file mode 100644
index 000000000..7d02392da
--- /dev/null
+++ b/src/rules/div-has-content.js
@@ -0,0 +1,54 @@
+/**
+ * @fileoverview check if div has content
+ * @author Felicia
+ * @flow
+ */
+
+// ----------------------------------------------------------------------------
+// Rule Definition
+// ----------------------------------------------------------------------------
+
+import { elementType } from 'jsx-ast-utils';
+// import type { JSXOpeningElement } from 'ast-types-flow';
+import { generateObjSchema, arraySchema } from '../util/schemas';
+import hasAccessibleChild from '../util/hasAccessibleChild';
+import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
+
+const errorMessage = 'Div must have content and the content must be accessible by a screen reader.';
+
+const headings = [
+ 'div',
+];
+
+const schema = generateObjSchema({ components: arraySchema });
+
+module.exports = {
+ meta: {
+ docs: {},
+ schema: [schema],
+ },
+
+ create: (context) => ({
+ JSXOpeningElement: (node) => {
+ const options = context.options[0] || {};
+ const componentOptions = options.components || [];
+ const typeCheck = headings.concat(componentOptions);
+ const nodeType = elementType(node);
+
+ // Only check 'div*' elements and custom types.
+ if (typeCheck.indexOf(nodeType) === -1) {
+ return;
+ }
+ if (hasAccessibleChild(node.parent)) {
+ return;
+ }
+ if (isHiddenFromScreenReader(nodeType, node.attributes)) {
+ return;
+ }
+ context.report({
+ node,
+ message: errorMessage,
+ });
+ },
+ }),
+};
diff --git a/src/util/hasApplyText.js b/src/util/hasApplyText.js
new file mode 100644
index 000000000..f67f89694
--- /dev/null
+++ b/src/util/hasApplyText.js
@@ -0,0 +1,28 @@
+// @flow
+
+import { elementType, hasAnyProp } from 'jsx-ast-utils';
+import type { JSXElement } from 'ast-types-flow';
+import isHiddenFromScreenReader from './isHiddenFromScreenReader';
+
+export default function hasApplyText(node: JSXElement): boolean {
+ return node.children.some((child) => {
+ switch (child.type) {
+ case 'Literal':
+ case 'JSXText':
+ // return (Boolean(child.value) && Boolean(child.value.toUpperCase() === 'APPLY'));
+ return Boolean(child.value);
+ case 'JSXElement':
+ return !isHiddenFromScreenReader(
+ elementType(child.openingElement),
+ child.openingElement.attributes,
+ );
+ case 'JSXExpressionContainer':
+ if (child.expression.type === 'Identifier') {
+ return child.expression.name !== 'undefined';
+ }
+ return true;
+ default:
+ return false;
+ }
+ }) || hasAnyProp(node.openingElement.attributes, ['dangerouslySetInnerHTML', 'children']);
+}