-
-
Notifications
You must be signed in to change notification settings - Fork 400
feat: Add: frame-title-require
rule
#1629
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,7 @@ | |
"idclassaddisabled", | ||
"Infima", | ||
"invision", | ||
"Labelledby", | ||
"langtag", | ||
"mingo", | ||
"msapplication", | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { Rule } from '../types' | ||
|
||
export default { | ||
id: 'frame-title-require', | ||
description: 'A <frame> or <iframe> element must have an accessible name.', | ||
init(parser, reporter) { | ||
parser.addListener('tagstart', (event) => { | ||
const tagName = event.tagName.toLowerCase() | ||
const mapAttrs = parser.getMapAttrs(event.attrs) | ||
const col = event.col + tagName.length + 1 | ||
|
||
if (tagName === 'frame' || tagName === 'iframe') { | ||
// Check if element has role="presentation" or role="none" | ||
const role = mapAttrs['role'] | ||
if (role === 'presentation' || role === 'none') { | ||
return // Rule passes - element semantics are overridden | ||
} | ||
|
||
// Check for accessible name attributes | ||
const hasAriaLabel = | ||
'aria-label' in mapAttrs && mapAttrs['aria-label'].trim() !== '' | ||
const hasAriaLabelledby = | ||
'aria-labelledby' in mapAttrs && | ||
mapAttrs['aria-labelledby'].trim() !== '' | ||
coliff marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const hasTitle = 'title' in mapAttrs && mapAttrs['title'].trim() !== '' | ||
coliff marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (!hasAriaLabel && !hasAriaLabelledby && !hasTitle) { | ||
reporter.warn( | ||
`A <${tagName}> element must have an accessible name.`, | ||
event.line, | ||
col, | ||
this, | ||
event.raw | ||
) | ||
} | ||
} | ||
}) | ||
}, | ||
} as Rule |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
const HTMLHint = require('../../dist/htmlhint.js').HTMLHint | ||
|
||
const ruleId = 'frame-title-require' | ||
const ruleOptions = {} | ||
|
||
ruleOptions[ruleId] = true | ||
|
||
describe(`Rules: ${ruleId}`, () => { | ||
it('Frame with aria-label should not result in an error', () => { | ||
const code = '<frame src="test.html" aria-label="Test frame">' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(0) | ||
}) | ||
|
||
it('Iframe with aria-label should not result in an error', () => { | ||
const code = '<iframe src="test.html" aria-label="Test iframe"></iframe>' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(0) | ||
}) | ||
|
||
it('Frame with aria-labelledby should not result in an error', () => { | ||
const code = '<frame src="test.html" aria-labelledby="label1">' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(0) | ||
}) | ||
|
||
it('Iframe with aria-labelledby should not result in an error', () => { | ||
const code = '<iframe src="test.html" aria-labelledby="label1"></iframe>' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(0) | ||
}) | ||
|
||
it('Frame with title should not result in an error', () => { | ||
const code = '<frame src="test.html" title="Test frame">' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(0) | ||
}) | ||
|
||
it('Iframe with title should not result in an error', () => { | ||
const code = '<iframe src="test.html" title="Test iframe"></iframe>' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(0) | ||
}) | ||
|
||
it('Frame with role="presentation" should not result in an error', () => { | ||
const code = '<frame src="test.html" role="presentation">' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(0) | ||
}) | ||
|
||
it('Iframe with role="none" should not result in an error', () => { | ||
const code = '<iframe src="test.html" role="none"></iframe>' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(0) | ||
}) | ||
|
||
it('Frame without accessible name should result in an error', () => { | ||
const code = '<frame src="test.html">' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(1) | ||
expect(messages[0].rule.id).toBe(ruleId) | ||
expect(messages[0].line).toBe(1) | ||
expect(messages[0].col).toBe(7) | ||
expect(messages[0].type).toBe('warning') | ||
expect(messages[0].message).toBe( | ||
'A <frame> element must have an accessible name.' | ||
) | ||
}) | ||
|
||
it('Iframe without accessible name should result in an error', () => { | ||
const code = '<iframe src="test.html"></iframe>' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(1) | ||
expect(messages[0].rule.id).toBe(ruleId) | ||
expect(messages[0].line).toBe(1) | ||
expect(messages[0].col).toBe(8) | ||
expect(messages[0].type).toBe('warning') | ||
expect(messages[0].message).toBe( | ||
'A <iframe> element must have an accessible name.' | ||
) | ||
}) | ||
|
||
it('Frame with empty aria-label should result in an error', () => { | ||
const code = '<frame src="test.html" aria-label="">' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(1) | ||
expect(messages[0].rule.id).toBe(ruleId) | ||
expect(messages[0].line).toBe(1) | ||
expect(messages[0].col).toBe(7) | ||
expect(messages[0].type).toBe('warning') | ||
}) | ||
|
||
it('Iframe with empty title should result in an error', () => { | ||
const code = '<iframe src="test.html" title=""></iframe>' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(1) | ||
expect(messages[0].rule.id).toBe(ruleId) | ||
expect(messages[0].line).toBe(1) | ||
expect(messages[0].col).toBe(8) | ||
expect(messages[0].type).toBe('warning') | ||
}) | ||
|
||
it('Frame with whitespace-only aria-label should result in an error', () => { | ||
const code = '<frame src="test.html" aria-label=" ">' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(1) | ||
expect(messages[0].rule.id).toBe(ruleId) | ||
expect(messages[0].line).toBe(1) | ||
expect(messages[0].col).toBe(7) | ||
expect(messages[0].type).toBe('warning') | ||
}) | ||
|
||
it('Iframe with whitespace-only aria-labelledby should result in an error', () => { | ||
const code = '<iframe src="test.html" aria-labelledby=" "></iframe>' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(1) | ||
expect(messages[0].rule.id).toBe(ruleId) | ||
expect(messages[0].line).toBe(1) | ||
expect(messages[0].col).toBe(8) | ||
expect(messages[0].type).toBe('warning') | ||
}) | ||
|
||
it('Multiple iframes - one valid, one invalid should result in one error', () => { | ||
const code = | ||
'<iframe src="test1.html" title="Valid frame"></iframe><iframe src="test2.html"></iframe>' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(1) | ||
expect(messages[0].rule.id).toBe(ruleId) | ||
expect(messages[0].line).toBe(1) | ||
expect(messages[0].col).toBe(62) | ||
expect(messages[0].type).toBe('warning') | ||
}) | ||
|
||
it('Other elements should not be affected', () => { | ||
const code = '<div><p>Content</p></div>' | ||
const messages = HTMLHint.verify(code, ruleOptions) | ||
expect(messages.length).toBe(0) | ||
}) | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
--- | ||
id: frame-title-require | ||
title: frame-title-require | ||
description: Requires that frame and iframe elements have an accessible name for screen readers and assistive technologies. | ||
sidebar: | ||
hidden: true | ||
badge: New | ||
--- | ||
|
||
import { Badge } from '@astrojs/starlight/components'; | ||
|
||
A `<frame>` or `<iframe>` element must have an accessible name. | ||
|
||
Level: <Badge text="Warning" variant="caution" /> | ||
|
||
## Config value | ||
|
||
- `true`: enable rule | ||
- `false`: disable rule | ||
|
||
### The following patterns are **not** considered rule violations | ||
|
||
```html | ||
<iframe src="content.html" aria-label="Interactive content"></iframe> | ||
<iframe src="content.html" aria-labelledby="frame-heading"></iframe> | ||
<iframe src="content.html" title="Embedded content"></iframe> | ||
<iframe src="content.html" role="presentation"></iframe> | ||
<frame src="content.html" title="Navigation frame"> | ||
``` | ||
|
||
### The following patterns are considered rule violations | ||
|
||
```html | ||
<iframe src="content.html"></iframe> | ||
<iframe src="content.html" aria-label=""></iframe> | ||
<iframe src="content.html" title=""></iframe> | ||
<frame src="content.html"> | ||
``` |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.