diff --git a/README.md b/README.md
index 70e8b49ab..283f0d397 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,9 @@ Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`:
"CustomButton": "button",
"MyButton": "button",
"RoundButton": "button"
+ },
+ "attributes": {
+ "for": ["htmlFor", "for"]
}
}
}
@@ -202,6 +205,11 @@ module.exports = [
To enable your custom components to be checked as DOM elements, you can set global settings in your configuration file by mapping each custom component name to a DOM element type.
+#### Attribute Mapping
+
+To configure the JSX property to use for attribute checking, you can set global settings in your configuration file by mapping each DOM attribute to the JSX property you want to check.
+For example, you may want to allow the `for` attribute in addition to the `htmlFor` attribute for checking label associations.
+
#### Polymorphic Components
You can optionally use the `polymorphicPropName` setting to define the prop your code uses to create polymorphic components.
diff --git a/__tests__/src/rules/label-has-associated-control-test.js b/__tests__/src/rules/label-has-associated-control-test.js
index c765d9713..6db067394 100644
--- a/__tests__/src/rules/label-has-associated-control-test.js
+++ b/__tests__/src/rules/label-has-associated-control-test.js
@@ -40,11 +40,23 @@ const componentsSettings = {
},
};
+const attributesSettings = {
+ 'jsx-a11y': {
+ attributes: {
+ for: ['htmlFor', 'for'],
+ },
+ },
+};
+
const htmlForValid = [
{ code: 'A label ', options: [{ depth: 4 }] },
{ code: ' ' },
{ code: ' ' },
{ code: '
A label
' },
+ { code: 'A label ', options: [{ depth: 4 }], settings: attributesSettings },
+ { code: ' ', settings: attributesSettings },
+ { code: ' ', settings: attributesSettings },
+ { code: 'A label
', settings: attributesSettings },
// Custom label component.
{ code: ' ', options: [{ labelComponents: ['CustomLabel'] }] },
{ code: ' ', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },
diff --git a/__tests__/src/rules/label-has-for-test.js b/__tests__/src/rules/label-has-for-test.js
index 9b4bd1478..93181dcc9 100644
--- a/__tests__/src/rules/label-has-for-test.js
+++ b/__tests__/src/rules/label-has-for-test.js
@@ -50,16 +50,28 @@ const optionsChildrenAllowed = [{
allowChildren: true,
}];
+const attributesSettings = {
+ 'jsx-a11y': {
+ attributes: {
+ for: ['htmlFor', 'for'],
+ },
+ },
+};
+
ruleTester.run('label-has-for', rule, {
valid: parsers.all([].concat(
// DEFAULT ELEMENT 'label' TESTS
{ code: '
' },
{ code: ' ' },
{ code: ' ' },
+ { code: ' ', settings: attributesSettings },
+ { code: ' ', settings: attributesSettings },
{ code: ' ' }, // lower-case convention refers to real HTML elements.
{ code: ' ' },
+ { code: ' ', settings: attributesSettings },
{ code: ' ' },
{ code: 'Test! ' },
+ { code: 'Test! ', settings: attributesSettings },
{ code: 'test ' },
// CUSTOM ELEMENT ARRAY OPTION TESTS
diff --git a/docs/rules/label-has-for.md b/docs/rules/label-has-for.md
index 71730c998..87e6f5332 100644
--- a/docs/rules/label-has-for.md
+++ b/docs/rules/label-has-for.md
@@ -13,7 +13,7 @@ Enforce label tags have associated control.
There are two supported ways to associate a label with a control:
- nesting: by wrapping a control in a label tag
-- id: by using the prop `htmlFor` as in `htmlFor=[ID of control]`
+- id: by using the prop `htmlFor` (or any configured attribute) as in `htmlFor=[ID of control]`
To fully cover 100% of assistive devices, you're encouraged to validate for both nesting and id.
diff --git a/flow/eslint.js b/flow/eslint.js
index e91291fc0..670d1ab1d 100644
--- a/flow/eslint.js
+++ b/flow/eslint.js
@@ -10,7 +10,8 @@ export type ESLintSettings = {
[string]: mixed,
'jsx-a11y'?: {
polymorphicPropName?: string,
- components?: {[string]: string},
+ components?: { [string]: string },
+ attributes?: { for?: string[] },
},
}
diff --git a/src/rules/label-has-associated-control.js b/src/rules/label-has-associated-control.js
index 5646859c7..8920e64ed 100644
--- a/src/rules/label-has-associated-control.js
+++ b/src/rules/label-has-associated-control.js
@@ -9,7 +9,7 @@
// Rule Definition
// ----------------------------------------------------------------------------
-import { getProp, getPropValue } from 'jsx-ast-utils';
+import { hasProp, getProp, getPropValue } from 'jsx-ast-utils';
import type { JSXElement } from 'ast-types-flow';
import { generateObjSchema, arraySchema } from '../util/schemas';
import type { ESLintConfig, ESLintContext, ESLintVisitorSelectorConfig } from '../../flow/eslint';
@@ -36,12 +36,22 @@ const schema = generateObjSchema({
},
});
-const validateId = (node) => {
- const htmlForAttr = getProp(node.attributes, 'htmlFor');
- const htmlForValue = getPropValue(htmlForAttr);
+function validateID(node, context) {
+ const { settings } = context;
+ const htmlForAttributes = settings['jsx-a11y']?.attributes?.for ?? ['htmlFor'];
- return htmlForAttr !== false && !!htmlForValue;
-};
+ for (let i = 0; i < htmlForAttributes.length; i += 1) {
+ const attribute = htmlForAttributes[i];
+ if (hasProp(node.attributes, attribute)) {
+ const htmlForAttr = getProp(node.attributes, attribute);
+ const htmlForValue = getPropValue(htmlForAttr);
+
+ return htmlForAttr !== false && !!htmlForValue;
+ }
+ }
+
+ return false;
+}
export default ({
meta: {
@@ -76,7 +86,7 @@ export default ({
options.depth === undefined ? 2 : options.depth,
25,
);
- const hasLabelId = validateId(node.openingElement);
+ const hasLabelId = validateID(node.openingElement, context);
// Check for multiple control components.
const hasNestedControl = controlComponents.some((name) => mayContainChildComponent(
node,
diff --git a/src/rules/label-has-for.js b/src/rules/label-has-for.js
index dd7ff0627..46081c977 100644
--- a/src/rules/label-has-for.js
+++ b/src/rules/label-has-for.js
@@ -7,7 +7,7 @@
// Rule Definition
// ----------------------------------------------------------------------------
-import { getProp, getPropValue } from 'jsx-ast-utils';
+import { hasProp, getProp, getPropValue } from 'jsx-ast-utils';
import { generateObjSchema, arraySchema, enumArraySchema } from '../util/schemas';
import getElementType from '../util/getElementType';
import hasAccessibleChild from '../util/hasAccessibleChild';
@@ -45,45 +45,55 @@ function validateNesting(node) {
return false;
}
-const validateId = (node) => {
- const htmlForAttr = getProp(node.attributes, 'htmlFor');
- const htmlForValue = getPropValue(htmlForAttr);
+function validateID({ attributes }, context) {
+ const { settings } = context;
+ const htmlForAttributes = settings['jsx-a11y']?.attributes?.for ?? ['htmlFor'];
- return htmlForAttr !== false && !!htmlForValue;
-};
+ for (let i = 0; i < htmlForAttributes.length; i += 1) {
+ const attribute = htmlForAttributes[i];
+ if (hasProp(attributes, attribute)) {
+ const htmlForAttr = getProp(attributes, attribute);
+ const htmlForValue = getPropValue(htmlForAttr);
+
+ return htmlForAttr !== false && !!htmlForValue;
+ }
+ }
-const validate = (node, required, allowChildren, elementType) => {
+ return false;
+}
+
+function validate(node, required, allowChildren, elementType, context) {
if (allowChildren === true) {
return hasAccessibleChild(node.parent, elementType);
}
if (required === 'nesting') {
return validateNesting(node);
}
- return validateId(node);
-};
+ return validateID(node, context);
+}
-const getValidityStatus = (node, required, allowChildren, elementType) => {
+function getValidityStatus(node, required, allowChildren, elementType, context) {
if (Array.isArray(required.some)) {
- const isValid = required.some.some((rule) => validate(node, rule, allowChildren, elementType));
+ const isValid = required.some.some((rule) => validate(node, rule, allowChildren, elementType, context));
const message = !isValid
? `Form label must have ANY of the following types of associated control: ${required.some.join(', ')}`
: null;
return { isValid, message };
}
if (Array.isArray(required.every)) {
- const isValid = required.every.every((rule) => validate(node, rule, allowChildren, elementType));
+ const isValid = required.every.every((rule) => validate(node, rule, allowChildren, elementType, context));
const message = !isValid
? `Form label must have ALL of the following types of associated control: ${required.every.join(', ')}`
: null;
return { isValid, message };
}
- const isValid = validate(node, required, allowChildren, elementType);
+ const isValid = validate(node, required, allowChildren, elementType, context);
const message = !isValid
? `Form label must have the following type of associated control: ${required}`
: null;
return { isValid, message };
-};
+}
export default {
meta: {
@@ -99,7 +109,7 @@ export default {
create: (context) => {
const elementType = getElementType(context);
return {
- JSXOpeningElement: (node) => {
+ JSXOpeningElement(node) {
const options = context.options[0] || {};
const componentOptions = options.components || [];
const typesToValidate = ['label'].concat(componentOptions);
@@ -113,7 +123,7 @@ export default {
const required = options.required || { every: ['nesting', 'id'] };
const allowChildren = options.allowChildren || false;
- const { isValid, message } = getValidityStatus(node, required, allowChildren, elementType);
+ const { isValid, message } = getValidityStatus(node, required, allowChildren, elementType, context);
if (!isValid) {
context.report({
node,