Skip to content

Commit fe9bd6f

Browse files
authored
Merge pull request #240 from ignatiusreza/label/nesting
Allow label-has-for rule to pass if containing nesting content.
2 parents 0228998 + 06c30c3 commit fe9bd6f

File tree

4 files changed

+82
-13
lines changed

4 files changed

+82
-13
lines changed

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,22 @@ import rule from '../../../src/rules/label-has-for';
1919
const ruleTester = new RuleTester();
2020

2121
const expectedError = {
22-
message: 'Form controls using a label to identify them must be ' +
23-
'programmatically associated with the control using htmlFor',
22+
message: 'Form label must have associated control',
2423
type: 'JSXOpeningElement',
2524
};
2625

2726
const array = [{
2827
components: ['Label', 'Descriptor'],
2928
}];
29+
const optionsRequiredNesting = [{
30+
required: 'nesting',
31+
}];
32+
const optionsRequiredSome = [{
33+
required: { some: ['nesting', 'id'] },
34+
}];
35+
const optionsRequiredEvery = [{
36+
required: { every: ['nesting', 'id'] },
37+
}];
3038

3139
ruleTester.run('label-has-for', rule, {
3240
valid: [
@@ -54,6 +62,10 @@ ruleTester.run('label-has-for', rule, {
5462
{ code: '<Descriptor htmlFor={`${id}`} />', options: array },
5563
{ code: '<div />', options: array },
5664
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>', options: array },
65+
{ code: '<label><input /></label>', options: optionsRequiredNesting },
66+
{ code: '<label><input /></label>', options: optionsRequiredSome },
67+
{ code: '<label htmlFor="foo" />', options: optionsRequiredSome },
68+
{ code: '<label htmlFor="foo"><input /></label>', options: optionsRequiredEvery },
5769
].map(parserOptionsMapper),
5870
invalid: [
5971
// DEFAULT ELEMENT 'label' TESTS
@@ -62,6 +74,7 @@ ruleTester.run('label-has-for', rule, {
6274
{ code: '<label htmlFor={`${undefined}`} />', errors: [expectedError] },
6375
{ code: '<label>First Name</label>', errors: [expectedError] },
6476
{ code: '<label {...props}>Foo</label>', errors: [expectedError] },
77+
{ code: '<label><input /></label>', errors: [expectedError] },
6578

6679
// CUSTOM ELEMENT ARRAY OPTION TESTS
6780
{ code: '<Label id="foo" />', errors: [expectedError], options: array },
@@ -102,5 +115,11 @@ ruleTester.run('label-has-for', rule, {
102115
errors: [expectedError],
103116
options: array,
104117
},
118+
{ code: '<label htmlFor="foo" />', errors: [expectedError], options: optionsRequiredNesting },
119+
{ code: '<label>First Name</label>', errors: [expectedError], options: optionsRequiredNesting },
120+
{ code: '<label>First Name</label>', errors: [expectedError], options: optionsRequiredSome },
121+
{ code: '<label htmlFor="foo" />', errors: [expectedError], options: optionsRequiredEvery },
122+
{ code: '<label><input /></label>', errors: [expectedError], options: optionsRequiredEvery },
123+
{ code: '<label>First Name</label>', errors: [expectedError], options: optionsRequiredEvery },
105124
].map(parserOptionsMapper),
106125
});

docs/rules/label-has-for.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# label-has-for
22

3-
Enforce label tags have htmlFor attribute. Form controls using a label to identify them must have only one label that is programmatically associated with the control using: label htmlFor=[ID of control].
3+
Enforce label tags have associated control.
4+
5+
There are two supported ways to associate a label with a control:
6+
7+
- nesting: by wrapping a control in a label tag
8+
- id: by using the prop `htmlFor` as in `htmlFor=[ID of control]`
9+
10+
To fully cover 100% of assistive devices, you're encouraged to validate for both nesting and id.
411

512
## Rule details
613

@@ -11,7 +18,10 @@ This rule takes one optional object argument of type object:
1118
"rules": {
1219
"jsx-a11y/label-has-for": [ 2, {
1320
"components": [ "Label" ],
14-
}],
21+
"required": {
22+
"every": [ "nesting", "id" ]
23+
}
24+
}],
1525
}
1626
}
1727
```
@@ -43,6 +53,14 @@ return (
4353
);
4454
```
4555

56+
The `required` option (defaults to `"required": "id"`) determines which checks are activated. You're allowed to pass in one of the following types:
57+
58+
- string: must be one of the acceptable strings (`"nesting"` or `"id"`)
59+
- object, must have one of the following properties:
60+
61+
- some: an array of acceptable strings, will pass if ANY of the requested checks passed
62+
- every: an array of acceptable strings, will pass if ALL of the requested checks passed
63+
4664
Note that passing props as spread attribute without `htmlFor` explicitly defined will cause this rule to fail. Explicitly pass down `htmlFor` prop for rule to pass. The prop must have an actual value to pass. Use `Label` component above as a reference. **It is a good thing to explicitly pass props that you expect to be passed for self-documentation.** For example:
4765

4866
#### Bad

src/rules/label-has-for.js

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,45 @@
88
// ----------------------------------------------------------------------------
99

1010
import { getProp, getPropValue, elementType } from 'jsx-ast-utils';
11-
import { generateObjSchema, arraySchema } from '../util/schemas';
11+
import { generateObjSchema, arraySchema, enumArraySchema } from '../util/schemas';
1212

13-
const errorMessage = 'Form controls using a label to identify them must be ' +
14-
'programmatically associated with the control using htmlFor';
13+
const errorMessage = 'Form label must have associated control';
1514

16-
const schema = generateObjSchema({ components: arraySchema });
15+
const enumValues = ['nesting', 'id'];
16+
const schema = {
17+
type: 'object',
18+
properties: {
19+
components: arraySchema,
20+
required: {
21+
oneOf: [
22+
{ type: 'string', enum: enumValues },
23+
generateObjSchema({ some: enumArraySchema(enumValues) }, ['some']),
24+
generateObjSchema({ every: enumArraySchema(enumValues) }, ['every']),
25+
],
26+
},
27+
},
28+
};
29+
30+
const validateNesting = node => !!node.parent.children.find(child => child.type === 'JSXElement');
31+
const validateId = (node) => {
32+
const htmlForAttr = getProp(node.attributes, 'htmlFor');
33+
const htmlForValue = getPropValue(htmlForAttr);
34+
35+
return htmlForAttr !== false && !!htmlForValue;
36+
};
37+
const validate = (node, required) => (
38+
required === 'nesting' ? validateNesting(node) : validateId(node)
39+
);
40+
41+
const isValid = (node, required) => {
42+
if (Array.isArray(required.some)) {
43+
return required.some.some(rule => validate(node, rule));
44+
} else if (Array.isArray(required.every)) {
45+
return required.every.every(rule => validate(node, rule));
46+
}
47+
48+
return validate(node, required);
49+
};
1750

1851
module.exports = {
1952
meta: {
@@ -33,11 +66,9 @@ module.exports = {
3366
return;
3467
}
3568

36-
const htmlForAttr = getProp(node.attributes, 'htmlFor');
37-
const htmlForValue = getPropValue(htmlForAttr);
38-
const isInvalid = htmlForAttr === false || !htmlForValue;
69+
const required = options.required || 'id';
3970

40-
if (isInvalid) {
71+
if (!isValid(node, required)) {
4172
context.report({
4273
node,
4374
message: errorMessage,

src/util/schemas.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export const enumArraySchema = (enumeratedList = []) => Object.assign({}, arrayS
2424
* Factory function to generate an object schema
2525
* with specified properties object
2626
*/
27-
export const generateObjSchema = (properties = {}) => ({
27+
export const generateObjSchema = (properties = {}, required) => ({
2828
type: 'object',
2929
properties,
30+
required,
3031
});

0 commit comments

Comments
 (0)