Skip to content

Commit 9d6ee01

Browse files
committed
Extract rule: template-no-heading-inside-button
1 parent 0149ef1 commit 9d6ee01

File tree

4 files changed

+157
-0
lines changed

4 files changed

+157
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ rules in templates can be disabled with eslint directives with mustache or html
186186
| [template-no-aria-hidden-body](docs/rules/template-no-aria-hidden-body.md) | disallow aria-hidden on body element | | 🔧 | |
187187
| [template-no-aria-unsupported-elements](docs/rules/template-no-aria-unsupported-elements.md) | disallow ARIA roles, states, and properties on elements that do not support them | | | |
188188
| [template-no-autofocus-attribute](docs/rules/template-no-autofocus-attribute.md) | disallow autofocus attribute | | 🔧 | |
189+
| [template-no-heading-inside-button](docs/rules/template-no-heading-inside-button.md) | disallow heading elements inside button elements | | | |
189190

190191
### Best Practices
191192

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# ember/template-no-heading-inside-button
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Assistive technology allows users to browse a page by heading elements (`<h1>` - `<h6>`). However, if those heading elements are nested inside of button elements, they will automatically be marked as presentational by browsers. Any HTML element where ["children presentational" is true](https://w3c.github.io/aria/#button) should be coerced by the browser to be presentational, and therefore not included in the accessibility tree.
6+
7+
As such, nesting a heading element inside of a button element will cause failures for WCAG requirement 1.3.1, Info and Relationships, because the heading has lost semantic meaning.
8+
9+
This rule checks `<button>` elements to see if they contain heading (`<h1>` - `<h6>`) elements, and gives an error message if they are found.
10+
11+
## Examples
12+
13+
This rule **forbids** the following:
14+
15+
```gjs
16+
<template><button><h1>Some Text</h1></button></template>
17+
```
18+
19+
This rule **allows** the following:
20+
21+
```gjs
22+
<template><button><span>Button Text</span></button></template>
23+
```
24+
25+
## Migration
26+
27+
- Replace `<h1>` - `<h6>` elements inside of `<button>` elements with classes that reflect the desired styling.
28+
29+
## References
30+
31+
- <https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html>
32+
- <https://w3c.github.io/aria/#button>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const HEADING_ELEMENTS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
2+
3+
function hasButtonParent(node) {
4+
let parent = node.parent;
5+
while (parent) {
6+
if (parent.type === 'GlimmerElementNode') {
7+
// Check if it's a button element
8+
if (parent.tag === 'button') {
9+
return true;
10+
}
11+
// Check if it has role="button"
12+
const roleAttr = parent.attributes?.find((a) => a.name === 'role');
13+
if (roleAttr?.value?.type === 'GlimmerTextNode' && roleAttr.value.chars === 'button') {
14+
return true;
15+
}
16+
}
17+
parent = parent.parent;
18+
}
19+
return false;
20+
}
21+
22+
/** @type {import('eslint').Rule.RuleModule} */
23+
module.exports = {
24+
meta: {
25+
type: 'problem',
26+
docs: {
27+
description: 'disallow heading elements inside button elements',
28+
category: 'Accessibility',
29+
strictGjs: true,
30+
strictGts: true,
31+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-heading-inside-button.md',
32+
},
33+
schema: [],
34+
messages: {
35+
noHeading: 'Buttons should not contain heading elements',
36+
},
37+
},
38+
create(context) {
39+
return {
40+
GlimmerElementNode(node) {
41+
if (HEADING_ELEMENTS.has(node.tag) && hasButtonParent(node)) {
42+
context.report({ node, messageId: 'noHeading' });
43+
}
44+
},
45+
};
46+
},
47+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const rule = require('../../../lib/rules/template-no-heading-inside-button');
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-heading-inside-button', rule, {
10+
valid: [
11+
'<template><button>Click me</button></template>',
12+
'<template><h1>Title</h1></template>',
13+
'<template><div><h2>Heading</h2></div></template>',
14+
15+
// Test cases ported from ember-template-lint
16+
'<template><button>Show More</button></template>',
17+
'<template><button><span>thumbs-up emoji</span>Show More</button></template>',
18+
'<template><button><div>Show More</div></button></template>',
19+
'<template><div>Showing that it is not a button</div></template>',
20+
'<template><div><h1>Page Title in a div is fine</h1></div></template>',
21+
'<template><h1>Page Title</h1></template>',
22+
],
23+
invalid: [
24+
{
25+
code: '<template><button><h1>Bad</h1></button></template>',
26+
output: null,
27+
errors: [{ messageId: 'noHeading' }],
28+
},
29+
{
30+
code: '<template><div role="button"><h2>Bad</h2></div></template>',
31+
output: null,
32+
errors: [{ messageId: 'noHeading' }],
33+
},
34+
35+
// Test cases ported from ember-template-lint
36+
{
37+
code: '<template><button><h1>Page Title</h1></button></template>',
38+
output: null,
39+
errors: [{ messageId: 'noHeading' }],
40+
},
41+
{
42+
code: '<template><button><h2>Heading Title</h2></button></template>',
43+
output: null,
44+
errors: [{ messageId: 'noHeading' }],
45+
},
46+
{
47+
code: '<template><button><h3>Heading Title</h3></button></template>',
48+
output: null,
49+
errors: [{ messageId: 'noHeading' }],
50+
},
51+
{
52+
code: '<template><button><h4>Heading Title</h4></button></template>',
53+
output: null,
54+
errors: [{ messageId: 'noHeading' }],
55+
},
56+
{
57+
code: '<template><button><h5>Heading Title</h5></button></template>',
58+
output: null,
59+
errors: [{ messageId: 'noHeading' }],
60+
},
61+
{
62+
code: '<template><button><div><h1>Heading Title</h1></div></button></template>',
63+
output: null,
64+
errors: [{ messageId: 'noHeading' }],
65+
},
66+
{
67+
code: '<template><button><h6>Heading Title</h6></button></template>',
68+
output: null,
69+
errors: [{ messageId: 'noHeading' }],
70+
},
71+
{
72+
code: '<template><div role="button"><h6>Heading in a div with a role of button</h6></div></template>',
73+
output: null,
74+
errors: [{ messageId: 'noHeading' }],
75+
},
76+
],
77+
});

0 commit comments

Comments
 (0)