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
parent 6bb8bf189f
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:
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.
// - Otherwise, return the value of aria-label.
return element->aria_label().value();
// https://github.com/w3c/aria/pull/2385 and https://github.com/w3c/accname/issues/173
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
@ -2444,39 +2445,46 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
else
total_accumulated_text.append(before->computed_values().content().data);
}
// iii. For each child node of the current node:
element->for_each_child([&total_accumulated_text, current_node, target, &document, &visited_nodes](
DOM::Node const& child_node) mutable {
if (!child_node.is_element() && !child_node.is_text())
return IterationDecision::Continue;
// iii. Determine Child Nodes: Determine the rendered child nodes of the current node:
// iii. Determine Child Nodes: Determine the rendered child nodes of the current node:
// c. [Otherwise,] set the rendered child nodes to be the child nodes of the current node.
auto child_nodes = current_node->children_as_vector();
// 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;
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) {
auto display = layout_node->display();
if (display.is_inline_outside() && display.is_flow_inside()) {
should_add_space = false;
}
}
if (visited_nodes.contains(child_node.unique_id()))
return IterationDecision::Continue;
if (visited_nodes.contains(child_node->unique_id()))
continue;
// 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.
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.
// AD-HOC: Doing the space-adding here is in a different order from what the spec states.
if (should_add_space)
total_accumulated_text.append(' ');
// c. Append the result to the accumulated text.
total_accumulated_text.append(result);
return IterationDecision::Continue;
});
}
// NOTE: See step ii.b above.
if (auto after = element->get_pseudo_element_node(CSS::Selector::PseudoElement::Type::After)) {
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>