mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-01 13:49:16 +00:00
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:
parent
7a4a9cc7bc
commit
68894306e2
Notes:
github-actions[bot]
2024-12-06 18:32:42 +00:00
Author: https://github.com/sideshowbarker
Commit: 68894306e2
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2779
Reviewed-by: https://github.com/tcl3 ✅
4 changed files with 140 additions and 14 deletions
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue