Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ require "erblint-github/linters"
linters:
GitHub::Accessibility::ImageHasAlt:
enabled: true
GitHub::Accessibility::NoAriaLabelMisuse:
enabled: true
GitHub::Accessibility::NoRedundantImageAlt:
enabled: true
```

### Rules

- [GitHub::Accessibility::ImageHasAlt](./docs/rules/accessibility/no-aria-label-misuse.md)
- [GitHub::Accessibility::NoAriaLabelMisuse](./docs/rules/accessibility/image-has-alt.md)
- [GitHub::Accessibility::NoRedundantImageAlt](./docs/rules/accessibility/no-redundant-image-alt.md)
- [GitHub::Accessibility::ImageHasAlt](./docs/rules/accessibility/image-has-alt.md)

## Testing

Expand Down
45 changes: 45 additions & 0 deletions docs/rules/accessibility/no-aria-label-misuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# No aria label misuse

## Rule Details

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.

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.

Learn more at [W3C Name Calcluation](https://w3c.github.io/aria/#namecalculation).

Also check out the following resources:
- [w3c/aria Consider prohibiting author naming certain roles #833](https://github.com/w3c/aria/issues/833)
- [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/)

👎 Examples of **incorrect** code for this rule:

```erb
<span aria-label="This does something">Hello</span>
```

```erb
<div aria-labelledby="heading1">Goodbye</div>
```

```erb
<h1 aria-label="This will override the content">Page title</h1>
```

👍 Examples of **correct** code for this rule:

```erb
<span>Hello</span>
```

```erb
<div>Goodbye</div>
```

```erb
<h1>Page title</h1>
```

```erb
<div role="dialog" aria-labelledby="dialogHeading"></div>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require_relative "../../custom_helpers"

module ERBLint
module Linters
module GitHub
module Accessibility
class NoAriaLabelMisuse < Linter
include ERBLint::Linters::CustomHelpers
include LinterRegistry

GENERIC_ELEMENTS = %w[span div].freeze
NAME_RESTRICTED_ELEMENTS = %w[h1 h2 h3 h4 h5 h6 strong i p b code].freeze

# https://w3c.github.io/aria/#namefromprohibited
ROLES_WHICH_CANNOT_BE_NAMED = %w[caption code definition deletion emphasis insertion mark none paragraph presentation strong subscript suggestion superscript term time].freeze

MESSAGE = "[aria-label] and [aria-labelledby] usage are only reliably supported on interactive elements and a subset of ARIA roles"

def run(processed_source)
tags(processed_source).each do |tag|
next if tag.closing?
next unless possible_attribute_values(tag, "aria-label").present? || possible_attribute_values(tag, "aria-labelledby").present?

if NAME_RESTRICTED_ELEMENTS.include?(tag.name)
generate_offense(self.class, processed_source, tag)
elsif GENERIC_ELEMENTS.include?(tag.name)
role = possible_attribute_values(tag, "role")
if role.present?
generate_offense(self.class, processed_source, tag) if ROLES_WHICH_CANNOT_BE_NAMED.include?(role.join)
else
generate_offense(self.class, processed_source, tag)
end
end
end
rule_disabled?(processed_source)
end
end
end
end
end
end
77 changes: 77 additions & 0 deletions test/linters/accessibility/no_aria_label_misuse_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

require "test_helper"

class NoAriaLabelMisuseTest < LinterTestCase
def linter_class
ERBLint::Linters::GitHub::Accessibility::NoAriaLabelMisuse
end

def example_invalid_case
<<~HTML
<span aria-labelledby='unique-id'>This is a span</span>
HTML
end

def test_warns_if_banned_elements_have_aria_label
@file = <<~HTML
<h1 aria-label="some label"></h1>
<h2 aria-labelledby="label1"></h2>
<h3 aria-label="some label"></h3>
<h4 aria-labelledby="label2"></h4>
<h5 aria-label="some label"></h5>
<h6 aria-labelledby="label3"></h6>
<strong aria-label="some label"></strong>
<i aria-labelledby="label5"></i>
<p aria-label="some label"></p>
<b aria-labelledby="label4"></b>
<code aria-label="some label"></code>
HTML

@linter.run(processed_source)
assert_equal @linter.offenses.length, 11
end

def test_warns_if_generic_elements_have_aria_label_and_no_role
@file = <<~HTML
<span aria-labelledby="unique-id">This is a span</span>
<div aria-label="text">This is a div</div>
HTML

@linter.run(processed_source)
assert_equal 2, @linter.offenses.length
end

def test_warns_if_generic_elements_have_aria_label_and_prohibited_role
@file = <<~HTML
<span role="caption" aria-labelledby="unique-id">This is a span</span>
<div role="code" aria-label="text">This is a div</div>
<span role="definition" aria-labelledby="unique-id">This is a span</span>
<div role="deletion" aria-label="text">This is a div</div>
<span role="insertion" aria-labelledby="unique-id">This is a span</span>
<div role="mark" aria-label="text">This is a div</div>
<span role="paragraph" aria-labelledby="unique-id">This is a span</span>
<div role="presentation" aria-label="text">This is a div</div>
<span role="strong" aria-labelledby="unique-id">This is a span</span>
<div role="subscript" aria-label="text">This is a div</div>
<span role="suggestion" aria-labelledby="unique-id">This is a span</span>
<div role="superscript" aria-label="text">This is a div</div>
<span role="term" aria-labelledby="unique-id">This is a span</span>
<div role="time" aria-label="text">This is a div</div>
HTML

@linter.run(processed_source)
assert_equal 14, @linter.offenses.length
end

def test_does_not_warn_if_generic_elements_have_aria_label_and_allowed_role
@file = <<~HTML
<div role="banner" aria-label="text"></div>
<div role="button" aria-label="text 1"></div>
<div role="combobox" aria-label="text 2"></div>
HTML

@linter.run(processed_source)
assert_empty @linter.offenses
end
end