Skip to content

Commit e4a1d9f

Browse files
authored
Merge pull request #5 from github/kh-add_aria_label_misuse_linter
Introduce linter to prevent aria-label misuse
2 parents 8c0182d + e6d0ef5 commit e4a1d9f

File tree

4 files changed

+169
-1
lines changed

4 files changed

+169
-1
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ require "erblint-github/linters"
2525
linters:
2626
GitHub::Accessibility::ImageHasAlt:
2727
enabled: true
28+
GitHub::Accessibility::NoAriaLabelMisuse:
29+
enabled: true
2830
GitHub::Accessibility::NoRedundantImageAlt:
2931
enabled: true
3032
```
3133
3234
### Rules
3335
36+
- [GitHub::Accessibility::ImageHasAlt](./docs/rules/accessibility/no-aria-label-misuse.md)
37+
- [GitHub::Accessibility::NoAriaLabelMisuse](./docs/rules/accessibility/image-has-alt.md)
3438
- [GitHub::Accessibility::NoRedundantImageAlt](./docs/rules/accessibility/no-redundant-image-alt.md)
35-
- [GitHub::Accessibility::ImageHasAlt](./docs/rules/accessibility/image-has-alt.md)
3639
3740
## Testing
3841
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# No aria label misuse
2+
3+
## Rule Details
4+
5+
This rule aims to minimize misuse of the `aria-label` and `aria-labelledby` attributes because the usage of these attributes is only guaranteed on interactive elements and a subset of ARIA roles. W3C provides [a list of ARIA roles which cannot be named](https://w3c.github.io/aria/#namefromprohibited) which is used as a basis for this linter.
6+
7+
There are conflicting resources on what elements should support these naming attributes. For now, this rule will operate under a relatively simple heuristic aimed to minimize false positives, but has room for future improvements.
8+
9+
Learn more at [W3C Name Calcluation](https://w3c.github.io/aria/#namecalculation).
10+
11+
Also check out the following resources:
12+
- [w3c/aria Consider prohibiting author naming certain roles #833](https://github.com/w3c/aria/issues/833)
13+
- [Not so short note on aria-label usage - Big Table Edition](https://html5accessibility.com/stuff/2020/11/07/not-so-short-note-on-aria-label-usage-big-table-edition/)
14+
15+
👎 Examples of **incorrect** code for this rule:
16+
17+
```erb
18+
<span aria-label="This does something">Hello</span>
19+
```
20+
21+
```erb
22+
<div aria-labelledby="heading1">Goodbye</div>
23+
```
24+
25+
```erb
26+
<h1 aria-label="This will override the content">Page title</h1>
27+
```
28+
29+
👍 Examples of **correct** code for this rule:
30+
31+
```erb
32+
<span>Hello</span>
33+
```
34+
35+
```erb
36+
<div>Goodbye</div>
37+
```
38+
39+
```erb
40+
<h1>Page title</h1>
41+
```
42+
43+
```erb
44+
<div role="dialog" aria-labelledby="dialogHeading"></div>
45+
```
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../../custom_helpers"
4+
5+
module ERBLint
6+
module Linters
7+
module GitHub
8+
module Accessibility
9+
class NoAriaLabelMisuse < Linter
10+
include ERBLint::Linters::CustomHelpers
11+
include LinterRegistry
12+
13+
GENERIC_ELEMENTS = %w[span div].freeze
14+
NAME_RESTRICTED_ELEMENTS = %w[h1 h2 h3 h4 h5 h6 strong i p b code].freeze
15+
16+
# https://w3c.github.io/aria/#namefromprohibited
17+
ROLES_WHICH_CANNOT_BE_NAMED = %w[caption code definition deletion emphasis insertion mark none paragraph presentation strong subscript suggestion superscript term time].freeze
18+
19+
MESSAGE = "[aria-label] and [aria-labelledby] usage are only reliably supported on interactive elements and a subset of ARIA roles"
20+
21+
def run(processed_source)
22+
tags(processed_source).each do |tag|
23+
next if tag.closing?
24+
next unless possible_attribute_values(tag, "aria-label").present? || possible_attribute_values(tag, "aria-labelledby").present?
25+
26+
if NAME_RESTRICTED_ELEMENTS.include?(tag.name)
27+
generate_offense(self.class, processed_source, tag)
28+
elsif GENERIC_ELEMENTS.include?(tag.name)
29+
role = possible_attribute_values(tag, "role")
30+
if role.present?
31+
generate_offense(self.class, processed_source, tag) if ROLES_WHICH_CANNOT_BE_NAMED.include?(role.join)
32+
else
33+
generate_offense(self.class, processed_source, tag)
34+
end
35+
end
36+
end
37+
rule_disabled?(processed_source)
38+
end
39+
end
40+
end
41+
end
42+
end
43+
end
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class NoAriaLabelMisuseTest < LinterTestCase
6+
def linter_class
7+
ERBLint::Linters::GitHub::Accessibility::NoAriaLabelMisuse
8+
end
9+
10+
def example_invalid_case
11+
<<~HTML
12+
<span aria-labelledby='unique-id'>This is a span</span>
13+
HTML
14+
end
15+
16+
def test_warns_if_banned_elements_have_aria_label
17+
@file = <<~HTML
18+
<h1 aria-label="some label"></h1>
19+
<h2 aria-labelledby="label1"></h2>
20+
<h3 aria-label="some label"></h3>
21+
<h4 aria-labelledby="label2"></h4>
22+
<h5 aria-label="some label"></h5>
23+
<h6 aria-labelledby="label3"></h6>
24+
<strong aria-label="some label"></strong>
25+
<i aria-labelledby="label5"></i>
26+
<p aria-label="some label"></p>
27+
<b aria-labelledby="label4"></b>
28+
<code aria-label="some label"></code>
29+
HTML
30+
31+
@linter.run(processed_source)
32+
assert_equal @linter.offenses.length, 11
33+
end
34+
35+
def test_warns_if_generic_elements_have_aria_label_and_no_role
36+
@file = <<~HTML
37+
<span aria-labelledby="unique-id">This is a span</span>
38+
<div aria-label="text">This is a div</div>
39+
HTML
40+
41+
@linter.run(processed_source)
42+
assert_equal 2, @linter.offenses.length
43+
end
44+
45+
def test_warns_if_generic_elements_have_aria_label_and_prohibited_role
46+
@file = <<~HTML
47+
<span role="caption" aria-labelledby="unique-id">This is a span</span>
48+
<div role="code" aria-label="text">This is a div</div>
49+
<span role="definition" aria-labelledby="unique-id">This is a span</span>
50+
<div role="deletion" aria-label="text">This is a div</div>
51+
<span role="insertion" aria-labelledby="unique-id">This is a span</span>
52+
<div role="mark" aria-label="text">This is a div</div>
53+
<span role="paragraph" aria-labelledby="unique-id">This is a span</span>
54+
<div role="presentation" aria-label="text">This is a div</div>
55+
<span role="strong" aria-labelledby="unique-id">This is a span</span>
56+
<div role="subscript" aria-label="text">This is a div</div>
57+
<span role="suggestion" aria-labelledby="unique-id">This is a span</span>
58+
<div role="superscript" aria-label="text">This is a div</div>
59+
<span role="term" aria-labelledby="unique-id">This is a span</span>
60+
<div role="time" aria-label="text">This is a div</div>
61+
HTML
62+
63+
@linter.run(processed_source)
64+
assert_equal 14, @linter.offenses.length
65+
end
66+
67+
def test_does_not_warn_if_generic_elements_have_aria_label_and_allowed_role
68+
@file = <<~HTML
69+
<div role="banner" aria-label="text"></div>
70+
<div role="button" aria-label="text 1"></div>
71+
<div role="combobox" aria-label="text 2"></div>
72+
HTML
73+
74+
@linter.run(processed_source)
75+
assert_empty @linter.offenses
76+
end
77+
end

0 commit comments

Comments
 (0)