Skip to content

Commit bf38786

Browse files
committed
Extract rule: template-require-iframe-title
1 parent a1378e5 commit bf38786

4 files changed

Lines changed: 244 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ rules in templates can be disabled with eslint directives with mustache or html
197197
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | | | |
198198
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | | | |
199199
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | | | |
200+
| [template-require-iframe-title](docs/rules/template-require-iframe-title.md) | require iframe elements to have a title attribute | | | |
200201

201202
### Best Practices
202203

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# ember/template-require-iframe-title
2+
3+
<!-- end auto-generated rule header -->
4+
5+
## `<iframe>`
6+
7+
`<iframe>` elements must have a unique title property to indicate its content to the user.
8+
9+
This rule takes no arguments.
10+
11+
## Examples
12+
13+
This rule **allows** the following:
14+
15+
```gjs
16+
<template>
17+
<iframe title='This is a unique title' />
18+
<iframe title={{someValue}} />
19+
</template>
20+
```
21+
22+
This rule **forbids** the following:
23+
24+
```gjs
25+
<template>
26+
<iframe />
27+
<iframe title='' />
28+
</template>
29+
```
30+
31+
## References
32+
33+
- [Deque University](https://dequeuniversity.com/rules/axe/1.1/frame-title)
34+
- [Technique H65: Using the title attribute of the frame and iframe elements](https://www.w3.org/TR/2014/NOTE-WCAG20-TECHS-20140408/H64)
35+
- [WCAG Success Criterion 2.4.1 - Bypass Blocks](https://www.w3.org/TR/UNDERSTANDING-WCAG20/navigation-mechanisms-skip.html)
36+
- [WCAG Success Criterion 4.1.2 - Name, Role, Value](https://www.w3.org/TR/UNDERSTANDING-WCAG20/ensure-compat-rsv.html)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'problem',
5+
docs: {
6+
description: 'require iframe elements to have a title attribute',
7+
category: 'Accessibility',
8+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-require-iframe-title.md',
9+
templateMode: 'both',
10+
},
11+
schema: [],
12+
messages: {
13+
missingTitle: '<iframe> elements must have a unique title property.',
14+
},
15+
originallyFrom: {
16+
name: 'ember-template-lint',
17+
rule: 'lib/rules/require-iframe-title.js',
18+
docs: 'docs/rule/require-iframe-title.md',
19+
tests: 'test/unit/rules/require-iframe-title-test.js',
20+
},
21+
},
22+
create(context) {
23+
const knownTitles = [];
24+
25+
return {
26+
GlimmerElementNode(node) {
27+
if (node.tag !== 'iframe') {
28+
return;
29+
}
30+
31+
// Skip if aria-hidden or hidden
32+
const hasAriaHidden = node.attributes?.some((a) => a.name === 'aria-hidden');
33+
const hasHidden = node.attributes?.some((a) => a.name === 'hidden');
34+
if (hasAriaHidden || hasHidden) {
35+
return;
36+
}
37+
38+
// Check for title attribute
39+
const titleAttr = node.attributes?.find((a) => a.name === 'title');
40+
if (!titleAttr) {
41+
context.report({ node, messageId: 'missingTitle' });
42+
return;
43+
}
44+
45+
if (titleAttr.value) {
46+
switch (titleAttr.value.type) {
47+
case 'GlimmerTextNode': {
48+
const value = titleAttr.value.chars.trim();
49+
if (value.length === 0) {
50+
context.report({ node, messageId: 'missingTitle' });
51+
} else {
52+
// Check for duplicate titles
53+
const existingIdx = knownTitles.findIndex(([val]) => val === value);
54+
if (existingIdx === -1) {
55+
knownTitles.push([value, node]);
56+
} else {
57+
context.report({ node, messageId: 'missingTitle' });
58+
}
59+
}
60+
break;
61+
}
62+
case 'GlimmerMustacheStatement': {
63+
// title={{false}} → BooleanLiteral false is invalid
64+
if (titleAttr.value.path?.type === 'GlimmerBooleanLiteral') {
65+
context.report({ node, messageId: 'missingTitle' });
66+
}
67+
break;
68+
}
69+
case 'GlimmerConcatStatement': {
70+
// title="{{false}}" → ConcatStatement with single BooleanLiteral part
71+
const parts = titleAttr.value.parts || [];
72+
if (
73+
parts.length === 1 &&
74+
parts[0].type === 'GlimmerMustacheStatement' &&
75+
parts[0].path?.type === 'GlimmerBooleanLiteral'
76+
) {
77+
context.report({ node, messageId: 'missingTitle' });
78+
}
79+
break;
80+
}
81+
default: {
82+
break;
83+
}
84+
}
85+
}
86+
},
87+
};
88+
},
89+
};
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
const rule = require('../../../lib/rules/template-require-iframe-title');
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-require-iframe-title', rule, {
10+
valid: [
11+
'<template><iframe title="Video"></iframe></template>',
12+
'<template><iframe title="Map" src="/map"></iframe></template>',
13+
'<template><iframe aria-hidden="true"></iframe></template>',
14+
'<template><iframe hidden></iframe></template>',
15+
16+
'<template><iframe title="Welcome to the Matrix!" /></template>',
17+
'<template><iframe title={{someValue}} /></template>',
18+
'<template><iframe title="" aria-hidden /></template>',
19+
'<template><iframe title="" hidden /></template>',
20+
'<template><iframe title="foo" /><iframe title="bar" /></template>',
21+
],
22+
invalid: [
23+
{
24+
code: '<template><iframe src="/content"></iframe></template>',
25+
output: null,
26+
errors: [{ messageId: 'missingTitle' }],
27+
},
28+
{
29+
code: '<template><iframe title=""></iframe></template>',
30+
output: null,
31+
errors: [{ messageId: 'missingTitle' }],
32+
},
33+
34+
{
35+
code: '<template><iframe title="foo" /><iframe title="foo" /></template>',
36+
output: null,
37+
errors: [{ messageId: 'missingTitle' }],
38+
},
39+
{
40+
code: '<template><iframe title="foo" /><iframe title="boo" /><iframe title="foo" /><iframe title="boo" /></template>',
41+
output: null,
42+
errors: [{ messageId: 'missingTitle' }, { messageId: 'missingTitle' }],
43+
},
44+
{
45+
code: '<template><iframe src="12" /></template>',
46+
output: null,
47+
errors: [{ messageId: 'missingTitle' }],
48+
},
49+
{
50+
code: '<template><iframe src="12" title={{false}} /></template>',
51+
output: null,
52+
errors: [{ messageId: 'missingTitle' }],
53+
},
54+
{
55+
code: '<template><iframe src="12" title="{{false}}" /></template>',
56+
output: null,
57+
errors: [{ messageId: 'missingTitle' }],
58+
},
59+
{
60+
code: '<template><iframe src="12" title="" /></template>',
61+
output: null,
62+
errors: [{ messageId: 'missingTitle' }],
63+
},
64+
],
65+
});
66+
67+
const hbsRuleTester = new RuleTester({
68+
parser: require.resolve('ember-eslint-parser/hbs'),
69+
parserOptions: {
70+
ecmaVersion: 2022,
71+
sourceType: 'module',
72+
},
73+
});
74+
75+
hbsRuleTester.run('template-require-iframe-title', rule, {
76+
valid: [
77+
'<iframe title="Welcome to the Matrix!" />',
78+
'<iframe title={{someValue}} />',
79+
'<iframe title="" aria-hidden />',
80+
'<iframe title="" hidden />',
81+
'<iframe title="foo" /><iframe title="bar" />',
82+
],
83+
invalid: [
84+
{
85+
code: '<iframe title="foo" /><iframe title="foo" />',
86+
output: null,
87+
errors: [{ message: '<iframe> elements must have a unique title property.' }],
88+
},
89+
{
90+
code: '<iframe title="foo" /><iframe title="boo" /><iframe title="foo" /><iframe title="boo" />',
91+
output: null,
92+
errors: [
93+
{ message: '<iframe> elements must have a unique title property.' },
94+
{ message: '<iframe> elements must have a unique title property.' },
95+
],
96+
},
97+
{
98+
code: '<iframe src="12" />',
99+
output: null,
100+
errors: [{ message: '<iframe> elements must have a unique title property.' }],
101+
},
102+
{
103+
code: '<iframe src="12" title={{false}} />',
104+
output: null,
105+
errors: [{ message: '<iframe> elements must have a unique title property.' }],
106+
},
107+
{
108+
code: '<iframe src="12" title="{{false}}" />',
109+
output: null,
110+
errors: [{ message: '<iframe> elements must have a unique title property.' }],
111+
},
112+
{
113+
code: '<iframe src="12" title="" />',
114+
output: null,
115+
errors: [{ message: '<iframe> elements must have a unique title property.' }],
116+
},
117+
],
118+
});

0 commit comments

Comments
 (0)