Skip to content

Commit 7a1ee63

Browse files
authored
Merge pull request #334 from etchteam/main
Add labelComponents option to form control has label
2 parents d5fbd8b + 229428c commit 7a1ee63

File tree

6 files changed

+86
-11
lines changed

6 files changed

+86
-11
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- `labelComponents` options to add custom label components in `form-control-has-label`
12+
913
## [1.1.1] - 2021-12-23
1014

1115
### Changed

Diff for: docs/form-control-has-label.md

+17
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ _References:_
88

99
## Rule details
1010

11+
This rule takes one optional object argument of type object:
12+
13+
```json
14+
{
15+
"rules": {
16+
"vuejs-accessibility/form-control-has-label": [
17+
"error",
18+
{
19+
"labelComponents": ["CustomLabel"],
20+
}
21+
]
22+
}
23+
}
24+
```
25+
26+
For the `labelComponents` option, these strings determine which elements (**always including** `<label>`) should be checked for having the `for` prop. This is a good use case when you have a wrapper component that simply renders a `label` element.
27+
1128
### Succeed
1229

1330
```

Diff for: src/rules/__tests__/form-control-has-label.test.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ makeRuleTester("form-control-has-label", rule, {
2121
<div aria-hidden="true">
2222
<input value="1" type="text" />
2323
</div>
24-
`
24+
`,
25+
{
26+
code: "<custom-label for='input'>text</custom-label><input type='text' id='input' />",
27+
options: [{ labelComponents: ["CustomLabel"] }]
28+
},
2529
],
26-
invalid: ["<input type='text' />", "<textarea type='text'></textarea>"]
30+
invalid: [
31+
"<input type='text' />",
32+
"<textarea type='text'></textarea>",
33+
"<custom-label for='input'>text</custom-label><input type='text' id='input' />"
34+
]
2735
});

Diff for: src/rules/form-control-has-label.ts

+31-9
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,38 @@
11
import type { Rule } from "eslint";
22
import type { AST } from "vue-eslint-parser";
33

4+
interface FormControlHasLabelOptions {
5+
labelComponents: string[];
6+
}
7+
48
import {
59
defineTemplateBodyVisitor,
610
getElementAttributeValue,
711
getElementType,
812
hasAriaLabel,
913
isAriaHidden,
10-
makeDocsURL
14+
isMatchingElement,
15+
makeDocsURL,
1116
} from "../utils";
1217

1318
function isLabelElement(
1419
node:
1520
| AST.VElement
1621
| AST.VDocumentFragment
1722
| AST.VText
18-
| AST.VExpressionContainer
23+
| AST.VExpressionContainer,
24+
{ labelComponents = [] }: FormControlHasLabelOptions
1925
) {
20-
return node.type === "VElement" && getElementType(node) === "label";
26+
const allLabelComponents = labelComponents.concat("label");
27+
return isMatchingElement(node, allLabelComponents);
2128
}
2229

23-
function hasLabelElement(node: AST.VElement): boolean {
30+
function hasLabelElement(node: AST.VElement, options: FormControlHasLabelOptions): boolean {
2431
const { parent } = node;
2532

2633
return (
27-
[parent, ...parent.children].some(isLabelElement) ||
28-
(parent && parent.type === "VElement" && hasLabelElement(parent))
34+
[parent, ...parent.children].some((node) => isLabelElement(node, options)) ||
35+
(parent && parent.type === "VElement" && hasLabelElement(parent, options))
2936
);
3037
}
3138

@@ -39,13 +46,28 @@ const rule: Rule.RuleModule = {
3946
default:
4047
"Each form element must have a programmatically associated label element."
4148
},
42-
schema: []
49+
schema: [
50+
{
51+
type: "object",
52+
properties: {
53+
labelComponents: {
54+
type: "array",
55+
items: {
56+
type: "string"
57+
},
58+
uniqueItems: true
59+
}
60+
}
61+
}
62+
]
4363
},
4464
create(context) {
4565
return defineTemplateBodyVisitor(context, {
4666
VElement(node) {
67+
const options = context.options[0] || {};
4768
const elementType = getElementType(node);
48-
if (!["input", "textarea"].includes(elementType)) {
69+
70+
if (!["input", "textarea", "select"].includes(elementType)) {
4971
return;
5072
}
5173

@@ -65,7 +87,7 @@ const rule: Rule.RuleModule = {
6587
if (
6688
!isAriaHidden(node) &&
6789
!hasAriaLabel(node) &&
68-
!hasLabelElement(node)
90+
!hasLabelElement(node, options)
6991
) {
7092
context.report({ node: node as any, messageId: "default" });
7193
}

Diff for: src/utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { default as isAriaHidden } from "./utils/isAriaHidden";
1414
export { default as isAttribute } from "./utils/isAttribute";
1515
export { default as isHiddenFromScreenReader } from "./utils/isHiddenFromScreenReader";
1616
export { default as isInteractiveElement } from "./utils/isInteractiveElement";
17+
export { default as isMatchingElement } from "./utils/isMatchingElement";
1718
export { default as isPresentationRole } from "./utils/isPresentationRole";
1819
export { default as makeDocsURL } from "./utils/makeDocsURL";
1920
export { default as makeKebabCase } from "./utils/makeKebabCase";

Diff for: src/utils/isMatchingElement.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { AST } from "vue-eslint-parser";
2+
3+
import getElementType from "./getElementType";
4+
import makeKebabCase from "./makeKebabCase";
5+
6+
function isMatchingElement(
7+
node:
8+
| AST.VElement
9+
| AST.VDocumentFragment
10+
| AST.VText
11+
| AST.VExpressionContainer,
12+
searchArray: string[]
13+
) {
14+
if (!(node.type === "VElement")) return false;
15+
16+
const elementType = getElementType(node);
17+
18+
return searchArray.some((item: string) => {
19+
return makeKebabCase(item) === elementType;
20+
});
21+
}
22+
23+
export default isMatchingElement;

0 commit comments

Comments
 (0)