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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ linters:
enabled: true
GitHub::Accessibility::NestedInteractiveElementsCounter:
enabled: true
GitHub::Accessibility::NoAriaHiddenOnFocusableCounter:
enabled: true
GitHub::Accessibility::NoAriaLabelMisuseCounter:
enabled: true
GitHub::Accessibility::NoPositiveTabIndexCounter:
Expand All @@ -61,6 +63,7 @@ linters:
- [GitHub::Accessibility::NestedInteractiveElementsCounter](./docs/rules/accessibility/nested-interactive-elements-counter.md)
- [GitHub::Accessibility::IframeHasTitleCounter](./docs/rules/accessibility/iframe-has-title-counter.md)
- [GitHub::Accessibility::ImageHasAltCounter](./docs/rules/accessibility/image-has-alt-counter.md)
- [GitHub::Accessibility::NoAriaHiddenOnFocusableCounter](./docs/rules/accessibility/no-aria-hidden-on-focusable-counter.md)
- [GitHub::Accessibility::NoAriaLabelMisuseCounter](./docs/rules/accessibility/no-aria-label-misuse-counter.md)
- [GitHub::Accessibility::NoPositiveTabIndexCounter](./docs/rules/accessibility/no-positive-tab-index-counter.md)
- [GitHub::Accessibility::NoRedundantImageAltCounter](./docs/rules/accessibility/no-redundant-image-alt-counter.md)
Expand Down
35 changes: 35 additions & 0 deletions docs/rules/accessibility/no-aria-hidden-on-focusable-counter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# No aria-hidden on focusable counter

## Rule Details

Elements that are focusable should never have `aria-hidden="true"` set.

`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.

### Resources

- [Accessibility insights: aria-hidden-focus](https://accessibilityinsights.io/info-examples/web/aria-hidden-focus/)
- [Deque: aria-hidden elements do not contain focusable elements](https://dequeuniversity.com/rules/axe/html/4.4/aria-hidden-focus)
- [W3: Element with aria-hidden has no content in sequential focus navigation](https://www.w3.org/WAI/standards-guidelines/act/rules/6cfa84/proposed/)

## Examples

### **Incorrect** code for this rule 👎

```erb
<button aria-hidden="true">Submit</button>
```

```erb
<div role="menuitem" aria-hidden="true" tabindex="0"></div>
```

### **Correct** code for this rule 👍

```erb
<button>Submit</button>
```

```erb
<div role="menuitem" aria-hidden="true" tabindex="-1"></div>
```
11 changes: 11 additions & 0 deletions lib/erblint-github/linters/custom_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
module ERBLint
module Linters
module CustomHelpers
INTERACTIVE_ELEMENTS = %w[button summary input select textarea a].freeze

def rule_disabled?(processed_source)
processed_source.parser.ast.descendants(:erb).each do |node|
indicator_node, _, code_node, = *node
Expand Down Expand Up @@ -89,6 +91,15 @@ def tags(processed_source)
def simple_class_name
self.class.name.gsub("ERBLint::Linters::", "")
end

def focusable?(tag)
tabindex = possible_attribute_values(tag, "tabindex")
if INTERACTIVE_ELEMENTS.include?(tag.name)
tabindex.empty? || tabindex.first.to_i >= 0
else
tabindex.any? && tabindex.first.to_i >= 0
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class NestedInteractiveElementsCounter < Linter
include ERBLint::Linters::CustomHelpers
include LinterRegistry

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

def run(processed_source)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require_relative "../../custom_helpers"

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

MESSAGE = "Elements that are focusable should not have `aria-hidden='true' because it will cause confusion for assistive technology users."

def run(processed_source)
tags(processed_source).each do |tag|
aria_hidden = possible_attribute_values(tag, "aria-hidden")
generate_offense(self.class, processed_source, tag) if aria_hidden.include?("true") && focusable?(tag)
end

counter_correct?(processed_source)
end

def autocorrect(processed_source, offense)
return unless offense.context

lambda do |corrector|
if processed_source.file_content.include?("erblint:counter #{simple_class_name}")
# update the counter if exists
corrector.replace(offense.source_range, offense.context)
else
# add comment with counter if none
corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
end
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

require "test_helper"

class NoAriaHiddenOnFocusableCounterTest < LinterTestCase
def linter_class
ERBLint::Linters::GitHub::Accessibility::NoAriaHiddenOnFocusableCounter
end

def test_does_not_warn_if_link_does_not_have_aria_hidden
@file = "<a href='github.com'>GitHub</a>"
@linter.run(processed_source)

assert_empty @linter.offenses
end

def test_does_not_consider_aria_hidden_as_aria_hidden_true
# aria-hidden is not the same as aria-hidden="true". Not ideal code.
@file = "<a aria-hidden href='github.com'>GitHub</a>"
@linter.run(processed_source)

assert_empty @linter.offenses
end

def test_does_not_warn_if_link_has_aria_hidden_false
@file = "<a aria-hidden='false' href='github.com'>GitHub</a>"
@linter.run(processed_source)

assert_empty @linter.offenses
end

def test_does_not_warn_when_link_has_aria_hidden_true_and_is_not_focusable
@file = "<a aria-hidden='true' tabindex='-1' href='github.com'>GitHub</a>"
@linter.run(processed_source)

assert_empty @linter.offenses
end

def test_warns_when_element_has_aria_hidden_true_and_not_tab_focusable
@file = "<div role='button' tabindex='0' aria-hidden='true'>GitHub</a>"
@linter.run(processed_source)
refute_empty @linter.offenses
end

def test_warns_when_link_has_aria_hidden_true
@file = "<a aria-hidden='true' href='github.com'>GitHub</a>"
@linter.run(processed_source)

refute_empty @linter.offenses
end

def test_warns_when_element_has_aria_hidden_true_and_is_tab_focusable
@file = "<div role='list' aria-hidden='true' tabindex='0'></div>"
@linter.run(processed_source)

refute_empty @linter.offenses
end
end