Skip to content

Commit 23ef636

Browse files
authored
Merge pull request #55 from github/kh-no-aria-hidden-on-focusable
rule: no-aria-hidden-on-focusable-elements
2 parents e9e435f + abde068 commit 23ef636

File tree

6 files changed

+148
-1
lines changed

6 files changed

+148
-1
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ linters:
3939
enabled: true
4040
GitHub::Accessibility::NestedInteractiveElementsCounter:
4141
enabled: true
42+
GitHub::Accessibility::NoAriaHiddenOnFocusableCounter:
43+
enabled: true
4244
GitHub::Accessibility::NoAriaLabelMisuseCounter:
4345
enabled: true
4446
GitHub::Accessibility::NoPositiveTabIndexCounter:
@@ -61,6 +63,7 @@ linters:
6163
- [GitHub::Accessibility::NestedInteractiveElementsCounter](./docs/rules/accessibility/nested-interactive-elements-counter.md)
6264
- [GitHub::Accessibility::IframeHasTitleCounter](./docs/rules/accessibility/iframe-has-title-counter.md)
6365
- [GitHub::Accessibility::ImageHasAltCounter](./docs/rules/accessibility/image-has-alt-counter.md)
66+
- [GitHub::Accessibility::NoAriaHiddenOnFocusableCounter](./docs/rules/accessibility/no-aria-hidden-on-focusable-counter.md)
6467
- [GitHub::Accessibility::NoAriaLabelMisuseCounter](./docs/rules/accessibility/no-aria-label-misuse-counter.md)
6568
- [GitHub::Accessibility::NoPositiveTabIndexCounter](./docs/rules/accessibility/no-positive-tab-index-counter.md)
6669
- [GitHub::Accessibility::NoRedundantImageAltCounter](./docs/rules/accessibility/no-redundant-image-alt-counter.md)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# No aria-hidden on focusable counter
2+
3+
## Rule Details
4+
5+
Elements that are focusable should never have `aria-hidden="true"` set.
6+
7+
`aria-hidden="true"` hides elements from assistive technologies. `aria-hidden="true"` should only be used to hide non-interactive content such as decorative elements or redundant text. If a focusable element has `aria-hidden="true"`, it can cause confusion amongst assistive technology users who may be able to reach the element but not receive information about it.
8+
9+
### Resources
10+
11+
- [Accessibility insights: aria-hidden-focus](https://accessibilityinsights.io/info-examples/web/aria-hidden-focus/)
12+
- [Deque: aria-hidden elements do not contain focusable elements](https://dequeuniversity.com/rules/axe/html/4.4/aria-hidden-focus)
13+
- [W3: Element with aria-hidden has no content in sequential focus navigation](https://www.w3.org/WAI/standards-guidelines/act/rules/6cfa84/proposed/)
14+
15+
## Examples
16+
17+
### **Incorrect** code for this rule 👎
18+
19+
```erb
20+
<button aria-hidden="true">Submit</button>
21+
```
22+
23+
```erb
24+
<div role="menuitem" aria-hidden="true" tabindex="0"></div>
25+
```
26+
27+
### **Correct** code for this rule 👍
28+
29+
```erb
30+
<button>Submit</button>
31+
```
32+
33+
```erb
34+
<div role="menuitem" aria-hidden="true" tabindex="-1"></div>
35+
```

lib/erblint-github/linters/custom_helpers.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
module ERBLint
77
module Linters
88
module CustomHelpers
9+
INTERACTIVE_ELEMENTS = %w[button summary input select textarea a].freeze
10+
911
def rule_disabled?(processed_source)
1012
processed_source.parser.ast.descendants(:erb).each do |node|
1113
indicator_node, _, code_node, = *node
@@ -89,6 +91,15 @@ def tags(processed_source)
8991
def simple_class_name
9092
self.class.name.gsub("ERBLint::Linters::", "")
9193
end
94+
95+
def focusable?(tag)
96+
tabindex = possible_attribute_values(tag, "tabindex")
97+
if INTERACTIVE_ELEMENTS.include?(tag.name)
98+
tabindex.empty? || tabindex.first.to_i >= 0
99+
else
100+
tabindex.any? && tabindex.first.to_i >= 0
101+
end
102+
end
92103
end
93104
end
94105
end

lib/erblint-github/linters/github/accessibility/nested_interactive_elements_counter.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ class NestedInteractiveElementsCounter < Linter
1010
include ERBLint::Linters::CustomHelpers
1111
include LinterRegistry
1212

13-
INTERACTIVE_ELEMENTS = %w[button summary input select textarea a].freeze
1413
MESSAGE = "Nesting interactive elements produces invalid HTML, and ssistive technologies, such as screen readers, might ignore or respond unexpectedly to such nested controls."
1514

1615
def run(processed_source)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 NoAriaHiddenOnFocusableCounter < Linter
10+
include ERBLint::Linters::CustomHelpers
11+
include LinterRegistry
12+
13+
MESSAGE = "Elements that are focusable should not have `aria-hidden='true' because it will cause confusion for assistive technology users."
14+
15+
def run(processed_source)
16+
tags(processed_source).each do |tag|
17+
aria_hidden = possible_attribute_values(tag, "aria-hidden")
18+
generate_offense(self.class, processed_source, tag) if aria_hidden.include?("true") && focusable?(tag)
19+
end
20+
21+
counter_correct?(processed_source)
22+
end
23+
24+
def autocorrect(processed_source, offense)
25+
return unless offense.context
26+
27+
lambda do |corrector|
28+
if processed_source.file_content.include?("erblint:counter #{simple_class_name}")
29+
# update the counter if exists
30+
corrector.replace(offense.source_range, offense.context)
31+
else
32+
# add comment with counter if none
33+
corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
34+
end
35+
end
36+
end
37+
end
38+
end
39+
end
40+
end
41+
end
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class NoAriaHiddenOnFocusableCounterTest < LinterTestCase
6+
def linter_class
7+
ERBLint::Linters::GitHub::Accessibility::NoAriaHiddenOnFocusableCounter
8+
end
9+
10+
def test_does_not_warn_if_link_does_not_have_aria_hidden
11+
@file = "<a href='github.com'>GitHub</a>"
12+
@linter.run(processed_source)
13+
14+
assert_empty @linter.offenses
15+
end
16+
17+
def test_does_not_consider_aria_hidden_as_aria_hidden_true
18+
# aria-hidden is not the same as aria-hidden="true". Not ideal code.
19+
@file = "<a aria-hidden href='github.com'>GitHub</a>"
20+
@linter.run(processed_source)
21+
22+
assert_empty @linter.offenses
23+
end
24+
25+
def test_does_not_warn_if_link_has_aria_hidden_false
26+
@file = "<a aria-hidden='false' href='github.com'>GitHub</a>"
27+
@linter.run(processed_source)
28+
29+
assert_empty @linter.offenses
30+
end
31+
32+
def test_does_not_warn_when_link_has_aria_hidden_true_and_is_not_focusable
33+
@file = "<a aria-hidden='true' tabindex='-1' href='github.com'>GitHub</a>"
34+
@linter.run(processed_source)
35+
36+
assert_empty @linter.offenses
37+
end
38+
39+
def test_warns_when_element_has_aria_hidden_true_and_not_tab_focusable
40+
@file = "<div role='button' tabindex='0' aria-hidden='true'>GitHub</a>"
41+
@linter.run(processed_source)
42+
refute_empty @linter.offenses
43+
end
44+
45+
def test_warns_when_link_has_aria_hidden_true
46+
@file = "<a aria-hidden='true' href='github.com'>GitHub</a>"
47+
@linter.run(processed_source)
48+
49+
refute_empty @linter.offenses
50+
end
51+
52+
def test_warns_when_element_has_aria_hidden_true_and_is_tab_focusable
53+
@file = "<div role='list' aria-hidden='true' tabindex='0'></div>"
54+
@linter.run(processed_source)
55+
56+
refute_empty @linter.offenses
57+
end
58+
end

0 commit comments

Comments
 (0)