From a2795f4b73a797810b986e8cc061fcc9472fccf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Bolonio?= Date: Wed, 27 Jul 2022 10:47:24 +0000 Subject: [PATCH] Add new linter rule: NestedInteractiveElementsCounter --- README.md | 9 ++- .../nested-interactive-elements-counter.md | 27 +++++++++ .../nested_interactive_elements_counter.rb | 55 +++++++++++++++++ ...ested_interactive_elements_counter_test.rb | 59 +++++++++++++++++++ 4 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 docs/rules/accessibility/nested-interactive-elements-counter.md create mode 100644 lib/erblint-github/linters/github/accessibility/nested_interactive_elements_counter.rb create mode 100644 test/linters/accessibility/nested_interactive_elements_counter_test.rb diff --git a/README.md b/README.md index 65c9256..8689ac1 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ linters: enabled: true GitHub::Accessibility::LinkHasHrefCounter: enabled: true + GitHub::Accessibility::NestedInteractiveElementsCounter: + enabled: true GitHub::Accessibility::NoAriaLabelMisuseCounter: enabled: true GitHub::Accessibility::NoPositiveTabIndex: @@ -54,12 +56,13 @@ linters: - [GitHub::Accessibility::DisabledAttributeCounter](./docs/rules/accessibility/disabled-attribute-counter-test) - [GitHub::Accessibility::IframeHasTitle](./docs/rules/accessibility/iframe-has-title.md) - [GitHub::Accessibility::ImageHasAlt](./docs/rules/accessibility/image-has-alt.md) -- [GitHub::Accessibility::LinkHasHrefCounter](./docs/rules/accessibility/link_has_href-counter.md) +- [GitHub::Accessibility::LinkHasHrefCounter](./docs/rules/accessibility/link-has-href-counter.md) +- [GitHub::Accessibility::NestedInteractiveElementsCounter](./docs/rules/accessibility/nested-interactive-elements-counter.md) - [GitHub::Accessibility::NoAriaLabelMisuseCounter](./docs/rules/accessibility/no-aria-label-misuse-counter.md) - [GitHub::Accessibility::NoPositiveTabIndex](./docs/rules/accessibility/no-positive-tab-index.md) - [GitHub::Accessibility::NoRedundantImageAlt](./docs/rules/accessibility/no-redundant-image-alt.md) - [GitHub::Accessibility::NoTitleAttributeCounter](./docs/rules/accessibility/no-title-attribute-counter.md) -- [GitHub::Accessibility::SvgHasAccessibleTextCounter](./docs/rules/accessibility/svg_has_accessible_text_counter.md) +- [GitHub::Accessibility::SvgHasAccessibleTextCounter](./docs/rules/accessibility/svg-has-accessible-text-counter.md) ## Testing @@ -75,4 +78,4 @@ If you use VS Code, we highly encourage [ERB Linter extension](https://marketpla ## Note This repo contains several accessibility-related linting rules to help surface accessibility issues that would otherwise go undetected until a later stage. Please note that due to the limitations of static code analysis, -these ERB accessibility checks are NOT enough for ensuring the accessibility of your app. This shouldn't be the only tool you use to catch accessibility issues and should be supplemented with other tools that can check the runtime browser DOM output, as well as processes like accessibility design reviews, manual audits, user testing, etc. +these ERB accessibility checks are NOT enough for ensuring the accessibility of your app. This shouldn't be the only tool you use to catch accessibility issues and should be supplemented with other tools that can check the runtime browser DOM output, as well as processes like accessibility design reviews, manual audits, user testing, etc. \ No newline at end of file diff --git a/docs/rules/accessibility/nested-interactive-elements-counter.md b/docs/rules/accessibility/nested-interactive-elements-counter.md new file mode 100644 index 0000000..20481f6 --- /dev/null +++ b/docs/rules/accessibility/nested-interactive-elements-counter.md @@ -0,0 +1,27 @@ +# Nested Interactive Elements Counter + +## Rule Details + +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. + +## Resources + +- [Deque University](https://dequeuniversity.com/rules/axe/4.2/nested-interactive) +- [Accessibility Insights](https://accessibilityinsights.io/info-examples/web/nested-interactive/) + +## Examples +### **Incorrect** code for this rule 👎 + +```erb + + +``` + +### **Correct** code for this rule 👍 + +```erb + + +``` diff --git a/lib/erblint-github/linters/github/accessibility/nested_interactive_elements_counter.rb b/lib/erblint-github/linters/github/accessibility/nested_interactive_elements_counter.rb new file mode 100644 index 0000000..c800a89 --- /dev/null +++ b/lib/erblint-github/linters/github/accessibility/nested_interactive_elements_counter.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require_relative "../../custom_helpers" + +module ERBLint + module Linters + module GitHub + module Accessibility + 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) + last_interactive_element = nil + tags(processed_source).each do |tag| + next unless INTERACTIVE_ELEMENTS.include?(tag.name) + + last_interactive_element = nil if last_interactive_element && tag.name == last_interactive_element.name && tag.closing? + next if tag.closing? + + if last_interactive_element + next if last_interactive_element.name == "summary" && tag.name == "a" + next if tag.name == "input" && tag.attributes["type"]&.value == "hidden" + + message = "Found <#{tag.name}> nested inside of <#{last_interactive_element.name}>.\n" + MESSAGE + generate_offense(self.class, processed_source, tag, message) + end + + last_interactive_element = tag unless tag&.name == "input" + 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 diff --git a/test/linters/accessibility/nested_interactive_elements_counter_test.rb b/test/linters/accessibility/nested_interactive_elements_counter_test.rb new file mode 100644 index 0000000..181acef --- /dev/null +++ b/test/linters/accessibility/nested_interactive_elements_counter_test.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require "test_helper" + +class NestedInteractiveElementsCounter < LinterTestCase + def linter_class + ERBLint::Linters::GitHub::Accessibility::NestedInteractiveElementsCounter + end + + def test_warns_if_there_are_nested_interactive_elements + @file = "" + @linter.run(processed_source) + + assert_equal(2, @linter.offenses.count) + error_messages = @linter.offenses.map(&:message).sort + assert_match(/If you must, add <%# erblint:counter GitHub::Accessibility::NestedInteractiveElementsCounter 1 %> to bypass this check./, error_messages.first) + 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) + end + + def test_does_not_warn_if_there_are_not_nested_interactive_elements + @file = "" + @linter.run(processed_source) + + assert_empty @linter.offenses + end + + def test_does_not_warn_if_there_are_not_nested_interactive_elements_and_has_correct_counter_comment + @file = <<~ERB + <%# erblint:counter GitHub::Accessibility::NestedInteractiveElementsCounter 1 %> + + ERB + @linter.run(processed_source) + + assert_equal 0, @linter.offenses.count + end + + def test_does_not_autocorrect_when_ignores_are_correct + @file = <<~ERB + <%# erblint:counter GitHub::Accessibility::NestedInteractiveElementsCounter 1 %> + + ERB + + assert_equal @file, corrected_content + end + + def test_does_autocorrect_when_ignores_are_not_correct + @file = <<~ERB + <%# erblint:counter GitHub::Accessibility::NestedInteractiveElementsCounter 3 %> + + ERB + refute_equal @file, corrected_content + + expected_content = <<~ERB + <%# erblint:counter GitHub::Accessibility::NestedInteractiveElementsCounter 1 %> + + ERB + assert_equal expected_content, corrected_content + end +end