Skip to content

Commit

Permalink
LibWeb: Ignore “orphaned” ARIA roles
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sideshowbarker committed Dec 19, 2024
1 parent 30e1375 commit 5b20483
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 1 deletion.
90 changes: 89 additions & 1 deletion Libraries/LibWeb/ARIA/ARIAMixin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,98 @@ 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;
// 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Harness status: OK

Found 2 tests

2 Pass
Pass orphan p with listitem role
Pass orphan div with listitem role
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Harness status: OK

Found 1 tests

1 Pass
Pass orphaned option outside the context of listbox
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Harness status: OK

Found 2 tests

2 Pass
Pass orphan button with tab role
Pass orphan span with tab role
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>

0 comments on commit 5b20483

Please sign in to comment.