Skip to content

Commit 8d97621

Browse files
Add accessibility rule: no-empty-headings (35/127)
Co-authored-by: NullVoxPopuli <[email protected]>
1 parent 2d03b5a commit 8d97621

7 files changed

Lines changed: 126 additions & 2 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ rules in templates can be disabled with eslint directives with mustache or html
205205
| [template-no-unnecessary-concat](docs/rules/template-no-unnecessary-concat.md) | disallow unnecessary string concatenation | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | 🔧 | |
206206
| [template-no-valueless-arguments](docs/rules/template-no-valueless-arguments.md) | disallow valueless named arguments | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | | |
207207
| [template-require-button-type](docs/rules/template-require-button-type.md) | require button elements to have a valid type attribute | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | 🔧 | |
208+
| [template-splat-attributes-only](docs/rules/template-splat-attributes-only.md) | disallow ...spread other than ...attributes | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | | |
208209

209210
### Components
210211

@@ -264,6 +265,7 @@ rules in templates can be disabled with eslint directives with mustache or html
264265
| [template-deprecated-inline-view-helper](docs/rules/template-deprecated-inline-view-helper.md) | disallow inline {{view}} helper | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | | |
265266
| [template-deprecated-render-helper](docs/rules/template-deprecated-render-helper.md) | disallow {{render}} helper | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | | |
266267
| [template-no-action](docs/rules/template-no-action.md) | disallow {{action}} helper | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | | |
268+
| [template-no-attrs-in-components](docs/rules/template-no-attrs-in-components.md) | disallow attrs in component templates | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | | |
267269
| [template-no-partial](docs/rules/template-no-partial.md) | disallow {{partial}} helper | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | | |
268270
| [template-no-unbound](docs/rules/template-no-unbound.md) | disallow {{unbound}} helper | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | | |
269271
| [template-no-with](docs/rules/template-no-with.md) | disallow {{with}} helper | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | | |
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# ember/template-no-attrs-in-components
2+
3+
💼 This rule is enabled in the following [configs](https://github.com/ember-cli/eslint-plugin-ember#-configurations): ![gjs logo](/docs/svgs/gjs.svg) `recommended-gjs`, ![gts logo](/docs/svgs/gts.svg) `recommended-gts`.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
## Examples
8+
9+
See ember-template-lint documentation.
10+
11+
## References
12+
13+
- [ember-template-lint no-attrs-in-components](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-attrs-in-components.md)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# ember/template-splat-attributes-only
2+
3+
💼 This rule is enabled in the following [configs](https://github.com/ember-cli/eslint-plugin-ember#-configurations): ![gjs logo](/docs/svgs/gjs.svg) `recommended-gjs`, ![gts logo](/docs/svgs/gts.svg) `recommended-gts`.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
## Examples
8+
9+
See ember-template-lint documentation.
10+
11+
## References
12+
13+
- [ember-template-lint splat-attributes-only](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/splat-attributes-only.md)

lib/recommended-rules-gjs.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
"ember/template-no-action": "error",
1414
"ember/template-no-args-paths": "error",
1515
"ember/template-no-aria-hidden-body": "error",
16+
"ember/template-no-attrs-in-components": "error",
1617
"ember/template-no-autofocus-attribute": "error",
1718
"ember/template-no-debugger": "error",
1819
"ember/template-no-duplicate-attributes": "error",
@@ -33,5 +34,6 @@ module.exports = {
3334
"ember/template-no-with": "error",
3435
"ember/template-require-button-type": "error",
3536
"ember/template-require-iframe-title": "error",
36-
"ember/template-require-valid-alt-text": "error"
37+
"ember/template-require-valid-alt-text": "error",
38+
"ember/template-splat-attributes-only": "error"
3739
}

lib/recommended-rules-gts.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
"ember/template-no-action": "error",
1414
"ember/template-no-args-paths": "error",
1515
"ember/template-no-aria-hidden-body": "error",
16+
"ember/template-no-attrs-in-components": "error",
1617
"ember/template-no-autofocus-attribute": "error",
1718
"ember/template-no-debugger": "error",
1819
"ember/template-no-duplicate-attributes": "error",
@@ -33,5 +34,6 @@ module.exports = {
3334
"ember/template-no-with": "error",
3435
"ember/template-require-button-type": "error",
3536
"ember/template-require-iframe-title": "error",
36-
"ember/template-require-valid-alt-text": "error"
37+
"ember/template-require-valid-alt-text": "error",
38+
"ember/template-splat-attributes-only": "error"
3739
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const HEADINGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
2+
3+
/** @type {import('eslint').Rule.RuleModule} */
4+
module.exports = {
5+
meta: {
6+
type: 'problem',
7+
docs: {
8+
description: 'disallow empty heading elements',
9+
category: 'Accessibility',
10+
recommendedGjs: true,
11+
recommendedGts: true,
12+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-empty-headings.md',
13+
},
14+
schema: [],
15+
messages: {
16+
emptyHeading:
17+
'Headings must contain accessible text content (or helper/component that provides text).',
18+
},
19+
},
20+
create(context) {
21+
function hasTextContent(node) {
22+
if (!node.children || node.children.length === 0) return false;
23+
24+
for (const child of node.children) {
25+
// Text nodes with content
26+
if (child.type === 'GlimmerTextNode' && child.chars.trim().length > 0) {
27+
return true;
28+
}
29+
// Mustache statements or helpers
30+
if (
31+
child.type === 'GlimmerMustacheStatement' ||
32+
child.type === 'GlimmerBlockStatement'
33+
) {
34+
return true;
35+
}
36+
// Nested elements
37+
if (child.type === 'GlimmerElementNode' && hasTextContent(child)) {
38+
return true;
39+
}
40+
}
41+
return false;
42+
}
43+
44+
return {
45+
GlimmerElementNode(node) {
46+
if (HEADINGS.has(node.tag)) {
47+
// Skip if hidden
48+
const hasHidden = node.attributes?.some((a) => a.name === 'hidden');
49+
const ariaHidden = node.attributes?.find((a) => a.name === 'aria-hidden');
50+
if (
51+
hasHidden ||
52+
(ariaHidden?.value?.type === 'GlimmerTextNode' && ariaHidden.value.chars === 'true')
53+
) {
54+
return;
55+
}
56+
57+
if (!hasTextContent(node)) {
58+
context.report({ node, messageId: 'emptyHeading' });
59+
}
60+
}
61+
},
62+
};
63+
},
64+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const rule = require('../../../lib/rules/template-no-empty-headings');
2+
const RuleTester = require('eslint').RuleTester;
3+
4+
const ruleTester = new RuleTester({
5+
parser: require.resolve('ember-eslint-parser'),
6+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
7+
});
8+
9+
ruleTester.run('template-no-empty-headings', rule, {
10+
valid: [
11+
'<template><h1>Title</h1></template>',
12+
'<template><h2>{{this.title}}</h2></template>',
13+
'<template><h3><span>Text</span></h3></template>',
14+
'<template><h4 hidden></h4></template>',
15+
],
16+
invalid: [
17+
{
18+
code: '<template><h1></h1></template>',
19+
output: null,
20+
errors: [{ messageId: 'emptyHeading' }],
21+
},
22+
{
23+
code: '<template><h2> </h2></template>',
24+
output: null,
25+
errors: [{ messageId: 'emptyHeading' }],
26+
},
27+
],
28+
});

0 commit comments

Comments
 (0)