Skip to content

Commit 29f41df

Browse files
committed
Sync with template-lint
1 parent 8e893bd commit 29f41df

3 files changed

Lines changed: 220 additions & 288 deletions

File tree

docs/rules/template-require-input-label.md

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,101 @@
22

33
<!-- end auto-generated rule header -->
44

5-
Require form input elements to have an associated label for accessibility.
5+
Users with assistive technology need user-input form elements to have
6+
associated labels.
67

7-
## Rule Details
8+
The rule applies to the following HTML tags:
89

9-
This rule enforces that input, textarea, and select elements have a way to be labeled, either through an `id` attribute (which can be referenced by a `<label for="...">`) or through `aria-label` or `aria-labelledby` attributes.
10+
- `<input>`
11+
- `<textarea>`
12+
- `<select>`
13+
14+
The rule also applies to the following ember components:
15+
16+
- `<Textarea />`
17+
- `<Input />`
18+
- `{{textarea}}`
19+
- `{{input}}`
20+
21+
The label is **essential** for users. Leaving it out will cause **three**
22+
different WCAG criteria to fail:
23+
24+
- [1.3.1, Info and Relationships](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html)
25+
- [3.3.2, Labels or Instructions](https://www.w3.org/WAI/WCAG21/Understanding/labels-or-instructions.html)
26+
- [4.1.2, Name, Role, Value](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html)
27+
28+
It is also associated with this common failure:
29+
30+
- [#68: Failure of Success Criterion 4.1.2 due to a user interface control not having a programmatically determined name](https://www.w3.org/WAI/WCAG21/Techniques/failures/F68)
31+
32+
This rule checks to see if the input is contained by a label element. If it is
33+
not, it checks to see if the input has any of these three attributes: `id`,
34+
`aria-label`, or `aria-labelledby`. While the `id` element on the input is not
35+
a concrete indicator of the presence of an associated `<label>` element with a
36+
`for` attribute, it is a good indicator that one likely exists.
37+
38+
This rule does not allow an input to use a `title` attribute for a valid label.
39+
This is because implementation by browsers is unreliable and incomplete.
40+
41+
This rule is unable to determine if a valid label is present if `...attributes`
42+
is used, and must allow it to pass. However, developers are encouraged to write
43+
tests to ensure that a valid label is present for each input element present.
1044

1145
## Examples
1246

13-
Examples of **incorrect** code for this rule:
47+
This rule **forbids** the following:
1448

1549
```gjs
1650
<template>
17-
<input type="text" />
51+
<div><input /></div>
1852
</template>
53+
```
1954

55+
```gjs
2056
<template>
21-
<textarea></textarea>
57+
<input title="some label text" />
2258
</template>
59+
```
2360

61+
```gjs
2462
<template>
25-
<select>
26-
<option>Option 1</option>
27-
</select>
63+
<textarea />
2864
</template>
2965
```
3066

31-
Examples of **correct** code for this rule:
67+
This rule **allows** the following:
3268

3369
```gjs
3470
<template>
35-
<label for="name">Name:</label>
36-
<input id="name" type="text" />
71+
<label>Some Label Text<input /></label>
3772
</template>
73+
```
3874

75+
```gjs
3976
<template>
40-
<input aria-label="Name" type="text" />
77+
<input id="someId" />
4178
</template>
79+
```
80+
81+
```gjs
82+
<template>
83+
<input aria-label="Label Text Here" />
84+
</template>
85+
```
86+
87+
```gjs
88+
<template>
89+
<input aria-labelledby="someButtonId" />
90+
</template>
91+
```
4292

93+
```gjs
4394
<template>
44-
<input aria-labelledby="name-label" type="text" />
95+
<input ...attributes />
4596
</template>
97+
```
4698

99+
```gjs
47100
<template>
48101
<input type="hidden" />
49102
</template>
@@ -55,13 +108,19 @@ Examples of **correct** code for this rule:
55108
- another option is to add an aria-label to the input element.
56109
- wrapping the input element in a label element is also allowed; however this is less flexible for styling purposes, so use with awareness.
57110

58-
## Options
111+
## Configuration
59112

60-
| Name | Type | Default | Description |
61-
| ----------- | ---------- | ------- | -------------------------------------------------------------------- |
62-
| `labelTags` | `string[]` | `[]` | Additional tag names to treat as label elements (besides `<label>`). |
113+
- boolean - `true` to enable / `false` to disable
114+
- object -- An object with the following keys:
115+
- `labelTags` -- An array of component names for that may be used as label replacements (in addition to the HTML `label` tag)
63116

64117
## References
65118

66-
- [WCAG 2.1 - Labels or Instructions](https://www.w3.org/WAI/WCAG21/Understanding/labels-or-instructions.html)
67-
- [MDN - aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label)
119+
- [Understanding Success Criterion 1.3.1: Info and Relationships](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships)
120+
- [Understanding Success Criterion 3.3.2: Labels or Instructions](https://www.w3.org/WAI/WCAG21/Understanding/labels-or-instructions.html)
121+
- [Understanding Success Criterion 4.1.2: Name, Role, Value](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html)
122+
- [Using label elements to associate text labels and form controls](https://www.w3.org/WAI/WCAG21/Techniques/html/H44.html)
123+
- [Using aria-labelledby to provide a name for user interface controls](https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA16)
124+
- [Using aria-label to provide an invisible label where a visible label cannot be used](https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA14.html)
125+
- [Failure due to a user interface control not having a programmatically determined name](https://www.w3.org/WAI/WCAG21/Techniques/failures/F68)
126+
- [Failure due to visually formatting a set of phone number fields but not including a text label](https://www.w3.org/WAI/WCAG21/Techniques/failures/F82)

lib/rules/template-require-input-label.js

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,40 @@ function hasAttr(node, name) {
22
return node.attributes?.some((a) => a.name === name);
33
}
44

5+
function isString(value) {
6+
return typeof value === 'string';
7+
}
8+
9+
function isRegExp(value) {
10+
return value instanceof RegExp;
11+
}
12+
13+
function allowedFormat(value) {
14+
return isString(value) || isRegExp(value);
15+
}
16+
17+
function parseConfig(config) {
18+
if (config === false) {
19+
return false;
20+
}
21+
22+
if (config === true || config === undefined) {
23+
return { labelTags: ['label'] };
24+
}
25+
26+
if (config && typeof config === 'object' && Array.isArray(config.labelTags)) {
27+
return {
28+
labelTags: ['label', ...config.labelTags.filter(allowedFormat)],
29+
};
30+
}
31+
32+
return { labelTags: ['label'] };
33+
}
34+
35+
function matchesLabelTag(tag, configuredTag) {
36+
return isRegExp(configuredTag) ? configuredTag.test(tag) : configuredTag === tag;
37+
}
38+
539
/** @type {import('eslint').Rule.RuleModule} */
640
module.exports = {
741
meta: {
@@ -14,19 +48,23 @@ module.exports = {
1448
},
1549
schema: [
1650
{
17-
type: 'object',
18-
properties: {
19-
labelTags: {
20-
type: 'array',
21-
items: { type: 'string' },
51+
anyOf: [
52+
{ type: 'boolean' },
53+
{
54+
type: 'object',
55+
properties: {
56+
labelTags: {
57+
type: 'array',
58+
},
59+
},
60+
additionalProperties: false,
2261
},
23-
},
24-
additionalProperties: false,
62+
],
2563
},
2664
],
2765
messages: {
28-
requireLabel: 'Input elements should have an associated label.',
29-
multipleLabels: 'Input element has multiple labelling mechanisms.',
66+
requireLabel: 'form elements require a valid associated label.',
67+
multipleLabels: 'form elements should not have multiple labels.',
3068
},
3169
originallyFrom: {
3270
name: 'ember-template-lint',
@@ -37,20 +75,27 @@ module.exports = {
3775
},
3876

3977
create(context) {
40-
const options = context.options[0] || {};
41-
const customLabelTags = options.labelTags || [];
42-
const labelTags = new Set(['label', ...customLabelTags]);
78+
const config = parseConfig(context.options[0]);
79+
if (config === false) {
80+
return {};
81+
}
82+
83+
const filename = context.getFilename();
84+
const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts');
4385
const elementStack = [];
4486

4587
function hasValidLabelParent() {
4688
for (let i = elementStack.length - 1; i >= 0; i--) {
4789
const entry = elementStack[i];
48-
if (labelTags.has(entry.tag)) {
49-
// Custom label tags (not 'label') are always considered valid
90+
const hasMatchingLabelTag = config.labelTags.some((configuredTag) =>
91+
matchesLabelTag(entry.tag, configuredTag)
92+
);
93+
94+
if (hasMatchingLabelTag) {
5095
if (entry.tag !== 'label') {
5196
return true;
5297
}
53-
// For 'label' tag, valid only if it has more than one child (text content + input)
98+
5499
const children = entry.node.children || [];
55100
return children.length > 1;
56101
}
@@ -62,6 +107,10 @@ module.exports = {
62107
GlimmerElementNode(node) {
63108
elementStack.push({ tag: node.tag, node });
64109

110+
if (isStrictMode && (node.tag === 'Input' || node.tag === 'Textarea')) {
111+
return;
112+
}
113+
65114
const tagName = node.tag?.toLowerCase();
66115
if (tagName !== 'input' && tagName !== 'textarea' && tagName !== 'select') {
67116
return;
@@ -101,7 +150,6 @@ module.exports = {
101150
return;
102151
}
103152

104-
// Special case: label parent + id is OK (common pattern)
105153
if (validLabel && hasId) {
106154
return;
107155
}

0 commit comments

Comments
 (0)