Skip to content

Commit 2031c01

Browse files
[New] add attributes setting
1 parent 0d5321a commit 2031c01

File tree

8 files changed

+61
-20
lines changed

8 files changed

+61
-20
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`:
103103
"CustomButton": "button",
104104
"MyButton": "button",
105105
"RoundButton": "button"
106+
},
107+
"attributes": {
108+
"for": ["htmlFor", "for"]
106109
}
107110
}
108111
}
@@ -113,6 +116,10 @@ Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`:
113116

114117
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.
115118

119+
#### Attribute Mapping
120+
121+
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.
122+
116123
#### Polymorphic Components
117124

118125
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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ const htmlForValid = [
4040
{ code: '<label htmlFor="js_id" aria-label="A label" />' },
4141
{ code: '<label htmlFor="js_id" aria-labelledby="A label" />' },
4242
{ code: '<div><label htmlFor="js_id">A label</label><input id="js_id" /></div>' },
43+
{ code: '<label for="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4, htmlForAttributes: ['htmlFor', 'for'] }] },
44+
{ code: '<label for="js_id" aria-label="A label" />', options: [{ htmlForAttributes: ['htmlFor', 'for'] }] },
45+
{ code: '<label for="js_id" aria-labelledby="A label" />', options: [{ htmlForAttributes: ['htmlFor', 'for'] }] },
46+
{ code: '<div><label for="js_id">A label</label><input id="js_id" /></div>', options: [{ htmlForAttributes: ['htmlFor', 'for'] }] },
4347
// Custom label component.
4448
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }] },
4549
{ code: '<CustomLabel htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,14 @@ ruleTester.run('label-has-for', rule, {
5656
{ code: '<div />' },
5757
{ code: '<label htmlFor="foo"><input /></label>' },
5858
{ code: '<label htmlFor="foo"><textarea /></label>' },
59+
{ code: '<label for="foo"><input /></label>', options: [{ htmlForAttributes: ['htmlFor', 'for'] }] },
60+
{ code: '<label for="foo"><textarea /></label>', options: [{ htmlForAttributes: ['htmlFor', 'for'] }] },
5961
{ code: '<Label />' }, // lower-case convention refers to real HTML elements.
6062
{ code: '<Label htmlFor="foo" />' },
63+
{ code: '<Label for="foo" />', options: [{ htmlForAttributes: ['htmlFor', 'for'] }] },
6164
{ code: '<Descriptor />' },
6265
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>' },
66+
{ code: '<Descriptor for="foo">Test!</Descriptor>', options: [{ htmlForAttributes: ['htmlFor', 'for'] }] },
6367
{ code: '<UX.Layout>test</UX.Layout>' },
6468

6569
// CUSTOM ELEMENT ARRAY OPTION TESTS

docs/rules/label-has-associated-control.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ And the configuration:
5353
{
5454
"rules": {
5555
"jsx-a11y/label-has-associated-control": [ 2, {
56+
"htmlForAttributes": ["htmlFor", "for"],
5657
"labelComponents": ["CustomInputLabel"],
5758
"labelAttributes": ["label"],
5859
"controlComponents": ["CustomInput"],
@@ -103,6 +104,7 @@ This rule takes one optional object argument of type object:
103104
}
104105
```
105106

107+
`htmlForAttributes`: is an array of strings that specify the attribute to check for an associated control. Default is `["htmlFor"]`.
106108
`labelComponents` is a list of custom React Component names that should be checked for an associated control.
107109
`labelAttributes` is a list of attributes to check on the label component and its children for a label. Use this if you have a custom component that uses a string passed on a prop to render an HTML `label`, for example.
108110
`controlComponents` is a list of custom React Components names that will output an input element. [Glob format](https://linuxhint.com/bash_globbing_tutorial/) is also supported for specifying names (e.g., `Label*` matches `LabelComponent` but not `CustomLabel`, `????Label` matches `LinkLabel` but not `CustomLabel`).

docs/rules/label-has-for.md

Lines changed: 4 additions & 1 deletion
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

@@ -25,6 +25,7 @@ This rule takes one optional object argument of type object:
2525
{
2626
"rules": {
2727
"jsx-a11y/label-has-for": [ 2, {
28+
"htmlForAttributes": ["htmlFor", "for"],
2829
"components": [ "Label" ],
2930
"required": {
3031
"every": [ "nesting", "id" ]
@@ -35,6 +36,8 @@ This rule takes one optional object argument of type object:
3536
}
3637
```
3738

39+
The `htmlForAttributes` allows you to specify which prop to check for. This is useful when you want to use a different property instead of `htmlFor`, for example `for`.
40+
3841
For the `components` option, these strings determine which JSX elements (**always including** `<label>`) should be checked for having `htmlFor` prop. This is a good use case when you have a wrapper component that simply renders a `label` element (like in React):
3942

4043
```js

flow/eslint.js

Lines changed: 2 additions & 1 deletion
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

Lines changed: 16 additions & 6 deletions
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';
@@ -20,6 +20,7 @@ import mayHaveAccessibleLabel from '../util/mayHaveAccessibleLabel';
2020
const errorMessage = 'A form label must be associated with a control.';
2121

2222
const schema = generateObjSchema({
23+
htmlForAttributes: arraySchema,
2324
labelComponents: arraySchema,
2425
labelAttributes: arraySchema,
2526
controlComponents: arraySchema,
@@ -35,11 +36,18 @@ const schema = generateObjSchema({
3536
},
3637
});
3738

38-
const validateId = (node) => {
39-
const htmlForAttr = getProp(node.attributes, 'htmlFor');
40-
const htmlForValue = getPropValue(htmlForAttr);
39+
const validateId = (node, htmlForAttributes) => {
40+
for (let i = 0; i < htmlForAttributes.length; i += 1) {
41+
const attribute = htmlForAttributes[i];
42+
if (hasProp(node.attributes, attribute)) {
43+
const htmlForAttr = getProp(node.attributes, attribute);
44+
const htmlForValue = getPropValue(htmlForAttr);
4145

42-
return htmlForAttr !== false && !!htmlForValue;
46+
return htmlForAttr !== false && !!htmlForValue;
47+
}
48+
}
49+
50+
return false;
4351
};
4452

4553
export default ({
@@ -52,7 +60,9 @@ export default ({
5260
},
5361

5462
create: (context: ESLintContext): ESLintVisitorSelectorConfig => {
63+
const { settings } = context;
5564
const options = context.options[0] || {};
65+
const htmlForAttributes = options.htmlForAttributes ?? settings['jsx-a11y']?.attributes?.for ?? ['htmlFor'];
5666
const labelComponents = options.labelComponents || [];
5767
const assertType = options.assert || 'either';
5868
const componentNames = ['label'].concat(labelComponents);
@@ -75,7 +85,7 @@ export default ({
7585
options.depth === undefined ? 2 : options.depth,
7686
25,
7787
);
78-
const hasLabelId = validateId(node.openingElement);
88+
const hasLabelId = validateId(node.openingElement, htmlForAttributes);
7989
// Check for multiple control components.
8090
const hasNestedControl = controlComponents.some((name) => mayContainChildComponent(
8191
node,

src/rules/label-has-for.js

Lines changed: 22 additions & 12 deletions
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';
@@ -16,6 +16,7 @@ const enumValues = ['nesting', 'id'];
1616
const schema = {
1717
type: 'object',
1818
properties: {
19+
htmlForAttributes: arraySchema,
1920
components: arraySchema,
2021
required: {
2122
oneOf: [
@@ -45,40 +46,47 @@ function validateNesting(node) {
4546
return false;
4647
}
4748

48-
const validateId = (node) => {
49-
const htmlForAttr = getProp(node.attributes, 'htmlFor');
50-
const htmlForValue = getPropValue(htmlForAttr);
49+
const validateId = (node, htmlForAttributes) => {
50+
for (let i = 0; i < htmlForAttributes.length; i += 1) {
51+
const attribute = htmlForAttributes[i];
52+
if (hasProp(node.attributes, attribute)) {
53+
const htmlForAttr = getProp(node.attributes, attribute);
54+
const htmlForValue = getPropValue(htmlForAttr);
5155

52-
return htmlForAttr !== false && !!htmlForValue;
56+
return htmlForAttr !== false && !!htmlForValue;
57+
}
58+
}
59+
60+
return false;
5361
};
5462

55-
const validate = (node, required, allowChildren, elementType) => {
63+
const validate = (node, required, allowChildren, elementType, htmlForAttributes) => {
5664
if (allowChildren === true) {
5765
return hasAccessibleChild(node.parent, elementType);
5866
}
5967
if (required === 'nesting') {
6068
return validateNesting(node);
6169
}
62-
return validateId(node);
70+
return validateId(node, htmlForAttributes);
6371
};
6472

65-
const getValidityStatus = (node, required, allowChildren, elementType) => {
73+
const getValidityStatus = (node, required, allowChildren, elementType, htmlForAttributes) => {
6674
if (Array.isArray(required.some)) {
67-
const isValid = required.some.some((rule) => validate(node, rule, allowChildren, elementType));
75+
const isValid = required.some.some((rule) => validate(node, rule, allowChildren, elementType, htmlForAttributes));
6876
const message = !isValid
6977
? `Form label must have ANY of the following types of associated control: ${required.some.join(', ')}`
7078
: null;
7179
return { isValid, message };
7280
}
7381
if (Array.isArray(required.every)) {
74-
const isValid = required.every.every((rule) => validate(node, rule, allowChildren, elementType));
82+
const isValid = required.every.every((rule) => validate(node, rule, allowChildren, elementType, htmlForAttributes));
7583
const message = !isValid
7684
? `Form label must have ALL of the following types of associated control: ${required.every.join(', ')}`
7785
: null;
7886
return { isValid, message };
7987
}
8088

81-
const isValid = validate(node, required, allowChildren, elementType);
89+
const isValid = validate(node, required, allowChildren, elementType, htmlForAttributes);
8290
const message = !isValid
8391
? `Form label must have the following type of associated control: ${required}`
8492
: null;
@@ -100,7 +108,9 @@ export default {
100108
const elementType = getElementType(context);
101109
return {
102110
JSXOpeningElement: (node) => {
111+
const { settings } = context;
103112
const options = context.options[0] || {};
113+
const htmlForAttributes = options.htmlForAttributes ?? settings['jsx-a11y']?.attributes?.for ?? ['htmlFor'];
104114
const componentOptions = options.components || [];
105115
const typesToValidate = ['label'].concat(componentOptions);
106116
const nodeType = elementType(node);
@@ -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, htmlForAttributes);
117127
if (!isValid) {
118128
context.report({
119129
node,

0 commit comments

Comments
 (0)