LibWeb: Handle accessible-name computation for shadow roots and slots

This change adds handling for the “Determine Child Nodes” substep at
https://w3c.github.io/accname/#comp_name_from_content_find_child in the
“Accessible Name and Description Computation” spec. Specifically, it
adds handling for the “If the current node has an attached shadow root”
and “if the current node is a slot with assigned nodes” conditions.

Otherwise, without this change, AT users don’t hear the expected
accessible names in cases where the content for which an accessible name
being computed is in a shadow root or slot element.
This commit is contained in:
sideshowbarker 2024-11-22 18:04:38 +09:00 committed by Andreas Kling
commit e2a7f844e6
Notes: github-actions[bot] 2024-11-25 10:53:42 +00:00
5 changed files with 149 additions and 18 deletions

View file

@ -2365,8 +2365,9 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
// is not the empty string: // is not the empty string:
if (target == NameOrDescription::Name && element->aria_label().has_value() && !element->aria_label()->is_empty() && !element->aria_label()->bytes_as_string_view().is_whitespace()) { if (target == NameOrDescription::Name && element->aria_label().has_value() && !element->aria_label()->is_empty() && !element->aria_label()->bytes_as_string_view().is_whitespace()) {
// TODO: - If traversal of the current node is due to recursion and the current node is an embedded control as defined in step 2E, ignore aria-label and skip to rule 2E. // TODO: - If traversal of the current node is due to recursion and the current node is an embedded control as defined in step 2E, ignore aria-label and skip to rule 2E.
// - Otherwise, return the value of aria-label. // https://github.com/w3c/aria/pull/2385 and https://github.com/w3c/accname/issues/173
return element->aria_label().value(); if (!element->is_html_slot_element())
return element->aria_label().value();
} }
// E. Host Language Label: Otherwise, if the current node's native markup provides an attribute (e.g. alt) or // E. Host Language Label: Otherwise, if the current node's native markup provides an attribute (e.g. alt) or
@ -2444,39 +2445,46 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
else else
total_accumulated_text.append(before->computed_values().content().data); total_accumulated_text.append(before->computed_values().content().data);
} }
// iii. For each child node of the current node: // iii. Determine Child Nodes: Determine the rendered child nodes of the current node:
element->for_each_child([&total_accumulated_text, current_node, target, &document, &visited_nodes]( // iii. Determine Child Nodes: Determine the rendered child nodes of the current node:
DOM::Node const& child_node) mutable { // c. [Otherwise,] set the rendered child nodes to be the child nodes of the current node.
if (!child_node.is_element() && !child_node.is_text()) auto child_nodes = current_node->children_as_vector();
return IterationDecision::Continue; // a. If the current node has an attached shadow root, set the rendered child nodes to be the child nodes of
// the shadow root.
if (element->is_shadow_host() && element->shadow_root() && element->shadow_root()->is_connected())
child_nodes = element->shadow_root()->children_as_vector();
// b. Otherwise, if the current node is a slot with assigned nodes, set the rendered child nodes to be the
// assigned nodes of the current node.
if (element->is_html_slot_element()) {
total_accumulated_text.append(element->text_content().value());
child_nodes = static_cast<HTML::HTMLSlotElement const*>(element)->assigned_nodes();
}
// iv. Name From Each Child: For each rendered child node of the current node
for (auto& child_node : child_nodes) {
if (!child_node->is_element() && !child_node->is_text())
continue;
bool should_add_space = true; bool should_add_space = true;
const_cast<DOM::Document&>(document).update_layout(); const_cast<DOM::Document&>(document).update_layout();
auto const* layout_node = child_node.layout_node(); auto const* layout_node = child_node->layout_node();
if (layout_node) { if (layout_node) {
auto display = layout_node->display(); auto display = layout_node->display();
if (display.is_inline_outside() && display.is_flow_inside()) { if (display.is_inline_outside() && display.is_flow_inside()) {
should_add_space = false; should_add_space = false;
} }
} }
if (visited_nodes.contains(child_node->unique_id()))
if (visited_nodes.contains(child_node.unique_id())) continue;
return IterationDecision::Continue;
// a. Set the current node to the child node. // a. Set the current node to the child node.
current_node = &child_node; current_node = child_node;
// b. Compute the text alternative of the current node beginning with step 2. Set the result to that text alternative. // b. Compute the text alternative of the current node beginning with step 2. Set the result to that text alternative.
auto result = MUST(current_node->name_or_description(target, document, visited_nodes, IsDescendant::Yes)); auto result = MUST(current_node->name_or_description(target, document, visited_nodes, IsDescendant::Yes));
// Append a space character and the result of each step above to the total accumulated text. // Append a space character and the result of each step above to the total accumulated text.
// AD-HOC: Doing the space-adding here is in a different order from what the spec states. // AD-HOC: Doing the space-adding here is in a different order from what the spec states.
if (should_add_space) if (should_add_space)
total_accumulated_text.append(' '); total_accumulated_text.append(' ');
// c. Append the result to the accumulated text. // c. Append the result to the accumulated text.
total_accumulated_text.append(result); total_accumulated_text.append(result);
}
return IterationDecision::Continue;
});
// NOTE: See step ii.b above. // NOTE: See step ii.b above.
if (auto after = element->get_pseudo_element_node(CSS::Selector::PseudoElement::Type::After)) { if (auto after = element->get_pseudo_element_node(CSS::Selector::PseudoElement::Type::After)) {
if (after->computed_values().content().alt_text.has_value()) if (after->computed_values().content().alt_text.has_value())

View file

@ -0,0 +1,12 @@
Summary
Harness status: OK
Rerun
Found 2 tests
2 Pass
Details
Result Test Name MessagePass aria-labelledby reference to element with text content inside shadow DOM
Pass aria-labelledby reference to element with aria-label inside shadow DOM

View file

@ -0,0 +1,14 @@
Summary
Harness status: OK
Rerun
Found 4 tests
4 Pass
Details
Result Test Name MessagePass aria-labelledby reference to element with slotted text content
Pass aria-labelledby reference to element with default slotted text content
Pass aria-labelledby reference to element with slotted text content and aria-label on slot
Pass aria-labelledby reference to element with default slotted text content and aria-label on slot

View file

@ -0,0 +1,37 @@
<!doctype html>
<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>
<p>Tests the basic shadow DOM portions of the AccName <em>Name Computation</em> algorithm, coming in <a href="https://github.com/w3c/accname/pull/167">ARIA #167</a>.</p>
<label id="label1">
<div id="host1"></div>
</label>
<button id="labelled1"
class="labelled"
type="button"
aria-labelledby="label1"
data-expectedlabel="foo"
data-testname="aria-labelledby reference to element with text content inside shadow DOM"></button>
<label id="label2">
<div id="host2"></div>
</label>
<button id="labelled2"
class="labelled"
type="button"
aria-labelledby="label2"
data-expectedlabel="bar"
data-testname="aria-labelledby reference to element with aria-label inside shadow DOM"></button>
<script>
document.getElementById('host1').attachShadow({ mode: 'open' }).innerHTML = 'foo';
document.getElementById('host2').attachShadow({ mode: 'open' }).innerHTML = '<div aria-label="bar"></div>';
AriaUtils.verifyLabelsBySelector('.labelled');
</script>

View file

@ -0,0 +1,60 @@
<!doctype html>
<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>
<p>Tests the shadow DOM slots portions of the AccName <em>Name Computation</em> algorithm, coming in <a href="https://github.com/w3c/accname/pull/167">ARIA #167</a>.</p>
<label id="label1">
<div id="host1">slotted</div>
</label>
<button id="labelled1"
class="labelled"
type="button"
aria-labelledby="label1"
data-expectedlabel="foo slotted bar"
data-testname="aria-labelledby reference to element with slotted text content"></button>
<label id="label2">
<div id="host2"></div>
</label>
<button id="labelled2"
class="labelled"
type="button"
aria-labelledby="label2"
data-expectedlabel="foo default bar"
data-testname="aria-labelledby reference to element with default slotted text content"></button>
<label id="label3">
<div id="host3">slotted</div>
</label>
<button id="labelled3"
class="labelled"
type="button"
aria-labelledby="label3"
data-expectedlabel="foo slotted bar"
data-testname="aria-labelledby reference to element with slotted text content and aria-label on slot"></button>
<label id="label4">
<div id="host4"></div>
</label>
<button id="labelled4"
class="labelled"
type="button"
aria-labelledby="label4"
data-expectedlabel="foo default bar"
data-testname="aria-labelledby reference to element with default slotted text content and aria-label on slot"></button>
<script>
document.getElementById('host1').attachShadow({ mode: 'open' }).innerHTML = 'foo <slot></slot> bar';
document.getElementById('host2').attachShadow({ mode: 'open' }).innerHTML = 'foo <slot>default</slot> bar';
document.getElementById('host3').attachShadow({ mode: 'open' }).innerHTML = 'foo <slot aria-label="label"></slot> bar';
document.getElementById('host4').attachShadow({ mode: 'open' }).innerHTML = 'foo <slot aria-label="label">default</slot> bar';
AriaUtils.verifyLabelsBySelector('.labelled');
</script>