LibWeb: Ignore “orphaned” ARIA roles

This change causes explicitly-specified role attributes to be ignored in
the case where the specified role is “orphaned” — that is, when its
element lacks a required ancestor with an appropriate role.
This commit is contained in:
sideshowbarker 2024-12-19 12:27:18 +09:00 committed by Sam Atkins
parent 56c7857053
commit 2cb7baa581
Notes: github-actions[bot] 2025-01-09 14:09:47 +00:00
13 changed files with 334 additions and 1 deletions

View file

@ -25,7 +25,6 @@ Optional<Role> ARIAMixin::role_from_role_attribute_value() const
// 3. Compare the substrings to all the names of the non-abstract WAI-ARIA roles. Case-sensitivity of the comparison inherits from the case-sensitivity of the host language.
for (auto const& role_name : role_list) {
// 4. Use the first such substring in textual order that matches the name of a non-abstract WAI-ARIA role.
auto role = role_from_string(role_name);
if (!role.has_value())
continue;
@ -43,6 +42,95 @@ Optional<Role> ARIAMixin::role_from_role_attribute_value() const
// "synonym presentation role == computedrole none" test that expects "none", not "presentation".
if (role == Role::presentation)
return Role::none;
// https://w3c.github.io/core-aam/#roleMappingComputedRole
// When an element has a role but is not contained in the required context (for example, an orphaned listitem
// without the required accessible parent of role list), User Agents MUST ignore the role token, and return the
// computedrole as if the ignored role token had not been included.
if (role == ARIA::Role::columnheader) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (ancestor->role_or_default() == ARIA::Role::row)
return ARIA::Role::columnheader;
}
continue;
}
if (role == ARIA::Role::gridcell) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (ancestor->role_or_default() == ARIA::Role::row)
return ARIA::Role::gridcell;
}
continue;
}
if (role == ARIA::Role::listitem) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (first_is_one_of(ancestor->role_or_default(), ARIA::Role::directory, ARIA::Role::list))
return ARIA::Role::listitem;
}
continue;
}
if (role == ARIA::Role::menuitem) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (first_is_one_of(ancestor->role_or_default(), ARIA::Role::menu, ARIA::Role::menubar))
return ARIA::Role::menuitem;
}
continue;
}
if (role == ARIA::Role::menuitemcheckbox) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (first_is_one_of(ancestor->role_or_default(), ARIA::Role::menu, ARIA::Role::menubar))
return ARIA::Role::menuitemcheckbox;
}
continue;
}
if (role == ARIA::Role::menuitemradio) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (first_is_one_of(ancestor->role_or_default(), ARIA::Role::menu, ARIA::Role::menubar))
return ARIA::Role::menuitemradio;
}
continue;
}
if (role == ARIA::Role::option) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (ancestor->role_or_default() == ARIA::Role::listbox)
return ARIA::Role::option;
}
continue;
}
if (role == ARIA::Role::row) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (first_is_one_of(ancestor->role_or_default(), ARIA::Role::table, ARIA::Role::grid, ARIA::Role::treegrid))
return ARIA::Role::row;
}
continue;
}
if (role == ARIA::Role::rowgroup) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (first_is_one_of(ancestor->role_or_default(), ARIA::Role::table, ARIA::Role::grid, ARIA::Role::treegrid))
return ARIA::Role::rowgroup;
}
continue;
}
if (role == ARIA::Role::rowheader) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (ancestor->role_or_default() == ARIA::Role::row)
return ARIA::Role::rowheader;
}
continue;
}
if (role == ARIA::Role::tab) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (ancestor->role_or_default() == ARIA::Role::tablist)
return ARIA::Role::tab;
}
continue;
}
if (role == ARIA::Role::treeitem) {
for (auto const* ancestor = to_element()->parent_element(); ancestor; ancestor = ancestor->parent_element()) {
if (ancestor->role_or_default() == ARIA::Role::tree)
return ARIA::Role::treeitem;
}
continue;
}
// 4. Use the first such substring in textual order that matches the name of a non-abstract WAI-ARIA role.
if (!is_abstract_role(*role))
return *role;
}

View file

@ -0,0 +1,11 @@
Harness status: OK
Found 6 tests
6 Pass
Pass orphaned button with gridcell role outside the context of row
Pass orphaned row outside the context of table
Pass orphaned rowgroup outside the context of row
Pass orphaned div with gridcell role outside the context of row
Pass orphaned rowheader outside the context of row
Pass orphaned columnheader outside the context of row

View file

@ -0,0 +1,7 @@
Harness status: OK
Found 2 tests
2 Pass
Pass orphan p with listitem role
Pass orphan div with listitem role

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass orphaned option outside the context of listbox

View file

@ -0,0 +1,14 @@
Harness status: OK
Found 9 tests
9 Pass
Pass orphaned menuitem outside the context of menu/menubar
Pass orphaned menuitemradio outside the context of menu/menubar
Pass orphaned menuitemcheckbox outside the context of menu/menubar
Pass orphan button with menuitem role
Pass orphan button with menuitemcheckbox role
Pass orphan button with menuitemradio role
Pass orphan div with menuitem role
Pass orphan div with menuitemcheckbox role
Pass orphan div with menuitemradio role

View file

@ -0,0 +1,7 @@
Harness status: OK
Found 2 tests
2 Pass
Pass orphan button with tab role
Pass orphan span with tab role

View file

@ -0,0 +1,7 @@
Harness status: OK
Found 2 tests
2 Pass
Pass orphaned treeitem outside the context of tree
Pass orphaned button with treeitem role outside tree context

View file

@ -0,0 +1,33 @@
<!doctype html>
<html>
<head>
<title>Tentative: Grid 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>
<!--
CORE-AAM requires that, for elements with roles not contained in the
required context, user agents must ignore the role token and return the
computed role as if the ignored role token had not been included.
See https://w3c.github.io/core-aam/#roleMappingComputedRole
-->
<span role="row" data-testname="orphaned row outside the context of table" class="ex-generic">x</span>
<span role="rowgroup" data-testname="orphaned rowgroup outside the context of row" class="ex-generic">x</span>
<div role="gridcell" data-testname="orphaned div with gridcell role outside the context of row" class="ex-generic">x</div>
<button role="gridcell" data-testname="orphaned button with gridcell role outside the context of row" data-expectedrole="button" class="ex">x</button>
<div role="rowheader" data-testname="orphaned rowheader outside the context of row" class="ex-generic">x</div>
<div role="columnheader" data-testname="orphaned columnheader outside the context of row" class="ex-generic">x</div>
<script>
AriaUtils.verifyRolesBySelector(".ex");
AriaUtils.verifyGenericRolesBySelector(".ex-generic");
</script>
</body>
</html>

View file

@ -0,0 +1,29 @@
<!doctype html>
<html>
<head>
<title>Tentative: List-related 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>
<!--
CORE-AAM requires that, for elements with roles not contained in the
required context, user agents must ignore the role token and return the
computed role as if the ignored role token had not been included.
See https://w3c.github.io/core-aam/#roleMappingComputedRole
-->
<div role="listitem" data-testname="orphan div with listitem role" class="ex-generic">x</div>
<p role="listitem" data-testname="orphan p with listitem role" data-expectedrole="paragraph" class="ex">x</p>
<script>
AriaUtils.verifyRolesBySelector(".ex");
AriaUtils.verifyGenericRolesBySelector(".ex-generic");
</script>
</body>
</html>

View file

@ -0,0 +1,29 @@
<!doctype html>
<html>
<head>
<title>Tentative: Listbox-related 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>
<!--
CORE-AAM requires that, for elements with roles not contained in the
required context, user agents must ignore the role token and return the
computed role as if the ignored role token had not been included.
See https://w3c.github.io/core-aam/#roleMappingComputedRole
-->
<nav role="option" data-testname="orphaned option outside the context of listbox" data-expectedrole="navigation"
class="ex">x
</nav>
<script>
AriaUtils.verifyRolesBySelector(".ex");
</script>
</body>
</html>

View file

@ -0,0 +1,45 @@
<!doctype html>
<html>
<head>
<title>Tentative: Menu-related 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>
<!--
CORE-AAM requires that, for elements with roles not contained in the
required context, user agents must ignore the role token and return the
computed role as if the ignored role token had not been included.
See https://w3c.github.io/core-aam/#roleMappingComputedRole
-->
<nav role="menuitem" data-testname="orphaned menuitem outside the context of menu/menubar" data-expectedrole="navigation"
class="ex">x
</nav>
<nav role="menuitemradio" data-testname="orphaned menuitemradio outside the context of menu/menubar" data-expectedrole="navigation"
class="ex">x
</nav>
<nav role="menuitemcheckbox" data-testname="orphaned menuitemcheckbox outside the context of menu/menubar" data-expectedrole="navigation"
class="ex">x
</nav>
<button role="menuitem" data-testname="orphan button with menuitem role" data-expectedrole="button" class="ex">x</button>
<div role="menuitem" data-testname="orphan div with menuitem role" class="ex-generic">x</div>
<button role="menuitemcheckbox" data-testname="orphan button with menuitemcheckbox role" data-expectedrole="button" class="ex">x</button>
<div role="menuitemcheckbox" data-testname="orphan div with menuitemcheckbox role" class="ex-generic">x</div>
<button role="menuitemradio" data-testname="orphan button with menuitemradio role" data-expectedrole="button" class="ex">x</button>
<div role="menuitemradio" data-testname="orphan div with menuitemradio role" class="ex-generic">x</div>
<script>
AriaUtils.verifyRolesBySelector(".ex");
AriaUtils.verifyGenericRolesBySelector(".ex-generic");
</script>
</body>
</html>

View file

@ -0,0 +1,29 @@
<!doctype html>
<html>
<head>
<title>Tentative: Tab-related 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>
<!--
CORE-AAM requires that, for elements with roles not contained in the
required context, user agents must ignore the role token and return the
computed role as if the ignored role token had not been included.
See https://w3c.github.io/core-aam/#roleMappingComputedRole
-->
<button role="tab" data-testname="orphan button with tab role" data-expectedrole="button" class="ex">x</button>
<span role="tab" data-testname="orphan span with tab role" class="ex-generic">x</span>
<script>
AriaUtils.verifyRolesBySelector(".ex");
AriaUtils.verifyGenericRolesBySelector(".ex-generic");
</script>
</body>
</html>

View file

@ -0,0 +1,28 @@
<!doctype html>
<html>
<head>
<title>Tentative: Tree related 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>
<!--
CORE-AAM requires that, for elements with roles not contained in the
required context, user agents must ignore the role token and return the
computed role as if the ignored role token had not been included.
See https://w3c.github.io/core-aam/#roleMappingComputedRole
-->
<nav role="treeitem" data-testname="orphaned treeitem outside the context of tree" data-expectedrole="navigation" class="ex">x</nav>
<button role="treeitem" data-testname="orphaned button with treeitem role outside tree context" data-expectedrole="button" class="ex">x</button>
<script>
AriaUtils.verifyRolesBySelector(".ex");
</script>
</body>
</html>