Explorar o código

LibWeb: Compute default ARIA roles context-sensitively where required

This change implements spec-conformant computation of default ARIA roles
for elements whose expected default role depends on the element’s
context — specifically, either on the element’s ancestry, or on whether
the element has an accessible name, or both. This affects the “aside”,
“footer”, “header”, and “section” elements.

Otherwise, without this change, “aside”, “footer”, “header”, and
“section” elements may unexpectedly end up with the wrong default roles.
sideshowbarker hai 8 meses
pai
achega
68894306e2

+ 11 - 1
Libraries/LibWeb/DOM/Node.cpp

@@ -2228,7 +2228,17 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
 
     if (is_element()) {
         auto const* element = static_cast<DOM::Element const*>(this);
-        auto role = element->role_or_default();
+        auto role = element->role_from_role_attribute_value();
+        // Per https://w3c.github.io/html-aam/#el-aside and https://w3c.github.io/html-aam/#el-section, computing a
+        // default role for an aside element or section element requires first computing its accessible name — that is,
+        // calling into this name_or_description code. But if we then try to determine a default role for the aside
+        // element or section element here, that’d then end up calling right back into this name_or_description code —
+        // which would cause the calls to loop infinitely. So to avoid that, we only compute a default role here if this
+        // isn’t an aside element or section element.
+        // https://github.com/w3c/aria/issues/2391
+        if (!role.has_value() && element->local_name() != HTML::TagNames::aside && element->local_name() != HTML::TagNames::section)
+            role = element->default_role();
+
         // 2. Compute the text alternative for the current node:
 
         // A. Hidden Not Referenced: If the current node is hidden and is:

+ 29 - 13
Libraries/LibWeb/HTML/HTMLElement.cpp

@@ -734,8 +734,16 @@ Optional<ARIA::Role> HTMLElement::default_role() const
     if (local_name() == TagNames::article)
         return ARIA::Role::article;
     // https://www.w3.org/TR/html-aria/#el-aside
-    if (local_name() == TagNames::aside)
+    if (local_name() == TagNames::aside) {
+        // https://w3c.github.io/html-aam/#el-aside
+        for (auto const* ancestor = parent_element(); ancestor; ancestor = ancestor->parent_element()) {
+            if (first_is_one_of(ancestor->local_name(), TagNames::article, TagNames::aside, TagNames::nav, TagNames::section)
+                && accessible_name(document()).value().is_empty())
+                return ARIA::Role::generic;
+        }
+        // https://w3c.github.io/html-aam/#el-aside-ancestorbodymain
         return ARIA::Role::complementary;
+    }
     // https://www.w3.org/TR/html-aria/#el-b
     if (local_name() == TagNames::b)
         return ARIA::Role::generic;
@@ -758,16 +766,22 @@ Optional<ARIA::Role> HTMLElement::default_role() const
     if (local_name() == TagNames::figure)
         return ARIA::Role::figure;
     // https://www.w3.org/TR/html-aria/#el-footer
-    if (local_name() == TagNames::footer) {
-        // TODO: If not a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region then role=contentinfo
-        // Otherwise, role=generic
-        return ARIA::Role::generic;
-    }
     // https://www.w3.org/TR/html-aria/#el-header
-    if (local_name() == TagNames::header) {
-        // TODO: If not a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region then role=banner
-        // Otherwise, role=generic
-        return ARIA::Role::generic;
+    if (local_name() == TagNames::footer || local_name() == TagNames::header) {
+        // If not a descendant of an article, aside, main, nav or section element, or an element with role=article,
+        // complementary, main, navigation or region then (footer) role=contentinfo (header) role=banner. Otherwise,
+        // role=generic.
+        for (auto const* ancestor = parent_element(); ancestor; ancestor = ancestor->parent_element()) {
+            if (first_is_one_of(ancestor->local_name(), TagNames::article, TagNames::aside, TagNames::main, TagNames::nav, TagNames::section))
+                return ARIA::Role::generic;
+            if (first_is_one_of(ancestor->role_or_default(), ARIA::Role::article, ARIA::Role::complementary, ARIA::Role::main, ARIA::Role::navigation, ARIA::Role::region))
+                return ARIA::Role::generic;
+        }
+        // then (footer) role=contentinfo.
+        if (local_name() == TagNames::footer)
+            return ARIA::Role::contentinfo;
+        // (header) role=banner
+        return ARIA::Role::banner;
     }
     // https://www.w3.org/TR/html-aria/#el-hgroup
     if (local_name() == TagNames::hgroup)
@@ -792,9 +806,11 @@ Optional<ARIA::Role> HTMLElement::default_role() const
         return ARIA::Role::search;
     // https://www.w3.org/TR/html-aria/#el-section
     if (local_name() == TagNames::section) {
-        // TODO:  role=region if the section element has an accessible name
-        //        Otherwise, no corresponding role
-        return ARIA::Role::region;
+        // role=region if the section element has an accessible name
+        if (!accessible_name(document()).value().is_empty())
+            return ARIA::Role::region;
+        // Otherwise, role=generic
+        return ARIA::Role::generic;
     }
     // https://www.w3.org/TR/html-aria/#el-small
     if (local_name() == TagNames::small)

+ 24 - 0
Tests/LibWeb/Text/expected/wpt-import/html-aam/roles-contextual.txt

@@ -0,0 +1,24 @@
+Harness status: OK
+
+Found 19 tests
+
+19 Pass
+Pass	el-a
+Pass	el-aside
+Pass	el-aside-in-main
+Pass	el-aside-in-article-in-main-with-name
+Pass	el-aside-in-article-with-name
+Pass	el-aside-in-aside-with-name
+Pass	el-aside-in-nav-with-name
+Pass	el-aside-in-nav-with-role
+Pass	el-aside-in-section-with-name
+Pass	el-footer-ancestorbody
+Pass	el-header-ancestorbody
+Pass	el-section
+Pass	el-a-no-href
+Pass	el-aside-in-article-in-main
+Pass	el-aside-in-article
+Pass	el-aside-in-aside
+Pass	el-aside-in-nav
+Pass	el-aside-in-section
+Pass	el-section-no-name

+ 76 - 0
Tests/LibWeb/Text/input/wpt-import/html-aam/roles-contextual.html

@@ -0,0 +1,76 @@
+<!doctype html>
+<html>
+<head>
+  <title>HTML-AAM Contextual-Specific Role Verification Tests</title>
+  <script src="../resources/testharness.js"></script>
+  <script src="../resources/testharnessreport.js"></script>
+  <script src="../resources/testdriver.js"></script>
+  <script src="../resources/testdriver-vendor.js"></script>
+  <script src="../resources/testdriver-actions.js"></script>
+  <script src="../wai-aria/scripts/aria-utils.js"></script>
+</head>
+<body>
+
+
+<p>Tests contextual computedrole mappings defined in <a href="https://w3c.github.io/html-aam/">HTML-AAM</a>, where the returned computed role is expected to change based on the context. Most test names correspond to a unique ID defined in the spec.<p>
+
+<p>These should remain in alphabetical order.</code></p>
+
+
+<!-- el-a -->
+<a href="#" data-testname="el-a" data-expectedrole="link" class="ex">x</a>
+<a data-testname="el-a-no-href" class="ex-generic">x</a>
+
+<!-- el-aside -->
+<aside data-testname="el-aside" data-expectedrole="complementary" class="ex">x</aside>
+<main>
+  <aside data-testname="el-aside-in-main" data-expectedrole="complementary" class="ex">x</aside>
+  <article>
+    <aside data-testname="el-aside-in-article-in-main" class="ex-generic">x</aside>
+    <aside data-testname="el-aside-in-article-in-main-with-name" data-expectedrole="complementary" aria-label="x" class="ex">x</aside>
+  </article>
+</main>
+<article>
+  <aside data-testname="el-aside-in-article" class="ex-generic">x</aside>
+  <aside data-testname="el-aside-in-article-with-name" data-expectedrole="complementary" aria-label="x" class="ex">x</aside>
+</article>
+<aside>
+  <aside data-testname="el-aside-in-aside" class="ex-generic">x</aside>
+  <aside data-testname="el-aside-in-aside-with-name" data-expectedrole="complementary" aria-label="x" class="ex">x</aside>
+</aside>
+<nav>
+  <aside data-testname="el-aside-in-nav" class="ex-generic">x</aside>
+  <aside data-testname="el-aside-in-nav-with-name" data-expectedrole="complementary" aria-label="x" class="ex">x</aside>
+  <aside data-testname="el-aside-in-nav-with-role" data-expectedrole="complementary" class="ex" role="complementary">x</aside>
+</nav>
+<!-- Spec says that the conditional aside mapping happens when nested in a sectioning content element.
+  However, this doesn't make sense if the parent <section> isn't a landmark in the first place.
+  Let's force the section to always be a landmark for now, but we should probably expand on this test
+  case pending discussions in https://github.com/w3c/html-aam/pull/484 -->
+<section aria-label="x">
+  <aside data-testname="el-aside-in-section" class="ex-generic">x</aside>
+  <aside data-testname="el-aside-in-section-with-name" data-expectedrole="complementary" aria-label="x" class="ex">x</aside>
+</section>
+
+<!-- el-footer -->
+<!-- nav>footer -> ./roles-contextual.tentative.html -->
+<footer data-testname="el-footer-ancestorbody" data-expectedrole="contentinfo" class="ex">x</footer>
+<!-- main>footer -> ./roles-contextual.tentative.html -->
+
+<!-- el-header -->
+<!-- nav>header -> ./roles-contextual.tentative.html -->
+<header data-testname="el-header-ancestorbody" data-expectedrole="banner" class="ex">x</header>
+<!-- main>header -> ./roles-contextual.tentative.html -->
+
+<!-- el-section -->
+<section data-testname="el-section" aria-label="x" data-expectedrole="region" class="ex">x</section>
+<section data-testname="el-section-no-name" class="ex-generic">x</section>
+
+
+<script>
+AriaUtils.verifyRolesBySelector(".ex");
+AriaUtils.verifyGenericRolesBySelector(".ex-generic");
+</script>
+
+</body>
+</html>