From 75da7cbaded179cc137a04bc6baeadea9a6e254e Mon Sep 17 00:00:00 2001 From: yassin-kammoun-sonarsource Date: Thu, 22 Feb 2024 12:18:56 +0100 Subject: [PATCH] SONARHTML-198 Create rule S6852: Elements with an interactive role should support focus --- .../resources/expected/Web-HeaderCheck.json | 3 + ...seEventWithoutKeyboardEquivalentCheck.json | 3 + .../test/resources/expected/Web-S6852.json | 5 + its/sources | 2 +- .../accessibility/AccessibilityUtils.java | 10 ++ .../FocusableInteractiveElementsCheck.java | 102 ++++++++++++++++++ .../plugins/html/rules/CheckClasses.java | 2 + .../org/sonar/l10n/web/rules/Web/S6852.html | 35 ++++++ .../org/sonar/l10n/web/rules/Web/S6852.json | 24 +++++ .../l10n/web/rules/Web/Sonar_way_profile.json | 1 + ...FocusableInteractiveElementsCheckTest.java | 42 ++++++++ .../FocusableInteractiveElementsCheck.html | 33 ++++++ 12 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 its/ruling/src/test/resources/expected/Web-S6852.json create mode 100644 sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/accessibility/FocusableInteractiveElementsCheck.java create mode 100644 sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/S6852.html create mode 100644 sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/S6852.json create mode 100644 sonar-html-plugin/src/test/java/org/sonar/plugins/html/checks/accessibility/FocusableInteractiveElementsCheckTest.java create mode 100644 sonar-html-plugin/src/test/resources/checks/FocusableInteractiveElementsCheck.html diff --git a/its/ruling/src/test/resources/expected/Web-HeaderCheck.json b/its/ruling/src/test/resources/expected/Web-HeaderCheck.json index d7f5225ec..45dd1df80 100644 --- a/its/ruling/src/test/resources/expected/Web-HeaderCheck.json +++ b/its/ruling/src/test/resources/expected/Web-HeaderCheck.json @@ -1568,6 +1568,9 @@ "project:custom/S6843.html": [ 0 ], +"project:custom/S6852.html": [ +0 +], "project:external_webkit-jb-mr1/Examples/NetscapeCocoaPlugin/test.html": [ 0 ], diff --git a/its/ruling/src/test/resources/expected/Web-MouseEventWithoutKeyboardEquivalentCheck.json b/its/ruling/src/test/resources/expected/Web-MouseEventWithoutKeyboardEquivalentCheck.json index 6fd4bb3d4..9ad288837 100644 --- a/its/ruling/src/test/resources/expected/Web-MouseEventWithoutKeyboardEquivalentCheck.json +++ b/its/ruling/src/test/resources/expected/Web-MouseEventWithoutKeyboardEquivalentCheck.json @@ -47,6 +47,9 @@ 2, 3 ], +"project:custom/S6852.html": [ +1 +], "project:external_webkit-jb-mr1/LayoutTests/dom/html/level2/html/HTMLInputElement01.html": [ 36 ], diff --git a/its/ruling/src/test/resources/expected/Web-S6852.json b/its/ruling/src/test/resources/expected/Web-S6852.json new file mode 100644 index 000000000..2b244c750 --- /dev/null +++ b/its/ruling/src/test/resources/expected/Web-S6852.json @@ -0,0 +1,5 @@ +{ +"project:custom/S6852.html": [ +1 +] +} diff --git a/its/sources b/its/sources index a35edb9b0..49de9f988 160000 --- a/its/sources +++ b/its/sources @@ -1 +1 @@ -Subproject commit a35edb9b0b167c5a62be94202865a9720a8a3fcb +Subproject commit 49de9f9885163a81e173eaecff48bce9f99546c3 diff --git a/sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/accessibility/AccessibilityUtils.java b/sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/accessibility/AccessibilityUtils.java index e861a62e9..d57c20cc8 100644 --- a/sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/accessibility/AccessibilityUtils.java +++ b/sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/accessibility/AccessibilityUtils.java @@ -30,4 +30,14 @@ public static boolean isHiddenFromScreenReader(TagNode element) { && "hidden".equalsIgnoreCase(element.getPropertyValue("type"))) || "true".equalsIgnoreCase(element.getPropertyValue("aria-hidden")); } + + public static boolean isDisabledElement(TagNode element) { + var disabledAttr = element.getAttribute("disabled"); + if (disabledAttr != null) { + return true; + } + + var ariaDisabledAttr = element.getAttribute("aria-disabled"); + return "true".equalsIgnoreCase(ariaDisabledAttr); + } } diff --git a/sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/accessibility/FocusableInteractiveElementsCheck.java b/sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/accessibility/FocusableInteractiveElementsCheck.java new file mode 100644 index 000000000..9977aba89 --- /dev/null +++ b/sonar-html-plugin/src/main/java/org/sonar/plugins/html/checks/accessibility/FocusableInteractiveElementsCheck.java @@ -0,0 +1,102 @@ +/* + * SonarSource HTML analyzer :: Sonar Plugin + * Copyright (c) 2010-2024 SonarSource SA and Matthijs Galesloot + * sonarqube@googlegroups.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sonar.plugins.html.checks.accessibility; + +import static org.sonar.plugins.html.api.HtmlConstants.INTERACTIVE_ELEMENTS; +import static org.sonar.plugins.html.api.HtmlConstants.INTERACTIVE_ROLES; +import static org.sonar.plugins.html.api.HtmlConstants.KNOWN_HTML_TAGS; +import static org.sonar.plugins.html.api.HtmlConstants.NON_INTERACTIVE_ELEMENTS; +import static org.sonar.plugins.html.api.HtmlConstants.NON_INTERACTIVE_ROLES; +import static org.sonar.plugins.html.api.HtmlConstants.PRESENTATION_ROLES; +import static org.sonar.plugins.html.checks.accessibility.AccessibilityUtils.isDisabledElement; +import static org.sonar.plugins.html.checks.accessibility.AccessibilityUtils.isHiddenFromScreenReader; + +import java.util.HashSet; +import java.util.Set; + +import org.sonar.check.Rule; +import org.sonar.plugins.html.checks.AbstractPageCheck; +import org.sonar.plugins.html.node.TagNode; + +@Rule(key = "S6852") +public class FocusableInteractiveElementsCheck extends AbstractPageCheck { + + private static final String MESSAGE_TEMPLATE = "Elements with the \"%s\" interactive role must be focusable."; + + private static final Set INTERACTIVE_PROPS = new HashSet<>(); + static { + INTERACTIVE_PROPS.addAll(EventHandlers.EVENT_HANDLERS_BY_TYPE.get("keyboard")); + INTERACTIVE_PROPS.addAll(EventHandlers.EVENT_HANDLERS_BY_TYPE.get("mouse")); + } + + @Override + public void startElement(TagNode element) { + var role = element.getAttribute("role"); + if (role == null) { + return; + } + + if (!isHTMLTag(element) + || !hasInteractiveProps(element) + || !hasInteractiveRole(element) + || isDisabledElement(element) + || isHiddenFromScreenReader(element) + || isInteractiveElement(element) + || isNoninteractiveElement(element) + || hasPresentationRole(element) + || hasNoninteractiveRole(element) + || element.hasProperty("tabindex") + ) { + return; + } + + var message = String.format(MESSAGE_TEMPLATE, role); + createViolation(element.getStartLinePosition(), message); + } + + private static boolean isHTMLTag(TagNode element) { + return KNOWN_HTML_TAGS.stream().anyMatch(tag -> tag.equalsIgnoreCase(element.getNodeName())); + } + + private static boolean hasInteractiveProps(TagNode element) { + return INTERACTIVE_PROPS.stream().anyMatch(prop -> { + var attr = element.getAttribute(prop); + return attr != null && !attr.isEmpty(); + }); + } + + private static boolean hasInteractiveRole(TagNode element) { + return INTERACTIVE_ROLES.stream().anyMatch(role -> role.equalsIgnoreCase(element.getAttribute("role"))); + } + + private static boolean isInteractiveElement(TagNode element) { + return INTERACTIVE_ELEMENTS.stream().anyMatch(tag -> tag.equalsIgnoreCase(element.getNodeName())); + } + + private static boolean isNoninteractiveElement(TagNode element) { + return NON_INTERACTIVE_ELEMENTS.stream().anyMatch(tag -> tag.equalsIgnoreCase(element.getNodeName())); + } + + private static boolean hasPresentationRole(TagNode element) { + return PRESENTATION_ROLES.stream().anyMatch(role -> role.equalsIgnoreCase(element.getAttribute("role"))); + } + + private static boolean hasNoninteractiveRole(TagNode element) { + return NON_INTERACTIVE_ROLES.stream().anyMatch(role -> role.equalsIgnoreCase(element.getAttribute("role"))); + } +} diff --git a/sonar-html-plugin/src/main/java/org/sonar/plugins/html/rules/CheckClasses.java b/sonar-html-plugin/src/main/java/org/sonar/plugins/html/rules/CheckClasses.java index 738130f20..112dc2e78 100644 --- a/sonar-html-plugin/src/main/java/org/sonar/plugins/html/rules/CheckClasses.java +++ b/sonar-html-plugin/src/main/java/org/sonar/plugins/html/rules/CheckClasses.java @@ -21,6 +21,7 @@ import org.sonar.plugins.html.checks.accessibility.AnchorsHaveContentCheck; import org.sonar.plugins.html.checks.accessibility.AriaProptypesCheck; +import org.sonar.plugins.html.checks.accessibility.FocusableInteractiveElementsCheck; import org.sonar.plugins.html.checks.accessibility.ValidAutocompleteCheck; import org.sonar.plugins.html.checks.accessibility.ImgRedundantAltCheck; import org.sonar.plugins.html.checks.accessibility.LabelHasAssociatedControlCheck; @@ -120,6 +121,7 @@ public final class CheckClasses { FileLengthCheck.class, FixmeCommentCheck.class, FlashUsesBothObjectAndEmbedCheck.class, + FocusableInteractiveElementsCheck.class, FrameWithoutTitleCheck.class, HeaderCheck.class, HeadingHasAccessibleContentCheck.class, diff --git a/sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/S6852.html b/sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/S6852.html new file mode 100644 index 000000000..687763faa --- /dev/null +++ b/sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/S6852.html @@ -0,0 +1,35 @@ +

Interactive elements being focusable is vital for website accessibility. It enables users, including those using assistive technologies, to +interact effectively with the website. Without this, some users may be unable to access certain features, leading to a poor user experience and +potential non-compliance with accessibility standards.

+

Why is this an issue?

+

Lack of focusability can hinder navigation and interaction with the website, resulting in an exclusionary user experience and possible violation of +accessibility guidelines.

+

How to fix it

+

Ensure that all interactive elements on your website can receive focus. This can be achieved by using standard HTML interactive elements, or by +assigning a tabindex attribute of "0" to custom interactive components.

+

Code examples

+

Noncompliant code example

+
+<!-- Element with mouse/keyboard handler has no tabindex -->
+<span onclick="submitForm();" role="button">Submit</span>
+
+<!-- Anchor element without href is not focusable -->
+<a onclick="showNextPage();" role="button">Next page</a>
+
+

Compliant solution

+
+<!-- Element with mouse handler has tabIndex -->
+<span onClick="doSomething();" tabIndex="0" role="button">Submit</span>
+
+<!-- Focusable anchor with mouse handler -->
+<a href="javascript:void(0);" onClick="doSomething();"> Next page </a>
+
+

Resources

+

Documentation

+ + diff --git a/sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/S6852.json b/sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/S6852.json new file mode 100644 index 000000000..94e53d927 --- /dev/null +++ b/sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/S6852.json @@ -0,0 +1,24 @@ +{ + "title": "Elements with an interactive role should support focus", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "accessibility" + ], + "defaultSeverity": "Minor", + "ruleSpecification": "RSPEC-6852", + "sqKey": "S6852", + "scope": "All", + "quickfix": "infeasible", + "code": { + "impacts": { + "MAINTAINABILITY": "LOW", + "RELIABILITY": "MEDIUM" + }, + "attribute": "CONVENTIONAL" + } +} diff --git a/sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/Sonar_way_profile.json b/sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/Sonar_way_profile.json index d26924107..f7d136ce7 100644 --- a/sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/Sonar_way_profile.json +++ b/sonar-html-plugin/src/main/resources/org/sonar/l10n/web/rules/Web/Sonar_way_profile.json @@ -37,6 +37,7 @@ "S6847", "S6850", "S6851", + "S6852", "S6853", "ServerSideImageMapsCheck", "TableHeaderHasIdOrScopeCheck", diff --git a/sonar-html-plugin/src/test/java/org/sonar/plugins/html/checks/accessibility/FocusableInteractiveElementsCheckTest.java b/sonar-html-plugin/src/test/java/org/sonar/plugins/html/checks/accessibility/FocusableInteractiveElementsCheckTest.java new file mode 100644 index 000000000..c24b548f1 --- /dev/null +++ b/sonar-html-plugin/src/test/java/org/sonar/plugins/html/checks/accessibility/FocusableInteractiveElementsCheckTest.java @@ -0,0 +1,42 @@ +/* + * SonarSource HTML analyzer :: Sonar Plugin + * Copyright (c) 2010-2024 SonarSource SA and Matthijs Galesloot + * sonarqube@googlegroups.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sonar.plugins.html.checks.accessibility; + +import java.io.File; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonar.plugins.html.checks.CheckMessagesVerifierRule; +import org.sonar.plugins.html.checks.TestHelper; +import org.sonar.plugins.html.visitor.HtmlSourceCode; + +class FocusableInteractiveElementsCheckTest { + + @RegisterExtension + public CheckMessagesVerifierRule checkMessagesVerifier = new CheckMessagesVerifierRule(); + + @Test + void test() throws Exception { + HtmlSourceCode sourceCode = TestHelper.scan( + new File("src/test/resources/checks/FocusableInteractiveElementsCheck.html"), + new FocusableInteractiveElementsCheck()); + + checkMessagesVerifier.verify(sourceCode.getIssues()) + .next().atLine(61).withMessage("Elements with the \"button\" interactive role must be focusable.") + .noMore(); + } +} diff --git a/sonar-html-plugin/src/test/resources/checks/FocusableInteractiveElementsCheck.html b/sonar-html-plugin/src/test/resources/checks/FocusableInteractiveElementsCheck.html new file mode 100644 index 000000000..c1912b4ff --- /dev/null +++ b/sonar-html-plugin/src/test/resources/checks/FocusableInteractiveElementsCheck.html @@ -0,0 +1,33 @@ + + + + + + + +
+ + +
+
+ + +