Skip to content

Commit 5d88709

Browse files
Add 3 more template rules: no-invalid-interactive, no-link-to-tagname, no-link-to-positional-params (58/127 total: 46%)
Co-authored-by: NullVoxPopuli <[email protected]>
1 parent e5ff0d5 commit 5d88709

11 files changed

Lines changed: 504 additions & 0 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# template-no-invalid-interactive
2+
3+
> Disallow non-interactive elements with interactive handlers
4+
5+
## Rule Details
6+
7+
This rule prevents adding interactive event handlers (like `onclick`, `onkeydown`, etc.) to non-interactive HTML elements without proper ARIA roles.
8+
9+
## Examples
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```gjs
14+
<template>
15+
<div onclick={{this.handleClick}}>Click me</div>
16+
</template>
17+
```
18+
19+
```gjs
20+
<template>
21+
<span onkeydown={{this.handleKey}}>Press key</span>
22+
</template>
23+
```
24+
25+
Examples of **correct** code for this rule:
26+
27+
```gjs
28+
<template>
29+
<button onclick={{this.handleClick}}>Click me</button>
30+
</template>
31+
```
32+
33+
```gjs
34+
<template>
35+
<div role="button" onclick={{this.handleClick}}>Click me</div>
36+
</template>
37+
```
38+
39+
```gjs
40+
<template>
41+
<button {{on "click" this.handleClick}}>Click me</button>
42+
</template>
43+
```
44+
45+
## References
46+
47+
- [WCAG 2.1 - 2.1.1 Keyboard](https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html)
48+
- [ember-template-lint no-invalid-interactive](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-invalid-interactive.md)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# template-no-link-to-positional-params
2+
3+
> Disallow positional params in LinkTo component
4+
5+
## Rule Details
6+
7+
Positional parameters in `<LinkTo>` components are deprecated. Use named arguments instead.
8+
9+
## Examples
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```gjs
14+
<template>
15+
{{! Old positional params style }}
16+
<LinkTo "posts.post" @model={{this.post}}>Post</LinkTo>
17+
</template>
18+
```
19+
20+
Examples of **correct** code for this rule:
21+
22+
```gjs
23+
<template>
24+
<LinkTo @route="posts.post" @model={{this.post}}>Post</LinkTo>
25+
</template>
26+
```
27+
28+
```gjs
29+
<template>
30+
<LinkTo @route="index">Home</LinkTo>
31+
</template>
32+
```
33+
34+
## References
35+
36+
- [ember-template-lint no-link-to-positional-params](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-link-to-positional-params.md)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# template-no-link-to-tagname
2+
3+
> Disallow tagName attribute on LinkTo component
4+
5+
## Rule Details
6+
7+
The `tagName` attribute on `<LinkTo>` components is deprecated. Use the appropriate HTML element or component instead.
8+
9+
## Examples
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```gjs
14+
<template>
15+
<LinkTo @route="index" tagName="button">Home</LinkTo>
16+
</template>
17+
```
18+
19+
```gjs
20+
<template>
21+
<LinkTo @route="about" @tagName="span">About</LinkTo>
22+
</template>
23+
```
24+
25+
Examples of **correct** code for this rule:
26+
27+
```gjs
28+
<template>
29+
<LinkTo @route="index">Home</LinkTo>
30+
</template>
31+
```
32+
33+
```gjs
34+
<template>
35+
<button type="button" {{on "click" (fn this.transitionTo "index")}}>Home</button>
36+
</template>
37+
```
38+
39+
## References
40+
41+
- [ember-template-lint no-link-to-tagname](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-link-to-tagname.md)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'problem',
5+
docs: {
6+
description: 'disallow non-interactive elements with interactive handlers',
7+
category: 'Accessibility',
8+
strictGjs: true,
9+
strictGts: true,
10+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-invalid-interactive.md',
11+
},
12+
schema: [],
13+
messages: {
14+
noInvalidInteractive:
15+
'Non-interactive element <{{tagName}}> should not have interactive handler "{{handler}}"',
16+
},
17+
},
18+
19+
create(context) {
20+
const INTERACTIVE_HANDLERS = [
21+
'onclick',
22+
'ondblclick',
23+
'onmousedown',
24+
'onmouseup',
25+
'onkeydown',
26+
'onkeyup',
27+
'onkeypress',
28+
];
29+
30+
const NON_INTERACTIVE_ELEMENTS = [
31+
'div',
32+
'span',
33+
'p',
34+
'section',
35+
'article',
36+
'main',
37+
'header',
38+
'footer',
39+
'aside',
40+
'nav',
41+
'h1',
42+
'h2',
43+
'h3',
44+
'h4',
45+
'h5',
46+
'h6',
47+
'ul',
48+
'ol',
49+
'li',
50+
'img',
51+
];
52+
53+
return {
54+
GlimmerElementNode(node) {
55+
const tagName = node.tag.toLowerCase();
56+
57+
if (!NON_INTERACTIVE_ELEMENTS.includes(tagName)) {
58+
return;
59+
}
60+
61+
// Check if element has role that makes it interactive
62+
const roleAttr = node.attributes.find(
63+
(attr) => attr.type === 'GlimmerAttrNode' && attr.name === 'role'
64+
);
65+
if (roleAttr && roleAttr.value && roleAttr.value.type === 'GlimmerTextNode') {
66+
const role = roleAttr.value.chars;
67+
const interactiveRoles = ['button', 'link', 'checkbox', 'radio', 'tab', 'menuitem'];
68+
if (interactiveRoles.includes(role)) {
69+
return; // Element has interactive role, so handlers are okay
70+
}
71+
}
72+
73+
// Check for interactive handlers
74+
for (const attr of node.attributes) {
75+
if (attr.type === 'GlimmerAttrNode') {
76+
const attrName = attr.name.toLowerCase();
77+
if (INTERACTIVE_HANDLERS.includes(attrName)) {
78+
context.report({
79+
node,
80+
messageId: 'noInvalidInteractive',
81+
data: {
82+
tagName,
83+
handler: attrName,
84+
},
85+
});
86+
}
87+
}
88+
}
89+
},
90+
};
91+
},
92+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'problem',
5+
docs: {
6+
description: 'disallow positional params in LinkTo component',
7+
category: 'Deprecations',
8+
strictGjs: true,
9+
strictGts: true,
10+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-link-to-positional-params.md',
11+
},
12+
schema: [],
13+
messages: {
14+
noLinkToPositionalParams: 'Positional params in LinkTo are deprecated. Use @route instead.',
15+
},
16+
},
17+
18+
create(context) {
19+
function isLinkToComponent(node) {
20+
if (node.type === 'GlimmerElementNode') {
21+
return node.tag === 'LinkTo' || node.tag === 'link-to';
22+
}
23+
return false;
24+
}
25+
26+
return {
27+
GlimmerElementNode(node) {
28+
if (!isLinkToComponent(node)) {
29+
return;
30+
}
31+
32+
// Check if there are positional params (non-@ attributes without = sign, which are params)
33+
const hasPositionalParams = node.attributes.some(
34+
(attr) =>
35+
attr.type === 'GlimmerAttrNode' &&
36+
!attr.name.startsWith('@') &&
37+
attr.value &&
38+
attr.value.type !== 'GlimmerTextNode' &&
39+
!node.modifiers.some((mod) => mod.path.original === attr.name)
40+
);
41+
42+
if (hasPositionalParams) {
43+
context.report({
44+
node,
45+
messageId: 'noLinkToPositionalParams',
46+
});
47+
}
48+
},
49+
};
50+
},
51+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'problem',
5+
docs: {
6+
description: 'disallow tagName attribute on LinkTo component',
7+
category: 'Deprecations',
8+
strictGjs: true,
9+
strictGts: true,
10+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-link-to-tagname.md',
11+
},
12+
schema: [],
13+
messages: {
14+
noLinkToTagname: 'tagName attribute on LinkTo is deprecated',
15+
},
16+
},
17+
18+
create(context) {
19+
function isLinkToComponent(node) {
20+
if (node.type === 'GlimmerElementNode') {
21+
return node.tag === 'LinkTo' || node.tag === 'link-to';
22+
}
23+
return false;
24+
}
25+
26+
return {
27+
GlimmerElementNode(node) {
28+
if (!isLinkToComponent(node)) {
29+
return;
30+
}
31+
32+
const tagNameAttr = node.attributes.find(
33+
(attr) =>
34+
attr.type === 'GlimmerAttrNode' &&
35+
(attr.name === 'tagName' || attr.name === '@tagName')
36+
);
37+
38+
if (tagNameAttr) {
39+
context.report({
40+
node: tagNameAttr,
41+
messageId: 'noLinkToTagname',
42+
});
43+
}
44+
},
45+
};
46+
},
47+
};

lib/strict-rules-gjs.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,38 @@ module.exports = {
2020
"ember/template-no-duplicate-id": "error",
2121
"ember/template-no-empty-headings": "error",
2222
"ember/template-no-heading-inside-button": "error",
23+
"ember/template-no-html-comments": "error",
24+
"ember/template-no-implicit-this": "error",
2325
"ember/template-no-input-block": "error",
2426
"ember/template-no-input-tagname": "error",
27+
"ember/template-no-invalid-aria-attributes": "error",
28+
"ember/template-no-invalid-interactive": "error",
29+
"ember/template-no-invalid-link-title": "error",
30+
"ember/template-no-link-to-positional-params": "error",
31+
"ember/template-no-link-to-tagname": "error",
2532
"ember/template-no-log": "error",
33+
"ember/template-no-negated-condition": "error",
2634
"ember/template-no-obsolete-elements": "error",
35+
"ember/template-no-outlet-outside-routes": "error",
2736
"ember/template-no-partial": "error",
2837
"ember/template-no-positive-tabindex": "error",
2938
"ember/template-no-this-in-template-only-components": "error",
3039
"ember/template-no-triple-curlies": "error",
3140
"ember/template-no-unbound": "error",
41+
"ember/template-no-unnecessary-component-helper": "error",
3242
"ember/template-no-unnecessary-concat": "error",
43+
"ember/template-no-unnecessary-curly-in-string-attrs": "error",
44+
"ember/template-no-unnecessary-curly-strings": "error",
45+
"ember/template-no-unknown-arguments-for-builtin-components": "error",
46+
"ember/template-no-unused-block-params": "error",
3347
"ember/template-no-valueless-arguments": "error",
48+
"ember/template-no-whitespace-within-word": "error",
3449
"ember/template-no-with": "error",
3550
"ember/template-require-button-type": "error",
51+
"ember/template-require-each-key": "error",
3652
"ember/template-require-iframe-title": "error",
53+
"ember/template-require-input-label": "error",
3754
"ember/template-require-valid-alt-text": "error",
55+
"ember/template-simple-unless": "error",
3856
"ember/template-splat-attributes-only": "error"
3957
}

0 commit comments

Comments
 (0)