Skip to content

Commit f50bbd8

Browse files
authored
Merge branch 'master' into heading-has-content-aria-hidden
2 parents 42f5449 + 20e894a commit f50bbd8

File tree

7 files changed

+115
-61
lines changed

7 files changed

+115
-61
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
language: node_js
22
node_js:
3+
- "10"
4+
- "9"
35
- "8"
46
- "7"
57
- "6"
@@ -27,5 +29,6 @@ matrix:
2729
- node_js: "node"
2830
env: LINT=true TEST=false
2931
allow_failures:
32+
- node_js: "9"
3033
- node_js: "7"
3134
- node_js: "5"

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

Lines changed: 62 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,22 @@ import rule from '../../../src/rules/label-has-for';
1919

2020
const ruleTester = new RuleTester();
2121

22-
const expectedError = {
23-
message: 'Form label must have associated control',
22+
const expectedNestingError = {
23+
message: 'Form label must have the following type of associated control: nesting',
2424
type: 'JSXOpeningElement',
2525
};
2626

27+
const expectedSomeError = {
28+
message: 'Form label must have ANY of the following types of associated control: nesting, id',
29+
type: 'JSXOpeningElement',
30+
};
31+
32+
const expectedEveryError = {
33+
message: 'Form label must have ALL of the following types of associated control: nesting, id',
34+
type: 'JSXOpeningElement',
35+
};
36+
37+
2738
const array = [{
2839
components: ['Label', 'Descriptor'],
2940
}];
@@ -81,78 +92,88 @@ ruleTester.run('label-has-for', rule, {
8192
].map(parserOptionsMapper),
8293
invalid: [
8394
// DEFAULT ELEMENT 'label' TESTS
84-
{ code: '<label id="foo" />', errors: [expectedError] },
85-
{ code: '<label htmlFor={undefined} />', errors: [expectedError] },
86-
{ code: '<label htmlFor={`${undefined}`} />', errors: [expectedError] },
87-
{ code: '<label>First Name</label>', errors: [expectedError] },
88-
{ code: '<label {...props}>Foo</label>', errors: [expectedError] },
89-
{ code: '<label><input /></label>', errors: [expectedError] },
90-
{ code: '<label>{children}</label>', errors: [expectedError] },
91-
{ code: '<label htmlFor="foo" />', errors: [expectedError] },
92-
{ code: '<label htmlFor={"foo"} />', errors: [expectedError] },
93-
{ code: '<label htmlFor={foo} />', errors: [expectedError] },
94-
{ code: '<label htmlFor={`${id}`} />', errors: [expectedError] },
95-
{ code: '<label htmlFor="foo">Test!</label>', errors: [expectedError] },
95+
{ code: '<label id="foo" />', errors: [expectedEveryError] },
96+
{ code: '<label htmlFor={undefined} />', errors: [expectedEveryError] },
97+
{ code: '<label htmlFor={`${undefined}`} />', errors: [expectedEveryError] },
98+
{ code: '<label>First Name</label>', errors: [expectedEveryError] },
99+
{ code: '<label {...props}>Foo</label>', errors: [expectedEveryError] },
100+
{ code: '<label><input /></label>', errors: [expectedEveryError] },
101+
{ code: '<label>{children}</label>', errors: [expectedEveryError] },
102+
{ code: '<label htmlFor="foo" />', errors: [expectedEveryError] },
103+
{ code: '<label htmlFor={"foo"} />', errors: [expectedEveryError] },
104+
{ code: '<label htmlFor={foo} />', errors: [expectedEveryError] },
105+
{ code: '<label htmlFor={`${id}`} />', errors: [expectedEveryError] },
106+
{ code: '<label htmlFor="foo">Test!</label>', errors: [expectedEveryError] },
96107
//
97108
// // CUSTOM ELEMENT ARRAY OPTION TESTS
98-
{ code: '<Label></Label>', errors: [expectedError], options: array },
99-
{ code: '<Label htmlFor="foo" />', errors: [expectedError], options: array },
100-
{ code: '<Label htmlFor={"foo"} />', errors: [expectedError], options: array },
101-
{ code: '<Label htmlFor={foo} />', errors: [expectedError], options: array },
102-
{ code: '<Label htmlFor={`${id}`} />', errors: [expectedError], options: array },
103-
{ code: '<Label htmlFor="foo">Test!</Label>', errors: [expectedError], options: array },
104-
{ code: '<Descriptor htmlFor="foo" />', errors: [expectedError], options: array },
105-
{ code: '<Descriptor htmlFor={"foo"} />', errors: [expectedError], options: array },
106-
{ code: '<Descriptor htmlFor={foo} />', errors: [expectedError], options: array },
107-
{ code: '<Descriptor htmlFor={`${id}`} />', errors: [expectedError], options: array },
109+
{ code: '<Label></Label>', errors: [expectedEveryError], options: array },
110+
{ code: '<Label htmlFor="foo" />', errors: [expectedEveryError], options: array },
111+
{ code: '<Label htmlFor={"foo"} />', errors: [expectedEveryError], options: array },
112+
{ code: '<Label htmlFor={foo} />', errors: [expectedEveryError], options: array },
113+
{ code: '<Label htmlFor={`${id}`} />', errors: [expectedEveryError], options: array },
114+
{ code: '<Label htmlFor="foo">Test!</Label>', errors: [expectedEveryError], options: array },
115+
{ code: '<Descriptor htmlFor="foo" />', errors: [expectedEveryError], options: array },
116+
{ code: '<Descriptor htmlFor={"foo"} />', errors: [expectedEveryError], options: array },
117+
{ code: '<Descriptor htmlFor={foo} />', errors: [expectedEveryError], options: array },
118+
{ code: '<Descriptor htmlFor={`${id}`} />', errors: [expectedEveryError], options: array },
108119
{
109120
code: '<Descriptor htmlFor="foo">Test!</Descriptor>',
110-
errors: [expectedError],
121+
errors: [expectedEveryError],
111122
options: array,
112123
},
113-
{ code: '<Label id="foo" />', errors: [expectedError], options: array },
124+
{ code: '<Label id="foo" />', errors: [expectedEveryError], options: array },
114125
{
115126
code: '<Label htmlFor={undefined} />',
116-
errors: [expectedError],
127+
errors: [expectedEveryError],
117128
options: array,
118129
},
119130
{
120131
code: '<Label htmlFor={`${undefined}`} />',
121-
errors: [expectedError],
132+
errors: [expectedEveryError],
122133
options: array,
123134
},
124-
{ code: '<Label>First Name</Label>', errors: [expectedError], options: array },
135+
{ code: '<Label>First Name</Label>', errors: [expectedEveryError], options: array },
125136
{
126137
code: '<Label {...props}>Foo</Label>',
127-
errors: [expectedError],
138+
errors: [expectedEveryError],
128139
options: array,
129140
},
130-
{ code: '<Descriptor id="foo" />', errors: [expectedError], options: array },
141+
{ code: '<Descriptor id="foo" />', errors: [expectedEveryError], options: array },
131142
{
132143
code: '<Descriptor htmlFor={undefined} />',
133-
errors: [expectedError],
144+
errors: [expectedEveryError],
134145
options: array,
135146
},
136147
{
137148
code: '<Descriptor htmlFor={`${undefined}`} />',
138-
errors: [expectedError],
149+
errors: [expectedEveryError],
139150
options: array,
140151
},
141152
{
142153
code: '<Descriptor>First Name</Descriptor>',
143-
errors: [expectedError],
154+
errors: [expectedEveryError],
144155
options: array,
145156
},
146157
{
147158
code: '<Descriptor {...props}>Foo</Descriptor>',
148-
errors: [expectedError],
159+
errors: [expectedEveryError],
149160
options: array,
150161
},
151-
{ code: '<label>{children}</label>', errors: [expectedError], options: array },
152-
{ code: '<label htmlFor="foo" />', errors: [expectedError], options: optionsRequiredNesting },
153-
{ code: '<label>First Name</label>', errors: [expectedError], options: optionsRequiredNesting },
154-
{ code: '<label>First Name</label>', errors: [expectedError], options: optionsRequiredSome },
155-
{ code: '<label>{children}</label>', errors: [expectedError], options: optionsRequiredSome },
156-
{ code: '<label>{children}</label>', errors: [expectedError], options: optionsRequiredNesting },
162+
{ code: '<label>{children}</label>', errors: [expectedEveryError], options: array },
163+
{ code: '<label htmlFor="foo" />', errors: [expectedNestingError], options: optionsRequiredNesting },
164+
{ code: '<label>First Name</label>', errors: [expectedNestingError], options: optionsRequiredNesting },
165+
{ code: '<label>First Name</label>', errors: [expectedSomeError], options: optionsRequiredSome },
166+
{ code: '<label>{children}</label>', errors: [expectedSomeError], options: optionsRequiredSome },
167+
{ code: '<label>{children}</label>', errors: [expectedNestingError], options: optionsRequiredNesting },
168+
{
169+
code: '<form><input type="text" id="howmuch" value="1" /><label htmlFor="howmuch">How much ?</label></form>',
170+
errors: [expectedEveryError],
171+
options: optionsRequiredEvery,
172+
},
173+
{
174+
code: '<form><input type="text" id="howmuch" value="1" /><label htmlFor="howmuch">How much ?<span /></label></form>',
175+
errors: [expectedEveryError],
176+
options: optionsRequiredEvery,
177+
},
157178
].map(parserOptionsMapper),
158179
});

__tests__/src/util/hasAccessibleChild-test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ describe('hasAccessibleChild', () => {
3636
expect(hasAccessibleChild(element)).toBe(true);
3737
});
3838

39+
it('Returns true for JSXText Element', () => {
40+
const child = {
41+
type: 'JSXText',
42+
value: 'foo',
43+
};
44+
const element = JSXElementMock('div', [], [child]);
45+
expect(hasAccessibleChild(element)).toBe(true);
46+
});
47+
3948
it('Returns false for hidden child JSXElement', () => {
4049
const ariaHiddenAttr = JSXAttributeMock('aria-hidden', true);
4150
const child = JSXElementMock('div', [ariaHiddenAttr]);

docs/rules/no-noninteractive-element-interactions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ This preserves the table cell semantics and the button semantics; the two are no
7777

7878
#### Option 2, convert the table into an ARIA grid
7979

80-
If you're user interface has a table-like layout, but is filled with interactive components in the cells, consider converting the table into a grid.
80+
If your user interface has a table-like layout, but is filled with interactive components in the cells, consider converting the table into a grid.
8181

8282
```
8383
<table role="grid">

package.json

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,23 @@
3131
},
3232
"devDependencies": {
3333
"babel-cli": "^6.26.0",
34-
"babel-core": "^6.26.0",
35-
"babel-eslint": "^8.2.1",
36-
"babel-jest": "^22.2.2",
34+
"babel-core": "^6.26.3",
35+
"babel-eslint": "^8.2.3",
36+
"babel-jest": "^21.2.0",
3737
"babel-plugin-transform-es2015-template-literals": "^6.22.0",
3838
"babel-plugin-transform-flow-strip-types": "^6.22.0",
3939
"babel-plugin-transform-object-rest-spread": "^6.26.0",
4040
"babel-polyfill": "^6.26.0",
4141
"babel-preset-es2015": "^6.24.1",
42-
"coveralls": "^3.0.0",
43-
"eslint": "^4.18.0",
42+
"coveralls": "^3.0.1",
43+
"eslint": "^4.19.1",
4444
"eslint-config-airbnb-base": "^12.1.0",
45-
"eslint-plugin-flowtype": "^2.44.0",
46-
"eslint-plugin-import": "^2.8.0",
47-
"expect": "^22.3.0",
45+
"eslint-plugin-flowtype": "^2.46.3",
46+
"eslint-plugin-import": "^2.12.0",
47+
"expect": "^21.2.1",
4848
"flow-bin": "^0.66.0",
4949
"in-publish": "^2.0.0",
50-
"jest": "^22.3.0",
50+
"jest": "^21.2.1",
5151
"jscodeshift": "^0.4.0",
5252
"minimist": "^1.2.0",
5353
"object.assign": "^4.1.0",
@@ -63,7 +63,7 @@
6363
"aria-query": "^0.7.1",
6464
"array-includes": "^3.0.3",
6565
"ast-types-flow": "^0.0.7",
66-
"axobject-query": "^1.0.2",
66+
"axobject-query": "^2.0.1",
6767
"damerau-levenshtein": "^1.0.4",
6868
"emoji-regex": "^6.5.1",
6969
"has": "^1.0.1",
@@ -85,5 +85,11 @@
8585
"<rootDir>/__tests__/__util__"
8686
],
8787
"testEnvironment": "node"
88+
},
89+
"greenkeeper": {
90+
"ignore": [
91+
"jest",
92+
"babel-jest"
93+
]
8894
}
8995
}

src/rules/label-has-for.js

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import { getProp, getPropValue, elementType } from 'jsx-ast-utils';
1111
import { generateObjSchema, arraySchema, enumArraySchema } from '../util/schemas';
1212
import hasAccessibleChild from '../util/hasAccessibleChild';
1313

14-
const errorMessage = 'Form label must have associated control';
15-
1614
const enumValues = ['nesting', 'id'];
1715
const schema = {
1816
type: 'object',
@@ -29,7 +27,10 @@ const schema = {
2927
},
3028
};
3129

32-
const validateNesting = node => node.parent.children.some(child => child.type === 'JSXElement');
30+
const validateNesting = node => node.parent.children.some((child) => {
31+
const opener = child.openingElement;
32+
return child.type === 'JSXElement' && opener && opener.name.name === 'input';
33+
});
3334

3435
const validateId = (node) => {
3536
const htmlForAttr = getProp(node.attributes, 'htmlFor');
@@ -48,14 +49,26 @@ const validate = (node, required, allowChildren) => {
4849
return validateId(node);
4950
};
5051

51-
const isValid = (node, required, allowChildren) => {
52+
const getValidityStatus = (node, required, allowChildren) => {
5253
if (Array.isArray(required.some)) {
53-
return required.some.some(rule => validate(node, rule, allowChildren));
54+
const isValid = required.some.some(rule => validate(node, rule, allowChildren));
55+
const message = !isValid
56+
? `Form label must have ANY of the following types of associated control: ${required.some.join(', ')}`
57+
: null;
58+
return { isValid, message };
5459
} else if (Array.isArray(required.every)) {
55-
return required.every.every(rule => validate(node, rule, allowChildren));
60+
const isValid = required.every.every(rule => validate(node, rule, allowChildren));
61+
const message = !isValid
62+
? `Form label must have ALL of the following types of associated control: ${required.every.join(', ')}`
63+
: null;
64+
return { isValid, message };
5665
}
5766

58-
return validate(node, required, allowChildren);
67+
const isValid = validate(node, required, allowChildren);
68+
const message = !isValid
69+
? `Form label must have the following type of associated control: ${required}`
70+
: null;
71+
return { isValid, message };
5972
};
6073

6174
module.exports = {
@@ -81,10 +94,11 @@ module.exports = {
8194
const required = options.required || { every: ['nesting', 'id'] };
8295
const allowChildren = options.allowChildren || false;
8396

84-
if (!isValid(node, required, allowChildren)) {
97+
const { isValid, message } = getValidityStatus(node, required, allowChildren);
98+
if (!isValid) {
8599
context.report({
86100
node,
87-
message: errorMessage,
101+
message,
88102
});
89103
}
90104
},

src/util/hasAccessibleChild.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default function hasAccessibleChild(node: JSXElement): boolean {
88
return node.children.some((child) => {
99
switch (child.type) {
1010
case 'Literal':
11+
case 'JSXText':
1112
return Boolean(child.value);
1213
case 'JSXElement':
1314
return !isHiddenFromScreenReader(

0 commit comments

Comments
 (0)