Reland "Make inferred roles for CSS toggles influence accessibility role."
This is a reland of commit23486a5ee1
and commit2957ce3557
. Original change's description: > Make inferred roles for CSS toggles influence accessibility role. > > The tests added in this CL are mostly testing the inference engine added > in the previous CL. > > Support for toggles is controlled by the CSSToggles flag (currently off) > in RuntimeEnabledFeatures. > > Bug: 1250716 > Change-Id: Ia0b38b5c7ffacc4d2a6eec88ecfe0ada852c4b09 > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4200858 > Reviewed-by: Aaron Leventhal <aleventhal@chromium.org> > Commit-Queue: David Baron <dbaron@chromium.org> > Cr-Commit-Position: refs/heads/main@{#1106967} Original change's description: > Refactor CSS toggle influence on ARIA role into its own function. > > Support for toggles is controlled by the CSSToggles flag (currently off) > in RuntimeEnabledFeatures. > > Bug: 1250716 > Change-Id: I7751d1d48a3444cc249979185ff5517ecd7c5e01 > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4265191 > Reviewed-by: Aaron Leventhal <aleventhal@chromium.org> > Commit-Queue: David Baron <dbaron@chromium.org> > Cr-Commit-Position: refs/heads/main@{#1106983} Bug: 1250716 Change-Id: I8dab23d70c84a2d3b845c79c245756ccab13e88d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4269430 Commit-Queue: Aaron Leventhal <aleventhal@chromium.org> Reviewed-by: Aaron Leventhal <aleventhal@chromium.org> Auto-Submit: David Baron <dbaron@chromium.org> Cr-Commit-Position: refs/heads/main@{#1107814}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
2d3b3e6f8a
commit
bfdf009143
third_party/blink
renderer
web_tests
external
wpt
css
css-toggle
@ -534,7 +534,8 @@ void CSSToggleInference::Rebuild() {
|
||||
element_roles_.insert(toggle_root, CSSToggleRole::kCheckbox);
|
||||
// TODO(dbaron): We should only set parent_role here when
|
||||
// there are multiple checkbox siblings with few non-checkbox
|
||||
// siblings!
|
||||
// siblings! (Once we do this we can probably remove the
|
||||
// tabs_ish_elements.Contains(parent) test below.)
|
||||
parent_role = CSSToggleRole::kCheckboxGroup;
|
||||
}
|
||||
|
||||
@ -543,7 +544,8 @@ void CSSToggleInference::Rebuild() {
|
||||
// order to ensure that hash map processing order doesn't affect
|
||||
// the result).
|
||||
if (parent_role != CSSToggleRole::kNone && parent &&
|
||||
!accordion_ish_elements.Contains(parent)) {
|
||||
!accordion_ish_elements.Contains(parent) &&
|
||||
!tabs_ish_elements.Contains(parent)) {
|
||||
auto parent_add_result = element_roles_.insert(parent, parent_role);
|
||||
// prefer checkbox group to radio group if some children
|
||||
// lead to either
|
||||
|
@ -46,6 +46,7 @@
|
||||
#include "third_party/blink/renderer/core/css/css_resolution_units.h"
|
||||
#include "third_party/blink/renderer/core/css/properties/longhands.h"
|
||||
#include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h"
|
||||
#include "third_party/blink/renderer/core/dom/css_toggle_inference.h"
|
||||
#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
|
||||
#include "third_party/blink/renderer/core/dom/layout_tree_builder_traversal.h"
|
||||
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
|
||||
@ -1343,6 +1344,75 @@ ax::mojom::blink::Role AXNodeObject::NativeRoleIgnoringAria() const {
|
||||
return RoleFromLayoutObjectOrNode();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
ax::mojom::blink::Role InferredCSSToggleRole(Node* node) {
|
||||
Element* element = DynamicTo<Element>(node);
|
||||
if (!element) {
|
||||
return ax::mojom::blink::Role::kUnknown;
|
||||
}
|
||||
|
||||
// toggle_inference is null when CSS toggles are not used in the document.
|
||||
CSSToggleInference* toggle_inference =
|
||||
element->GetDocument().GetCSSToggleInference();
|
||||
if (!toggle_inference) {
|
||||
return ax::mojom::blink::Role::kUnknown;
|
||||
}
|
||||
|
||||
DCHECK(RuntimeEnabledFeatures::CSSTogglesEnabled());
|
||||
|
||||
switch (toggle_inference->RoleForElement(element)) {
|
||||
case CSSToggleRole::kNone:
|
||||
break;
|
||||
case CSSToggleRole::kButtonWithPopup:
|
||||
return ax::mojom::blink::Role::kPopUpButton;
|
||||
case CSSToggleRole::kDisclosure:
|
||||
break;
|
||||
case CSSToggleRole::kDisclosureButton:
|
||||
return ax::mojom::blink::Role::kButton;
|
||||
case CSSToggleRole::kTree:
|
||||
return ax::mojom::blink::Role::kTree;
|
||||
case CSSToggleRole::kTreeGroup:
|
||||
return ax::mojom::blink::Role::kGroup;
|
||||
case CSSToggleRole::kTreeItem:
|
||||
return ax::mojom::blink::Role::kTreeItem;
|
||||
case CSSToggleRole::kAccordion:
|
||||
break;
|
||||
case CSSToggleRole::kAccordionItem:
|
||||
return ax::mojom::blink::Role::kRegion;
|
||||
case CSSToggleRole::kAccordionItemButton:
|
||||
return ax::mojom::blink::Role::kButton;
|
||||
case CSSToggleRole::kTabContainer:
|
||||
// TODO(dbaron): We should verify that using kTabList really
|
||||
// works here, since this is a container that has both the tab
|
||||
// list *and* the tab panels. We should also make sure that
|
||||
// posinset/setsize work correctly for the tabs.
|
||||
return ax::mojom::blink::Role::kTabList;
|
||||
case CSSToggleRole::kTab:
|
||||
return ax::mojom::blink::Role::kTab;
|
||||
case CSSToggleRole::kTabPanel:
|
||||
return ax::mojom::blink::Role::kTabPanel;
|
||||
case CSSToggleRole::kRadioGroup:
|
||||
return ax::mojom::blink::Role::kRadioGroup;
|
||||
case CSSToggleRole::kRadioItem:
|
||||
return ax::mojom::blink::Role::kRadioButton;
|
||||
case CSSToggleRole::kCheckboxGroup:
|
||||
break;
|
||||
case CSSToggleRole::kCheckbox:
|
||||
return ax::mojom::blink::Role::kCheckBox;
|
||||
case CSSToggleRole::kListbox:
|
||||
return ax::mojom::blink::Role::kListBox;
|
||||
case CSSToggleRole::kListboxItem:
|
||||
return ax::mojom::blink::Role::kListBoxOption;
|
||||
case CSSToggleRole::kButton:
|
||||
return ax::mojom::blink::Role::kButton;
|
||||
}
|
||||
|
||||
return ax::mojom::blink::Role::kUnknown;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ax::mojom::blink::Role AXNodeObject::DetermineAccessibilityRole() {
|
||||
#if DCHECK_IS_ON()
|
||||
base::AutoReset<bool> reentrancy_protector(&is_computing_role_, true);
|
||||
@ -1357,8 +1427,25 @@ ax::mojom::blink::Role AXNodeObject::DetermineAccessibilityRole() {
|
||||
|
||||
aria_role_ = DetermineAriaRoleAttribute();
|
||||
|
||||
return aria_role_ == ax::mojom::blink::Role::kUnknown ? native_role_
|
||||
: aria_role_;
|
||||
// Order of precedence is currently:
|
||||
// 1. ARIA role
|
||||
// 2. Inferred role from CSS Toggle inference engine
|
||||
// 3. Native markup role
|
||||
// but we may decide to change how the CSS Toggle inference fits in.
|
||||
//
|
||||
// TODO(dbaron): Perhaps revisit whether there are types of elements
|
||||
// where toggles should not work.
|
||||
|
||||
if (aria_role_ != ax::mojom::blink::Role::kUnknown) {
|
||||
return aria_role_;
|
||||
}
|
||||
|
||||
ax::mojom::blink::Role css_toggle_role = InferredCSSToggleRole(GetNode());
|
||||
if (css_toggle_role != ax::mojom::blink::Role::kUnknown) {
|
||||
return css_toggle_role;
|
||||
}
|
||||
|
||||
return native_role_;
|
||||
}
|
||||
|
||||
void AXNodeObject::AccessibilityChildrenFromAOMProperty(
|
||||
@ -1417,7 +1504,8 @@ void AXNodeObject::Init(AXObject* parent) {
|
||||
#endif
|
||||
AXObject::Init(parent);
|
||||
|
||||
DCHECK(role_ == native_role_ || role_ == aria_role_)
|
||||
DCHECK(role_ == native_role_ || role_ == aria_role_ ||
|
||||
GetNode()->GetDocument().GetCSSToggleInference())
|
||||
<< "Role must be either the cached native role or cached aria role: "
|
||||
<< "\n* Final role: " << role_ << "\n* Native role: " << native_role_
|
||||
<< "\n* Aria role: " << aria_role_ << "\n* Node: " << GetNode();
|
||||
|
354
third_party/blink/web_tests/external/wpt/css/css-toggle/toggle-aria-roles.tentative.html
vendored
Normal file
354
third_party/blink/web_tests/external/wpt/css/css-toggle/toggle-aria-roles.tentative.html
vendored
Normal file
@ -0,0 +1,354 @@
|
||||
<!DOCTYPE HTML>
|
||||
<meta charset="UTF-8">
|
||||
<title>CSS Toggles: ARIA roles</title>
|
||||
<link rel="author" title="L. David Baron" href="https://dbaron.org/">
|
||||
<link rel="author" title="Google" href="http://www.google.com/">
|
||||
<link rel="help" href="https://tabatkins.github.io/css-toggle/">
|
||||
<link rel="help" href="https://github.com/tabatkins/css-toggle/issues/41">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="support/toggle-helpers.js"></script>
|
||||
<style id="style"></style>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="container"></div>
|
||||
<script>
|
||||
|
||||
let aria_role_tests = [
|
||||
// Markup to create the test assertions:
|
||||
// data-expected-role: The expected aria role for this element.
|
||||
//
|
||||
// Helper markup to create more markup:
|
||||
// class=group: group the group with the toggle-group property
|
||||
// class=group-self: same, but with the self keyword (narrow scope)
|
||||
// class=root: create a test-role toggle with the toggle-root property
|
||||
// class=root-group: same, but with the 'group' keyword
|
||||
// class=root-self: same, but with the 'self' keyword
|
||||
// class=trigger: toggle-trigger to activate test-role toggle
|
||||
// class=visibility: toggle-visibility connected to test-role toggle
|
||||
`
|
||||
<div></div>
|
||||
`,
|
||||
`
|
||||
<div class="root">
|
||||
<div></div>
|
||||
</div>
|
||||
`,
|
||||
`
|
||||
<div class="root trigger" data-expected-role="checkbox"></div>
|
||||
`,
|
||||
// Test that ARIA attributes override the toggle inference:
|
||||
`
|
||||
<div class="root trigger" role="link" data-expected-role="link"></div>
|
||||
`,
|
||||
`
|
||||
<div class="root">
|
||||
<div class="trigger" data-expected-role="button"></div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
// Radios and radio groups:
|
||||
`
|
||||
<div class="group" data-expected-role="radiogroup">
|
||||
<div class="root-group trigger" data-expected-role="radio"></div>
|
||||
</div>
|
||||
`,
|
||||
`
|
||||
<div class="group" data-expected-role="radiogroup">
|
||||
<div class="root-group trigger" data-expected-role="radio"></div>
|
||||
<div class="root-group trigger" data-expected-role="radio"></div>
|
||||
</div>
|
||||
`,
|
||||
`
|
||||
<div>
|
||||
<div class="root-group trigger" data-expected-role="radio"></div>
|
||||
</div>
|
||||
`,
|
||||
`
|
||||
<div style="toggle-group: another-group">
|
||||
<div class="root-group trigger" data-expected-role="radio"></div>
|
||||
</div>
|
||||
`,
|
||||
`
|
||||
<div style="toggle-group: another-group, test-role, third-group" data-expected-role="radiogroup">
|
||||
<div class="root-group trigger" data-expected-role="radio"></div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
|
||||
// Checkboxes and checkbox groups:
|
||||
`
|
||||
<div>
|
||||
<div class="root trigger" data-expected-role="checkbox"></div>
|
||||
</div>
|
||||
`,
|
||||
// TODO(dbaron): This is a checkbox group... but we can't distinguish
|
||||
// that with current ARIA roles.
|
||||
`
|
||||
<div>
|
||||
<div class="root trigger" data-expected-role="checkbox"></div>
|
||||
<div class="root trigger" data-expected-role="checkbox"></div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
// Disclosure:
|
||||
// TODO(dbaron): This is a disclosure... but how is it possible to
|
||||
// distinguish with ARIA roles (compare to next test!)?
|
||||
`
|
||||
<div class="root">
|
||||
<div class="trigger" data-expected-role="button"></div>
|
||||
<div class="visibility"></div>
|
||||
</div>
|
||||
`,
|
||||
// This is not a disclosure because it has a toggle-group.
|
||||
`
|
||||
<div class="root-group">
|
||||
<div class="trigger" data-expected-role="button"></div>
|
||||
<div class="visibility"></div>
|
||||
</div>
|
||||
`,
|
||||
// This is button with popup (absolute positioning)
|
||||
// TODO(dbaron): This test doesn't actually distinguish this from
|
||||
// disclosure because the internal kPopUpButton role maps to "button"
|
||||
// in kReverseRoles in ax_object.cc.
|
||||
`
|
||||
<div class="root">
|
||||
<div class="trigger" data-expected-role="button"></div>
|
||||
<div class="visibility" style="position: absolute"></div>
|
||||
</div>
|
||||
`,
|
||||
// This is button with popup (fixed positioning)
|
||||
// TODO(dbaron): This test doesn't actually distinguish this from
|
||||
// disclosure because the internal kPopUpButton role maps to "button"
|
||||
// in kReverseRoles in ax_object.cc.
|
||||
`
|
||||
<div class="root">
|
||||
<div class="trigger" data-expected-role="button"></div>
|
||||
<div class="visibility" style="position: fixed"></div>
|
||||
</div>
|
||||
`,
|
||||
// This is button with popup (popover)
|
||||
// TODO(dbaron): This test doesn't actually distinguish this from
|
||||
// disclosure because the internal kPopUpButton role maps to "button"
|
||||
// in kReverseRoles in ax_object.cc.
|
||||
`
|
||||
<div class="root">
|
||||
<div class="trigger" data-expected-role="button"></div>
|
||||
<div class="visibility" popover="auto"></div>
|
||||
</div>
|
||||
`,
|
||||
// This is disclosure (NOT button with popup) (sticky positioning)
|
||||
`
|
||||
<div class="root">
|
||||
<div class="trigger" data-expected-role="button"></div>
|
||||
<div class="visibility" style="position: sticky"></div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
// Accordion:
|
||||
`
|
||||
<div class="group">
|
||||
<div class="root-group" data-expected-role="region">
|
||||
<div class="trigger" data-expected-role="button"></div>
|
||||
<div class="visibility"></div>
|
||||
</div>
|
||||
<div class="root-group" data-expected-role="region">
|
||||
<div class="trigger" data-expected-role="button"></div>
|
||||
<div class="visibility"></div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
// Not accordion because of other siblings:
|
||||
`
|
||||
<div class="group">
|
||||
<div class="root-group">
|
||||
<div class="trigger" data-expected-role="button"></div>
|
||||
<div class="visibility"></div>
|
||||
</div>
|
||||
<div class="root-group">
|
||||
<div class="trigger" data-expected-role="button"></div>
|
||||
<div class="visibility"></div>
|
||||
</div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
// Tree:
|
||||
// TODO(dbaron): This should probably also work with the toggles on
|
||||
// the <button>!
|
||||
// TODO(dbaron): This should probably mark the non-interactive items
|
||||
// as treeitem as well!
|
||||
// TODO(dbaron): Do the elements getting the roles here make sense?
|
||||
// TODO(dbaron): The requirement for having multiple disclosure-ish
|
||||
// children to qualify as accordion-ish probably doesn't make sense
|
||||
// here. The test below is basically the minimal example that gets
|
||||
// detected as a tree, but simpler things definitely should be!
|
||||
// TODO(dbaron): The inner parts of the tree should also be getting
|
||||
// tree roles!
|
||||
`
|
||||
<ul data-expected-role="tree">
|
||||
<li class="root-self" data-expected-role="group">
|
||||
<button class="trigger" data-expected-role="treeitem"></button>
|
||||
<ul class="visibility" data-expected-role="list">
|
||||
<li>item</li>
|
||||
<li class="root-self">
|
||||
<button class="trigger" data-expected-role="button"></button>
|
||||
<ul class="visibility" data-expected-role="list">
|
||||
<li>item</li>
|
||||
<li>item</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="root-self">
|
||||
<button class="trigger" data-expected-role="button"></button>
|
||||
<ul class="visibility" data-expected-role="list">
|
||||
<li>item</li>
|
||||
<li>item</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="root-self" data-expected-role="group">
|
||||
<button class="trigger" data-expected-role="treeitem"></button>
|
||||
<ul class="visibility" data-expected-role="list">
|
||||
<li class="root-self">
|
||||
<button class="trigger" data-expected-role="button"></button>
|
||||
<ul class="visibility" data-expected-role="list">
|
||||
<li>item</li>
|
||||
<li>item</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="root-self">
|
||||
<button class="trigger" data-expected-role="button"></button>
|
||||
<ul class="visibility" data-expected-role="list">
|
||||
<li>item</li>
|
||||
<li>item</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
|
||||
// Tabs:
|
||||
`
|
||||
<section class="group" data-expected-role="tablist">
|
||||
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
||||
<div class="visibility" data-expected-role="tabpanel"></div>
|
||||
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
||||
<div class="visibility" data-expected-role="tabpanel"></div>
|
||||
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
||||
<div class="visibility" data-expected-role="tabpanel"></div>
|
||||
</section>
|
||||
`,
|
||||
`
|
||||
<section class="group" data-expected-role="tablist">
|
||||
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
||||
<div class="visibility" data-expected-role="tabpanel"></div>
|
||||
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
||||
<div class="visibility" data-expected-role="tabpanel"></div>
|
||||
<div></div>
|
||||
</section>
|
||||
`,
|
||||
`
|
||||
<section class="group" data-expected-role="tablist">
|
||||
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
||||
<div class="visibility" data-expected-role="tabpanel"></div>
|
||||
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
||||
<div class="visibility" data-expected-role="tabpanel"></div>
|
||||
<h1 style="toggle-root: other-toggle; toggle-trigger: other-toggle" data-expected-role="checkbox"></h1>
|
||||
</section>
|
||||
`,
|
||||
`
|
||||
<section class="group" data-expected-role="tablist">
|
||||
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
||||
<div class="visibility" data-expected-role="tabpanel"></div>
|
||||
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
||||
<div class="visibility" data-expected-role="tabpanel"></div>
|
||||
<h1 style="toggle-root: other-toggle; toggle-trigger: other-toggle" data-expected-role="button"></h1>
|
||||
<div style="toggle-visibility: toggle other-toggle"></div>
|
||||
</section>
|
||||
`,
|
||||
// TODO(https://crbug.com/758089): The expected role for the <section>
|
||||
// should be generic rather than null!
|
||||
`
|
||||
<section class="group" data-expected-role="null">
|
||||
<h1 class="root-group trigger" data-expected-role="button"></h1>
|
||||
<div class="visibility"></div>
|
||||
<h1 class="root-group trigger" data-expected-role="button"></h1>
|
||||
<div class="visibility"></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</section>
|
||||
`,
|
||||
`
|
||||
<section class="group" data-expected-role="radiogroup">
|
||||
<h1 class="root-group trigger" data-expected-role="radio"></h1>
|
||||
<h1 class="root-group trigger" data-expected-role="radio"></h1>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</section>
|
||||
`,
|
||||
];
|
||||
|
||||
for (let t of aria_role_tests) {
|
||||
promise_test(async function() {
|
||||
container.innerHTML = t;
|
||||
|
||||
for (let e of container.querySelectorAll('.group')) {
|
||||
e.style.toggleGroup = "test-role";
|
||||
}
|
||||
for (let e of container.querySelectorAll('.group-self')) {
|
||||
e.style.toggleGroup = "test-role self";
|
||||
}
|
||||
for (let e of container.querySelectorAll('.root')) {
|
||||
e.style.toggleRoot = "test-role";
|
||||
}
|
||||
for (let e of container.querySelectorAll('.root-group')) {
|
||||
e.style.toggleRoot = "test-role group";
|
||||
}
|
||||
for (let e of container.querySelectorAll('.root-self')) {
|
||||
e.style.toggleRoot = "test-role self";
|
||||
}
|
||||
for (let e of container.querySelectorAll('.trigger')) {
|
||||
e.style.toggleTrigger = "test-role";
|
||||
}
|
||||
for (let e of container.querySelectorAll('.visibility')) {
|
||||
e.style.toggleVisibility = "toggle test-role";
|
||||
}
|
||||
|
||||
for (let e of container.querySelectorAll('.root, .root-nogroup')) {
|
||||
await wait_for_toggle_creation(e);
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (let e of container.querySelectorAll("*")) {
|
||||
if (e == container)
|
||||
continue;
|
||||
|
||||
let expected_role = "generic";
|
||||
if (e.hasAttribute("data-expected-role")) {
|
||||
expected_role = e.getAttribute("data-expected-role");
|
||||
// TODO(https://crbug.com/758089): See above regarding <section>;
|
||||
// this null handling should eventually be removed.
|
||||
if (expected_role === "null") {
|
||||
expected_role = null;
|
||||
}
|
||||
}
|
||||
++count;
|
||||
// NOTE: This relies on Element.computedRole, which is an
|
||||
// experimental feature behind the ComputedAccessibilityInfo flag
|
||||
// in blink.
|
||||
assert_equals(e.computedRole, expected_role, `role on ${e.tagName} element (#${count})`);
|
||||
}
|
||||
|
||||
}, `aria role test: ${t}`);
|
||||
}
|
||||
|
||||
</script>
|
Reference in New Issue
Block a user