Skip to content

Commit 316d588

Browse files
author
Adrián Bolonio
committed
Add new linter rule: NestedInteractiveElementsCounter
1 parent 3e6125e commit 316d588

File tree

4 files changed

+146
-2
lines changed

4 files changed

+146
-2
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ linters:
3535
enabled: true
3636
GitHub::Accessibility::LinkHasHrefCounter:
3737
enabled: true
38+
GitHub::Accessibility::NestedInteractiveElementsCounter:
39+
enabled: true
3840
GitHub::Accessibility::NoAriaLabelMisuseCounter:
3941
enabled: true
4042
GitHub::Accessibility::NoPositiveTabIndex:
@@ -54,12 +56,13 @@ linters:
5456
- [GitHub::Accessibility::DisabledAttributeCounter](./docs/rules/accessibility/disabled-attribute-counter-test)
5557
- [GitHub::Accessibility::IframeHasTitle](./docs/rules/accessibility/iframe-has-title.md)
5658
- [GitHub::Accessibility::ImageHasAlt](./docs/rules/accessibility/image-has-alt.md)
57-
- [GitHub::Accessibility::LinkHasHrefCounter](./docs/rules/accessibility/link_has_href-counter.md)
59+
- [GitHub::Accessibility::LinkHasHrefCounter](./docs/rules/accessibility/link-has-href-counter.md)
60+
- [GitHub::Accessibility::NestedInteractiveElementsCounter](./docs/rules/accessibility/nested-interactive-elements-counter.md)
5861
- [GitHub::Accessibility::NoAriaLabelMisuseCounter](./docs/rules/accessibility/no-aria-label-misuse-counter.md)
5962
- [GitHub::Accessibility::NoPositiveTabIndex](./docs/rules/accessibility/no-positive-tab-index.md)
6063
- [GitHub::Accessibility::NoRedundantImageAlt](./docs/rules/accessibility/no-redundant-image-alt.md)
6164
- [GitHub::Accessibility::NoTitleAttributeCounter](./docs/rules/accessibility/no-title-attribute-counter.md)
62-
- [GitHub::Accessibility::SvgHasAccessibleTextCounter](./docs/rules/accessibility/svg_has_accessible_text_counter.md)
65+
- [GitHub::Accessibility::SvgHasAccessibleTextCounter](./docs/rules/accessibility/svg-has-accessible-text-counter.md)
6366
6467
## Testing
6568
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Nested Interactive Elements Counter
2+
3+
## Rule Details
4+
5+
Certain interactive controls such as `button`, `summary`, `input`, `select`, `textarea`, or `a` can't have interactive children. Nesting interactive elements produces invalid HTML, and ssistive technologies, such as screen readers, might ignore or respond unexpectedly to such nested controls.
6+
7+
## Resources
8+
9+
- [Deque University](https://dequeuniversity.com/rules/axe/4.2/nested-interactive)
10+
- [Accessibility Insights](https://accessibilityinsights.io/info-examples/web/nested-interactive/)
11+
12+
## Examples
13+
### **Incorrect** code for this rule 👎
14+
15+
```erb
16+
<!-- incorrect -->
17+
<button>
18+
<a href='https://github.com/'>Go to GitHub</a>
19+
</button>
20+
```
21+
22+
### **Correct** code for this rule 👍
23+
24+
```erb
25+
<!-- correct -->
26+
<button>Confirm</button>
27+
```
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 NestedInteractiveElementsCounter < Linter
10+
include ERBLint::Linters::CustomHelpers
11+
include LinterRegistry
12+
13+
INTERACTIVE_ELEMENTS = %w[button summary input select textarea a].freeze
14+
MESSAGE = "Nesting interactive elements produces invalid HTML, and ssistive technologies, such as screen readers, might ignore or respond unexpectedly to such nested controls."
15+
16+
def run(processed_source)
17+
last_interactive_element = nil
18+
tags(processed_source).each do |tag|
19+
next unless INTERACTIVE_ELEMENTS.include?(tag.name)
20+
21+
last_interactive_element = nil if last_interactive_element && tag.name == last_interactive_element.name && tag.closing?
22+
next if tag.closing?
23+
24+
if last_interactive_element
25+
next if last_interactive_element.name == "summary" && tag.name == "a"
26+
next if tag.name == "input" && tag.attributes["type"]&.value == "hidden"
27+
28+
message = "Found <#{tag.name}> nested inside of <#{last_interactive_element.name}>.\n" + MESSAGE
29+
generate_offense(self.class, processed_source, tag, message)
30+
end
31+
32+
last_interactive_element = tag unless tag&.name == "input"
33+
end
34+
35+
counter_correct?(processed_source)
36+
end
37+
38+
def autocorrect(processed_source, offense)
39+
return unless offense.context
40+
41+
lambda do |corrector|
42+
if processed_source.file_content.include?("erblint:counter #{simple_class_name}")
43+
# update the counter if exists
44+
corrector.replace(offense.source_range, offense.context)
45+
else
46+
# add comment with counter if none
47+
corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
48+
end
49+
end
50+
end
51+
end
52+
end
53+
end
54+
end
55+
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class NestedInteractiveElementsCounter < LinterTestCase
6+
def linter_class
7+
ERBLint::Linters::GitHub::Accessibility::NestedInteractiveElementsCounter
8+
end
9+
10+
def test_warns_if_there_are_nested_interactive_elements
11+
@file = "<button><a href='https://github.com/'>Go to GitHub</a></button>"
12+
@linter.run(processed_source)
13+
14+
assert_equal(2, @linter.offenses.count)
15+
error_messages = @linter.offenses.map(&:message).sort
16+
assert_match(/If you must, add <%# erblint:counter GitHub::Accessibility::NestedInteractiveElementsCounter 1 %> to bypass this check./, error_messages.first)
17+
assert_match(/Nesting interactive elements produces invalid HTML, and ssistive technologies, such as screen readers, might ignore or respond unexpectedly to such nested controls./, error_messages.last)
18+
end
19+
20+
def test_does_not_warn_if_there_are_not_nested_interactive_elements
21+
@file = "<button>Confirm</button>"
22+
@linter.run(processed_source)
23+
24+
assert_empty @linter.offenses
25+
end
26+
27+
def test_does_not_warn_if_there_are_not_nested_interactive_elements_and_has_correct_counter_comment
28+
@file = <<~ERB
29+
<%# erblint:counter GitHub::Accessibility::NestedInteractiveElementsCounter 1 %>
30+
<button><a href='https://github.com/'>Go to GitHub</a></button>
31+
ERB
32+
@linter.run(processed_source)
33+
34+
assert_equal 0, @linter.offenses.count
35+
end
36+
37+
def test_does_not_autocorrect_when_ignores_are_correct
38+
@file = <<~ERB
39+
<%# erblint:counter GitHub::Accessibility::NestedInteractiveElementsCounter 1 %>
40+
<button><a href='https://github.com/'>Go to GitHub</a></button>
41+
ERB
42+
43+
assert_equal @file, corrected_content
44+
end
45+
46+
def test_does_autocorrect_when_ignores_are_not_correct
47+
@file = <<~ERB
48+
<%# erblint:counter GitHub::Accessibility::NestedInteractiveElementsCounter 3 %>
49+
<button><a href='https://github.com/'>Go to GitHub</a></button>
50+
ERB
51+
refute_equal @file, corrected_content
52+
53+
expected_content = <<~ERB
54+
<%# erblint:counter GitHub::Accessibility::NestedInteractiveElementsCounter 1 %>
55+
<button><a href='https://github.com/'>Go to GitHub</a></button>
56+
ERB
57+
assert_equal expected_content, corrected_content
58+
end
59+
end

0 commit comments

Comments
 (0)