From ed7a718071cca07eb2ed3fb8c21358f39d30c15f Mon Sep 17 00:00:00 2001 From: Kate Higa <16447748+khiga8@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:55:56 -0400 Subject: [PATCH 1/9] Add new rule to flag downcased aria-label --- README.md | 3 + .../aria_label_is_well_formatted.rb | 37 +++++++++++ .../aria_label_is_well_formatted_test.rb | 62 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 lib/erblint-github/linters/github/accessibility/aria_label_is_well_formatted.rb create mode 100644 test/linters/accessibility/aria_label_is_well_formatted_test.rb diff --git a/README.md b/README.md index af7e20a..0e1bdb2 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ require "erblint-github/linters" ```yaml --- linters: + GitHub::Accessibility::AriaLabelIsWellFormatted: + enabled: true GitHub::Accessibility::AvoidBothDisabledAndAriaDisabled: enabled: true GitHub::Accessibility::AvoidGenericLinkText: @@ -55,6 +57,7 @@ linters: ## Rules +- [GitHub::Accessibility::AriaLabelIsWellFormatted](./docs/rules/accessibility/aria-label-is-well-formatted.md) - [GitHub::Accessibility::AvoidBothDisabledAndAriaDisabled](./docs/rules/accessibility/avoid-both-disabled-and-aria-disabled.md) - [GitHub::Accessibility::AvoidGenericLinkText](./docs/rules/accessibility/avoid-generic-link-text.md) - [GitHub::Accessibility::DisabledAttribute](./docs/rules/accessibility/disabled-attribute.md) diff --git a/lib/erblint-github/linters/github/accessibility/aria_label_is_well_formatted.rb b/lib/erblint-github/linters/github/accessibility/aria_label_is_well_formatted.rb new file mode 100644 index 0000000..4f9073f --- /dev/null +++ b/lib/erblint-github/linters/github/accessibility/aria_label_is_well_formatted.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require_relative "../../custom_helpers" + +module ERBLint + module Linters + module GitHub + module Accessibility + class AriaLabelIsWellFormatted < Linter + include ERBLint::Linters::CustomHelpers + include LinterRegistry + + MESSAGE = "[aria-label] text should be formatted the same as you would visual text. Use sentence case." + + class ConfigSchema < LinterConfig + property :exceptions, accepts: array_of?(String), + default: -> { [] } + end + self.config_schema = ConfigSchema + + def run(processed_source) + tags(processed_source).each do |tag| + next if tag.closing? + + aria_label = possible_attribute_values(tag, "aria-label").join + next if aria_label.empty? + + if aria_label.match?(/^[a-z]/) && !@config.exceptions.include?(aria_label) + generate_offense(self.class, processed_source, tag) + end + end + end + end + end + end + end +end diff --git a/test/linters/accessibility/aria_label_is_well_formatted_test.rb b/test/linters/accessibility/aria_label_is_well_formatted_test.rb new file mode 100644 index 0000000..7fc2043 --- /dev/null +++ b/test/linters/accessibility/aria_label_is_well_formatted_test.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "test_helper" + +class AriaLabelIsWellFormatted < LinterTestCase + def linter_class + ERBLint::Linters::GitHub::Accessibility::AriaLabelIsWellFormatted + end + + def test_warns_when_aria_label_starts_with_downcase + @file = <<~HTML + + + + + HTML + @linter.run(processed_source) + + assert_equal 4, @linter.offenses.count + end + + def test_does_not_warn_when_aria_labelledby_starts_with_downcase + @file = " + + + HTML + @linter.run(processed_source) + + assert_empty @linter.offenses + end + + def test_does_not_warn_when_aria_label_is_excepted_in_config + @file = <<~HTML + + + + HTML + @linter.run(processed_source) + + assert_empty @linter.offenses + end + + + def test_does_not_warn_if_aria_label_is_in_excepted_list + @file = <<~HTML + + + HTML + @linter.config.exceptions = ["hello"] + @linter.run(processed_source) + + assert_empty @linter.offenses + end +end From 558ee6f8f7ba0a41e16933aa5aa05c5f7093c34c Mon Sep 17 00:00:00 2001 From: Kate Higa <16447748+khiga8@users.noreply.github.com> Date: Wed, 5 Apr 2023 21:02:08 -0400 Subject: [PATCH 2/9] Allow exceptions and document it --- .../aria-label-is-well-formatted.md | 46 +++++++++++++++++++ .../aria_label_is_well_formatted_test.rb | 4 +- 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 docs/rules/accessibility/aria-label-is-well-formatted.md diff --git a/docs/rules/accessibility/aria-label-is-well-formatted.md b/docs/rules/accessibility/aria-label-is-well-formatted.md new file mode 100644 index 0000000..fda090b --- /dev/null +++ b/docs/rules/accessibility/aria-label-is-well-formatted.md @@ -0,0 +1,46 @@ +# aria-label is well formatted + +## Rule Details + +`[aria-label]` content should be formatted in the same way you would visual text. Please use sentence case. + +Do not connect the words like you would an ID. An `aria-label` is different from `aria-labelledby`. +An `aria-label` is not an ID, and should be formatted as human-friendly text. + +## Config + +If you determine that there are valid scenarios for `aria-label` to start with downcase, you may except it in your `.erb-lint.yml` config like so: + +```yml + GitHub::Accessibility::AriaLabelIsWellFormatted: + enabled: true + exceptions: + - allowed for some reason + - also allowed for some reason +``` + +## Examples + +### **Incorrect** code for this rule 👎 + +```erb + - + HTML @linter.config.exceptions = ["hello"] @linter.run(processed_source) - assert_empty @linter.offenses + assert_equal 1, @linter.offenses.count end end From 6e5b2d2b23898c30c94a1b3a27432a24a2e3981a Mon Sep 17 00:00:00 2001 From: Kate Higa <16447748+khiga8@users.noreply.github.com> Date: Wed, 5 Apr 2023 21:10:56 -0400 Subject: [PATCH 3/9] Bump dependencies --- Gemfile.lock | 22 +++++++++++----------- erblint-github.gemspec | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b130c77..9612077 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,14 +43,14 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.5.9) mini_portile2 (2.8.1) - minitest (5.17.0) + minitest (5.18.0) mocha (2.0.2) ruby2_keywords (>= 0.0.5) nokogiri (1.14.0) mini_portile2 (~> 2.8.0) racc (~> 1.4) parallel (1.22.1) - parser (3.2.0.0) + parser (3.2.2.0) ast (~> 2.4.1) racc (1.6.2) rack (3.0.4.1) @@ -61,32 +61,32 @@ GEM loofah (~> 2.19, >= 2.19.1) rainbow (3.1.1) rake (13.0.6) - regexp_parser (2.6.2) + regexp_parser (2.7.0) rexml (3.2.5) - rubocop (1.44.1) + rubocop (1.49.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.24.1, < 2.0) + rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.24.1) - parser (>= 3.1.1.0) + rubocop-ast (1.28.0) + parser (>= 3.2.1.0) rubocop-github (0.20.0) rubocop (>= 1.37) rubocop-performance (>= 1.15) rubocop-rails (>= 2.17) - rubocop-performance (1.15.2) + rubocop-performance (1.16.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) rubocop-rails (2.17.4) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - ruby-progressbar (1.11.0) + ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) smart_properties (1.17.0) tzinfo (2.0.5) @@ -99,10 +99,10 @@ PLATFORMS DEPENDENCIES erb_lint (~> 0.3.0) erblint-github! - minitest (~> 5.17.0) + minitest (~> 5.18.0) mocha (~> 2.0.2) rake (~> 13.0.6) - rubocop (= 1.44.1) + rubocop (= 1.49.0) rubocop-github (~> 0.20.0) BUNDLED WITH diff --git a/erblint-github.gemspec b/erblint-github.gemspec index c0db1dd..9b02da7 100644 --- a/erblint-github.gemspec +++ b/erblint-github.gemspec @@ -17,11 +17,11 @@ Gem::Specification.new do |s| s.authors = ["GitHub Open Source"] s.add_development_dependency "erb_lint", "~> 0.3.0" - s.add_development_dependency "minitest", "~> 5.17.0" + s.add_development_dependency "minitest", "~> 5.18.0" s.add_development_dependency "mocha", "~> 2.0.2" s.add_development_dependency "rake", "~> 13.0.6" - s.add_development_dependency "rubocop", "= 1.44.1" + s.add_development_dependency "rubocop", "= 1.49.0" s.add_development_dependency "rubocop-github", "~> 0.20.0" s.metadata["rubygems_mfa_required"] = "true" end From 9907a59d61aa2fb1a50662237f18a562d19c9e58 Mon Sep 17 00:00:00 2001 From: Kate Higa <16447748+khiga8@users.noreply.github.com> Date: Thu, 6 Apr 2023 17:04:00 -0400 Subject: [PATCH 4/9] Update docs/rules/accessibility/aria-label-is-well-formatted.md Co-authored-by: Joyce Zhu --- docs/rules/accessibility/aria-label-is-well-formatted.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/accessibility/aria-label-is-well-formatted.md b/docs/rules/accessibility/aria-label-is-well-formatted.md index fda090b..96aacae 100644 --- a/docs/rules/accessibility/aria-label-is-well-formatted.md +++ b/docs/rules/accessibility/aria-label-is-well-formatted.md @@ -4,7 +4,7 @@ `[aria-label]` content should be formatted in the same way you would visual text. Please use sentence case. -Do not connect the words like you would an ID. An `aria-label` is different from `aria-labelledby`. +Do not kebab case the words like you would an HTML ID. An `aria-label` is different from `aria-labelledby`. An `aria-label` is not an ID, and should be formatted as human-friendly text. ## Config From 21767fe3f47b7405abc3a2d036e323bc9c231d6d Mon Sep 17 00:00:00 2001 From: Kate Higa <16447748+khiga8@users.noreply.github.com> Date: Thu, 6 Apr 2023 17:04:05 -0400 Subject: [PATCH 5/9] Update docs/rules/accessibility/aria-label-is-well-formatted.md Co-authored-by: Joyce Zhu --- docs/rules/accessibility/aria-label-is-well-formatted.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/accessibility/aria-label-is-well-formatted.md b/docs/rules/accessibility/aria-label-is-well-formatted.md index 96aacae..e8fc344 100644 --- a/docs/rules/accessibility/aria-label-is-well-formatted.md +++ b/docs/rules/accessibility/aria-label-is-well-formatted.md @@ -9,7 +9,7 @@ An `aria-label` is not an ID, and should be formatted as human-friendly text. ## Config -If you determine that there are valid scenarios for `aria-label` to start with downcase, you may except it in your `.erb-lint.yml` config like so: +If you determine that there are valid scenarios for `aria-label` to start with lowercase, you may exempt it in your `.erb-lint.yml` config like so: ```yml GitHub::Accessibility::AriaLabelIsWellFormatted: From 5295b9332bef1776520b55b4abd081daabbac946 Mon Sep 17 00:00:00 2001 From: Kate Higa <16447748+khiga8@users.noreply.github.com> Date: Thu, 6 Apr 2023 17:25:09 -0400 Subject: [PATCH 6/9] Fix lint rule logic --- .../accessibility/landmark_has_label.rb | 44 ++++--------------- .../accessibility/landmark_has_label_test.rb | 38 +++++++++++----- 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/lib/erblint-github/linters/github/accessibility/landmark_has_label.rb b/lib/erblint-github/linters/github/accessibility/landmark_has_label.rb index d97fafd..d02749b 100644 --- a/lib/erblint-github/linters/github/accessibility/landmark_has_label.rb +++ b/lib/erblint-github/linters/github/accessibility/landmark_has_label.rb @@ -10,31 +10,7 @@ class LandmarkHasLabel < Linter include ERBLint::Linters::CustomHelpers include LinterRegistry - LANDMARK_ROLES = %w[complementary navigation region search].freeze - LANDMARK_TAGS = %w[aside nav section].freeze - MESSAGE = "Landmark elements should have an aria-label attribute, or aria-labelledby if a heading elements exists in the landmark." - ROLE_TAG_MAPPING = { "complementary" => "aside", "navigation" => "nav", "region" => "section" }.freeze - - def get_additional_message(tag, roles) - role_matched = (roles & ROLE_TAG_MAPPING.keys).first - if role_matched - tag_matched = ROLE_TAG_MAPPING[role_matched] - - if tag.name == tag_matched - "The <#{tag_matched}> element will automatically communicate a role of '#{role_matched}'. You can safely drop the role attribute." - else - replace_message = if tag.name == "div" - "If possible replace this tag with a <#{tag_matched}>." - else - "Wrapping this element in a <#{tag_matched}> and setting a label on it is reccomended." - end - - "The <#{tag_matched}> element will automatically communicate a role of '#{role_matched}'. #{replace_message}" - end - elsif roles.include?("search") && tag.name != "form" - "The 'search' role works best when applied to a
element. If possible replace this tag with a ." - end - end + MESSAGE = "The navigation landmark should have a unique accessible name via `aria-label` or `aria-labelledby`." class ConfigSchema < LinterConfig property :counter_enabled, accepts: [true, false], default: false, reader: :counter_enabled? @@ -44,17 +20,15 @@ class ConfigSchema < LinterConfig def run(processed_source) tags(processed_source).each do |tag| next if tag.closing? - - possible_roles = possible_attribute_values(tag, "role") - next unless LANDMARK_TAGS.include?(tag.name) && (possible_roles & LANDMARK_ROLES).empty? - next if tag.attributes["aria-label"]&.value&.present? || tag.attributes["aria-labelledby"]&.value&.present? - - message = get_additional_message(tag, possible_roles) - if message - generate_offense(self.class, processed_source, tag, "#{MESSAGE}\n#{message}") - else - generate_offense(self.class, processed_source, tag) + next unless possible_attribute_values(tag, "role").include?("navigation") || tag.name == "nav" + if possible_attribute_values(tag, "aria-label").empty? && possible_attribute_values(tag, "aria-labelledby").empty? + message = MESSAGE + if tag.name != "nav" + message += "Additionally, you can safely drop the `role='navigation'` and replace it with the native HTML `nav` element." + end + generate_offense(self.class, processed_source, tag, message) end + end if @config.counter_enabled? diff --git a/test/linters/accessibility/landmark_has_label_test.rb b/test/linters/accessibility/landmark_has_label_test.rb index 7fa64be..379703d 100644 --- a/test/linters/accessibility/landmark_has_label_test.rb +++ b/test/linters/accessibility/landmark_has_label_test.rb @@ -7,24 +7,42 @@ def linter_class ERBLint::Linters::GitHub::Accessibility::LandmarkHasLabel end - def test_warns_if_landmark_has_no_label + def test_warns_if_navigation_landmark_has_no_label @file = <<~ERB -
-

This is a text

-
+ ERB @linter.run(processed_source) assert_equal(1, @linter.offenses.count) - error_messages = @linter.offenses.map(&:message).sort - assert_match(/Landmark elements should have an aria-label attribute, or aria-labelledby if a heading elements exists in the landmark./, error_messages.last) + assert_match(/The navigation landmark should have a unique accessible name via `aria-label` or `aria-labelledby`./, @linter.offenses.first.message) end - def test_does_not_warn_if_landmark_has_label + def test_warns_if_navigation_role_landmark_has_no_label @file = <<~ERB -
-

This is a text

-
+
+
+ ERB + @linter.run(processed_source) + + assert_equal(1, @linter.offenses.count) + assert_match(/The navigation landmark should have a unique accessible name via `aria-label` or `aria-labelledby`./, @linter.offenses.first.message) + end + + def test_does_not_warn_if_navigation_landmark_has_aria_labelled_by + @file = <<~ERB + + ERB + @linter.run(processed_source) + + assert_empty @linter.offenses + end + + def test_does_not_warn_if_navigation_landmark_has_aria_label + @file = <<~ERB + ERB @linter.run(processed_source) From a95a56f45047af6430c511b96ddb95871fa696a7 Mon Sep 17 00:00:00 2001 From: Kate Higa <16447748+khiga8@users.noreply.github.com> Date: Thu, 6 Apr 2023 19:24:17 -0400 Subject: [PATCH 7/9] Rename rule and deprecate old one --- README.md | 4 +-- .../rules/accessibility/landmark-has-label.md | 28 ----------------- .../accessibility/navigation-has-label.md | 30 +++++++++++++++++++ ...k_has_label.rb => navigation_has_label.rb} | 11 +------ ...l_test.rb => navigation_has_label_test.rb} | 4 +-- 5 files changed, 35 insertions(+), 42 deletions(-) delete mode 100644 docs/rules/accessibility/landmark-has-label.md create mode 100644 docs/rules/accessibility/navigation-has-label.md rename lib/erblint-github/linters/github/accessibility/{landmark_has_label.rb => navigation_has_label.rb} (75%) rename test/linters/accessibility/{landmark_has_label_test.rb => navigation_has_label_test.rb} (91%) diff --git a/README.md b/README.md index af7e20a..82f680d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ linters: enabled: true GitHub::Accessibility::ImageHasAlt: enabled: true - GitHub::Accessibility::LandmarkHasLabel: + GitHub::Accessibility::NavigationHasLabel: enabled: true GitHub::Accessibility::LinkHasHref: enabled: true @@ -58,7 +58,7 @@ linters: - [GitHub::Accessibility::AvoidBothDisabledAndAriaDisabled](./docs/rules/accessibility/avoid-both-disabled-and-aria-disabled.md) - [GitHub::Accessibility::AvoidGenericLinkText](./docs/rules/accessibility/avoid-generic-link-text.md) - [GitHub::Accessibility::DisabledAttribute](./docs/rules/accessibility/disabled-attribute.md) -- [GitHub::Accessibility::LandmarkHasLabel](./docs/rules/accessibility/landmark-has-label.md) +- [GitHub::Accessibility::NavigationHasLabel](./docs/rules/accessibility/navigation-has-label.md) - [GitHub::Accessibility::LinkHasHref](./docs/rules/accessibility/link-has-href.md) - [GitHub::Accessibility::NestedInteractiveElements](./docs/rules/accessibility/nested-interactive-elements.md) - [GitHub::Accessibility::IframeHasTitle](./docs/rules/accessibility/iframe-has-title.md) diff --git a/docs/rules/accessibility/landmark-has-label.md b/docs/rules/accessibility/landmark-has-label.md deleted file mode 100644 index 6b6856f..0000000 --- a/docs/rules/accessibility/landmark-has-label.md +++ /dev/null @@ -1,28 +0,0 @@ -# Landmark Has Label - -## Rule Details - -Landmark elements should have an `aria-label` attribute, or `aria-labelledby` if a heading elements exists in the landmark. - -## Resources - -- [ARIA Landmarks Example](https://www.w3.org/WAI/ARIA/apg/example-index/landmarks/index.html) - -## Examples -### **Incorrect** code for this rule 👎 - -```erb - -
-

This is a text

-
-``` - -### **Correct** code for this rule 👍 - -```erb - -
-

This is a text

-
-``` diff --git a/docs/rules/accessibility/navigation-has-label.md b/docs/rules/accessibility/navigation-has-label.md new file mode 100644 index 0000000..a664b10 --- /dev/null +++ b/docs/rules/accessibility/navigation-has-label.md @@ -0,0 +1,30 @@ +# Navigation Has Label + +## Rule Details + +This rule enforces that a navigation landmark (a `