0

[A11y] Add actions for elements with button or link children

Option and menuitem elements can have button or link children. Surface
these children as implicit actions so that they are more discoverable.

https://chromestatus.com/feature/5161589307867136

https://github.com/w3c/aria/issues/1440
https://github.com/w3c/aria/pull/1805

Bug: 369781734
Change-Id: Ia9b9d78b13dc2f2bd3a48d09b6d6db28c7026f01
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6147720
Commit-Queue: Jocelyn Tran <jocelyntran@google.com>
Reviewed-by: Florin Malita <fmalita@chromium.org>
Reviewed-by: Aaron Leventhal <aleventhal@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1405567}
This commit is contained in:
Jocelyn Tran
2025-01-13 09:38:28 -08:00
committed by Chromium LUCI CQ
parent d70a55f2b2
commit 7dbd6a91b9
7 changed files with 141 additions and 0 deletions

@ -775,6 +775,13 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilityAriaActions) {
RunAriaTest(FILE_PATH_LITERAL("aria-actions.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
AccessibilityAriaActionsImplicit) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
RunAriaTest(FILE_PATH_LITERAL("aria-actions-implicit.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
AccessibilityAriaActionsReferenceTarget) {
RunAriaTest(FILE_PATH_LITERAL("aria-actions-reference-target.html"));

@ -388,6 +388,8 @@ data/accessibility/aria/aria-actions-expected-auralinux.txt
data/accessibility/aria/aria-actions-expected-blink.txt
data/accessibility/aria/aria-actions-expected-uia-win.txt
data/accessibility/aria/aria-actions-expected-win.txt
data/accessibility/aria/aria-actions-implicit-expected-blink.txt
data/accessibility/aria/aria-actions-implicit.html
data/accessibility/aria/aria-actions-reference-target-expected-blink.txt
data/accessibility/aria/aria-actions-reference-target.html
data/accessibility/aria/aria-actions-target-id-change-expected-blink.txt

@ -0,0 +1,47 @@
rootWebArea
++genericContainer ignored
++++genericContainer ignored
++++++comboBoxSelect collapsed value='hello world'
++++++++menuListPopup invisible ispopup=auto
++++++++++menuListOption name='hello world' selected=true actionsIds=button
++++++++++++genericContainer ignored
++++++++++++++button name='hello world'
++++++++++++++++staticText name='hello world'
++++++++++menuListOption invisible name='search engine' selected=false actionsIds=link
++++++++++++genericContainer ignored invisible
++++++++++++++link invisible name='search engine'
++++++++++++++++staticText invisible name='search engine'
++++++++++menuListOption invisible name='my-first-button my-second-button my-link' selected=false actionsIds=button,button,link
++++++++++++genericContainer ignored invisible
++++++++++++++button invisible name='my-first-button'
++++++++++++++++staticText invisible name='my-first-button'
++++++++++++++button invisible name='my-second-button'
++++++++++++++++staticText invisible name='my-second-button'
++++++++++++++link invisible name='my-link'
++++++++++++++++staticText invisible name='my-link'
++++++listBox
++++++++listBoxOption name='Item 1 hello world' selected=false actionsIds=button
++++++++++staticText name='Item 1'
++++++++++++inlineTextBox name='Item 1'
++++++++++button name='hello world'
++++++++++++staticText name='hello world'
++++++++++++++inlineTextBox name='hello world'
++++++++listBoxOption name='Item 2search engine' selected=false actionsIds=link
++++++++++staticText name='Item 2'
++++++++++++inlineTextBox name='Item 2'
++++++++++link name='search engine'
++++++++++++staticText name='search engine'
++++++++++++++inlineTextBox name='search engine'
++++++menu
++++++++menuItem name='File1 hello world' actionsIds=button
++++++++++staticText name='File1'
++++++++++++inlineTextBox name='File1'
++++++++++button name='hello world'
++++++++++++staticText name='hello world'
++++++++++++++inlineTextBox name='hello world'
++++++++menuItem name='File2search engine' actionsIds=link
++++++++++staticText name='File2'
++++++++++++inlineTextBox name='File2'
++++++++++link name='search engine'
++++++++++++staticText name='search engine'
++++++++++++++inlineTextBox name='search engine'

@ -0,0 +1,39 @@
<!--
@BLINK-ALLOW:actions*
-->
<select>
<button>
<selectedoption></selectedoption>
</button>
<option>
<button type="button">hello world</button>
</option>
<option>
<a href="google.com">search engine</a>
</option>
<option>
<button type="button">my-first-button</button>
<button type="button">my-second-button</button>
<a href="google.com">my-link</a>
</option>
</select>
<div role="listbox">
<div tabIndex="0" role="option">Item 1<button type="button">hello world</button></div>
<div tabIndex="1" role="option">Item 2<a href="google.com">search engine</a></div>
</div>
<div role="menu">
<div tabindex="0" role="menuitem">File1<button type="button">hello world</button></div>
<div tabindex="1" role="menuitem">File2<a href="google.com">search engine</a></div>
</div>
<style>
selectedoption .description {
display: none;
}
select, ::picker(select) {
appearance: base-select;
}
</style>

@ -1632,6 +1632,39 @@ void AXObject::SerializeColorAttributes(ui::AXNodeData* node_data) const {
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kColor, color);
}
void AXObject::SerializeImplicitActions(ui::AXNodeData* node_data) const {
// Serialize implicit actions for the following roles only.
if (RoleValue() != ax::mojom::blink::Role::kMenuItem &&
RoleValue() != ax::mojom::blink::Role::kListBoxOption &&
RoleValue() != ax::mojom::blink::Role::kMenuListOption) {
return;
}
// Sometimes the children of these elements are nested in a generic container,
// skip the container to reach the actions we wish to surface.
const AXObjectVector& children = ChildrenIncludingIgnored();
bool hasContainer =
children.size() == 1 &&
(children[0]->RoleValue() == ax::mojom::blink::Role::kGenericContainer ||
children[0]->RoleValue() == ax::mojom::blink::Role::kNone);
const AXObjectVector& potential_actions =
hasContainer ? children[0]->ChildrenIncludingIgnored() : children;
auto actions_ids = node_data->GetIntListAttribute(
ax::mojom::blink::IntListAttribute::kActionsIds);
for (const auto& child : potential_actions) {
if (child->RoleValue() == ax::mojom::blink::Role::kButton ||
child->RoleValue() == ax::mojom::blink::Role::kLink) {
actions_ids.push_back(child->AXObjectID());
}
}
if (!actions_ids.empty()) {
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kActionsIds, actions_ids);
}
}
void AXObject::SerializeElementAttributes(ui::AXNodeData* node_data) const {
Element* element = GetElement();
if (!element)
@ -2594,6 +2627,12 @@ void AXObject::SerializeUnignoredAttributes(ui::AXNodeData* node_data,
node_data->AddState(ax::mojom::blink::State::kHasActions);
}
// Author-defined actions should take precedence over implicit ones.
if (RuntimeEnabledFeatures::AccessibilityImplicitActionsEnabled() &&
!HasAriaAttribute(html_names::kAriaActionsAttr)) {
SerializeImplicitActions(node_data);
}
if (IsScrollableContainer())
SerializeScrollAttributes(node_data);

@ -1544,6 +1544,7 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
void SerializeChildTreeID(ui::AXNodeData* node_data) const;
void SerializeChooserPopupAttributes(ui::AXNodeData* node_data) const;
void SerializeColorAttributes(ui::AXNodeData* node_data) const;
void SerializeImplicitActions(ui::AXNodeData* node_data) const;
void SerializeElementAttributes(ui::AXNodeData* node_data) const;
void SerializeHTMLNonStandardAttributesForJAWS(
ui::AXNodeData* node_data) const;

@ -240,6 +240,12 @@
name: "AccessibilityExposeDisplayNone",
status: "test",
},
{
// If the author did not define aria-actions, surface button and link
// children inside option and menuitem elements as implicit actions.
name: "AccessibilityImplicitActions",
status: "experimental",
},
{
// Use a minimum role of group on elements that are keyboard-focusable.
// See https://w3c.github.io/html-aam/#minimum-role.