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.
This commit is contained in:
sideshowbarker 2024-12-04 21:55:39 +09:00 committed by Tim Ledbetter
commit 68894306e2
Notes: github-actions[bot] 2024-12-06 18:32:42 +00:00
4 changed files with 140 additions and 14 deletions

View file

@ -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, thatd 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
// isnt 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:

View file

@ -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)

View file

@ -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

View file

@ -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>