Skip to content

Commit a1ee7f8

Browse files
edoardocavazzaljharb
authored andcommitted
[New] add attributes setting
1 parent 83fd9c4 commit a1ee7f8

File tree

7 files changed

+78
-25
lines changed

7 files changed

+78
-25
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`:
104104
"CustomButton": "button",
105105
"MyButton": "button",
106106
"RoundButton": "button"
107+
},
108+
"attributes": {
109+
"for": ["htmlFor", "for"]
107110
}
108111
}
109112
}
@@ -202,6 +205,11 @@ module.exports = [
202205

203206
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.
204207

208+
#### Attribute Mapping
209+
210+
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.
211+
For example, you may want to allow the `for` attribute in addition to the `htmlFor` attribute for checking label associations.
212+
205213
#### Polymorphic Components
206214

207215
You can optionally use the `polymorphicPropName` setting to define the prop your code uses to create polymorphic components.

__tests__/src/rules/label-has-associated-control-test.js

+12
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,23 @@ const componentsSettings = {
4040
},
4141
};
4242

43+
const attributesSettings = {
44+
'jsx-a11y': {
45+
attributes: {
46+
for: ['htmlFor', 'for'],
47+
},
48+
},
49+
};
50+
4351
const htmlForValid = [
4452
{ code: '<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }] },
4553
{ code: '<label htmlFor="js_id" aria-label="A label" />' },
4654
{ code: '<label htmlFor="js_id" aria-labelledby="A label" />' },
4755
{ code: '<div><label htmlFor="js_id">A label</label><input id="js_id" /></div>' },
56+
{ code: '<label for="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }], settings: attributesSettings },
57+
{ code: '<label for="js_id" aria-label="A label" />', settings: attributesSettings },
58+
{ code: '<label for="js_id" aria-labelledby="A label" />', settings: attributesSettings },
59+
{ code: '<div><label for="js_id">A label</label><input id="js_id" /></div>', settings: attributesSettings },
4860
// Custom label component.
4961
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }] },
5062
{ code: '<CustomLabel htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },

__tests__/src/rules/label-has-for-test.js

+12
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,28 @@ const optionsChildrenAllowed = [{
5050
allowChildren: true,
5151
}];
5252

53+
const attributesSettings = {
54+
'jsx-a11y': {
55+
attributes: {
56+
for: ['htmlFor', 'for'],
57+
},
58+
},
59+
};
60+
5361
ruleTester.run('label-has-for', rule, {
5462
valid: parsers.all([].concat(
5563
// DEFAULT ELEMENT 'label' TESTS
5664
{ code: '<div />' },
5765
{ code: '<label htmlFor="foo"><input /></label>' },
5866
{ code: '<label htmlFor="foo"><textarea /></label>' },
67+
{ code: '<label for="foo"><input /></label>', settings: attributesSettings },
68+
{ code: '<label for="foo"><textarea /></label>', settings: attributesSettings },
5969
{ code: '<Label />' }, // lower-case convention refers to real HTML elements.
6070
{ code: '<Label htmlFor="foo" />' },
71+
{ code: '<Label for="foo" />', settings: attributesSettings },
6172
{ code: '<Descriptor />' },
6273
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>' },
74+
{ code: '<Descriptor for="foo">Test!</Descriptor>', settings: attributesSettings },
6375
{ code: '<UX.Layout>test</UX.Layout>' },
6476

6577
// CUSTOM ELEMENT ARRAY OPTION TESTS

docs/rules/label-has-for.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Enforce label tags have associated control.
1313
There are two supported ways to associate a label with a control:
1414

1515
- nesting: by wrapping a control in a label tag
16-
- id: by using the prop `htmlFor` as in `htmlFor=[ID of control]`
16+
- id: by using the prop `htmlFor` (or any configured attribute) as in `htmlFor=[ID of control]`
1717

1818
To fully cover 100% of assistive devices, you're encouraged to validate for both nesting and id.
1919

flow/eslint.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export type ESLintSettings = {
1010
[string]: mixed,
1111
'jsx-a11y'?: {
1212
polymorphicPropName?: string,
13-
components?: {[string]: string},
13+
components?: { [string]: string },
14+
attributes?: { for?: string[] },
1415
},
1516
}
1617

src/rules/label-has-associated-control.js

+17-7
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// Rule Definition
1010
// ----------------------------------------------------------------------------
1111

12-
import { getProp, getPropValue } from 'jsx-ast-utils';
12+
import { hasProp, getProp, getPropValue } from 'jsx-ast-utils';
1313
import type { JSXElement } from 'ast-types-flow';
1414
import { generateObjSchema, arraySchema } from '../util/schemas';
1515
import type { ESLintConfig, ESLintContext, ESLintVisitorSelectorConfig } from '../../flow/eslint';
@@ -36,12 +36,22 @@ const schema = generateObjSchema({
3636
},
3737
});
3838

39-
const validateId = (node) => {
40-
const htmlForAttr = getProp(node.attributes, 'htmlFor');
41-
const htmlForValue = getPropValue(htmlForAttr);
39+
function validateID(node, context) {
40+
const { settings } = context;
41+
const htmlForAttributes = settings['jsx-a11y']?.attributes?.for ?? ['htmlFor'];
4242

43-
return htmlForAttr !== false && !!htmlForValue;
44-
};
43+
for (let i = 0; i < htmlForAttributes.length; i += 1) {
44+
const attribute = htmlForAttributes[i];
45+
if (hasProp(node.attributes, attribute)) {
46+
const htmlForAttr = getProp(node.attributes, attribute);
47+
const htmlForValue = getPropValue(htmlForAttr);
48+
49+
return htmlForAttr !== false && !!htmlForValue;
50+
}
51+
}
52+
53+
return false;
54+
}
4555

4656
export default ({
4757
meta: {
@@ -76,7 +86,7 @@ export default ({
7686
options.depth === undefined ? 2 : options.depth,
7787
25,
7888
);
79-
const hasLabelId = validateId(node.openingElement);
89+
const hasLabelId = validateID(node.openingElement, context);
8090
// Check for multiple control components.
8191
const hasNestedControl = controlComponents.some((name) => mayContainChildComponent(
8292
node,

src/rules/label-has-for.js

+26-16
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// Rule Definition
88
// ----------------------------------------------------------------------------
99

10-
import { getProp, getPropValue } from 'jsx-ast-utils';
10+
import { hasProp, getProp, getPropValue } from 'jsx-ast-utils';
1111
import { generateObjSchema, arraySchema, enumArraySchema } from '../util/schemas';
1212
import getElementType from '../util/getElementType';
1313
import hasAccessibleChild from '../util/hasAccessibleChild';
@@ -45,45 +45,55 @@ function validateNesting(node) {
4545
return false;
4646
}
4747

48-
const validateId = (node) => {
49-
const htmlForAttr = getProp(node.attributes, 'htmlFor');
50-
const htmlForValue = getPropValue(htmlForAttr);
48+
function validateID({ attributes }, context) {
49+
const { settings } = context;
50+
const htmlForAttributes = settings['jsx-a11y']?.attributes?.for ?? ['htmlFor'];
5151

52-
return htmlForAttr !== false && !!htmlForValue;
53-
};
52+
for (let i = 0; i < htmlForAttributes.length; i += 1) {
53+
const attribute = htmlForAttributes[i];
54+
if (hasProp(attributes, attribute)) {
55+
const htmlForAttr = getProp(attributes, attribute);
56+
const htmlForValue = getPropValue(htmlForAttr);
57+
58+
return htmlForAttr !== false && !!htmlForValue;
59+
}
60+
}
5461

55-
const validate = (node, required, allowChildren, elementType) => {
62+
return false;
63+
}
64+
65+
function validate(node, required, allowChildren, elementType, context) {
5666
if (allowChildren === true) {
5767
return hasAccessibleChild(node.parent, elementType);
5868
}
5969
if (required === 'nesting') {
6070
return validateNesting(node);
6171
}
62-
return validateId(node);
63-
};
72+
return validateID(node, context);
73+
}
6474

65-
const getValidityStatus = (node, required, allowChildren, elementType) => {
75+
function getValidityStatus(node, required, allowChildren, elementType, context) {
6676
if (Array.isArray(required.some)) {
67-
const isValid = required.some.some((rule) => validate(node, rule, allowChildren, elementType));
77+
const isValid = required.some.some((rule) => validate(node, rule, allowChildren, elementType, context));
6878
const message = !isValid
6979
? `Form label must have ANY of the following types of associated control: ${required.some.join(', ')}`
7080
: null;
7181
return { isValid, message };
7282
}
7383
if (Array.isArray(required.every)) {
74-
const isValid = required.every.every((rule) => validate(node, rule, allowChildren, elementType));
84+
const isValid = required.every.every((rule) => validate(node, rule, allowChildren, elementType, context));
7585
const message = !isValid
7686
? `Form label must have ALL of the following types of associated control: ${required.every.join(', ')}`
7787
: null;
7888
return { isValid, message };
7989
}
8090

81-
const isValid = validate(node, required, allowChildren, elementType);
91+
const isValid = validate(node, required, allowChildren, elementType, context);
8292
const message = !isValid
8393
? `Form label must have the following type of associated control: ${required}`
8494
: null;
8595
return { isValid, message };
86-
};
96+
}
8797

8898
export default {
8999
meta: {
@@ -99,7 +109,7 @@ export default {
99109
create: (context) => {
100110
const elementType = getElementType(context);
101111
return {
102-
JSXOpeningElement: (node) => {
112+
JSXOpeningElement(node) {
103113
const options = context.options[0] || {};
104114
const componentOptions = options.components || [];
105115
const typesToValidate = ['label'].concat(componentOptions);
@@ -113,7 +123,7 @@ export default {
113123
const required = options.required || { every: ['nesting', 'id'] };
114124
const allowChildren = options.allowChildren || false;
115125

116-
const { isValid, message } = getValidityStatus(node, required, allowChildren, elementType);
126+
const { isValid, message } = getValidityStatus(node, required, allowChildren, elementType, context);
117127
if (!isValid) {
118128
context.report({
119129
node,

0 commit comments

Comments
 (0)