LibWeb: Correctly descend element nodes when computing accessible name

This change implements the “is a descendant of a native host language
text alternative element” condition in the “F: Name From Content” step
at https://w3c.github.io/accname/#step2F in the “Accessible Name and
Description Computation” spec — to ensure that all descendant nodes get
included as expected in computations for accessible names for elements.

Otherwise, without this change, Ladybird unexpectedly skips descendant
element nodes when computing accessible names — which can result in the
wrong accessible name being returned.
This commit is contained in:
sideshowbarker 2024-11-01 21:11:32 +09:00 committed by Tim Flynn
parent aeab342fd7
commit 437879f849
Notes: github-actions[bot] 2024-11-01 22:14:36 +00:00
4 changed files with 210 additions and 4 deletions

View file

@ -0,0 +1,60 @@
Summary
Harness status: OK
Rerun
Found 50 tests
50 Pass
Details
Result Test Name MessagePass span[role=button] with text/element/text nodes, no space
Pass div[role=heading] with text/element/text nodes, no space
Pass button with text/element/text nodes, no space
Pass heading with text/element/text nodes, no space
Pass link with text/element/text nodes, no space
Pass span[role=button] with text/comment/text nodes, no space
Pass div[role=heading] with text/comment/text nodes, no space
Pass button with text/comment/text nodes, no space
Pass heading with text/comment/text nodes, no space
Pass link with text/comment/text nodes, no space
Pass span[role=button] with text/comment/text nodes, with space
Pass div[role=heading] with text/comment/text nodes, with space
Pass button with text/comment/text nodes, with space
Pass heading with text/comment/text nodes, with space
Pass link with text/comment/text nodes, with space
Pass span[role=button] with text node, with tab char
Pass div[role=heading] with text node, with tab char
Pass button with text node, with tab char
Pass heading with text node, with tab char
Pass link with text node, with tab char
Pass span[role=button] with text node, with non-breaking space
Pass div[role=heading] with text node, with non-breaking space
Pass button with text node, with non-breaking space
Pass heading with text node, with non-breaking space
Pass link with text node, with non-breaking space
Pass span[role=button] with text node, with extra non-breaking space
Pass div[role=heading] with text node, with extra non-breaking space
Pass button with text node, with extra non-breaking space
Pass heading with text node, with extra non-breaking space
Pass link with text node, with extra non-breaking space
Pass span[role=button] with text node, with leading/trailing non-breaking space
Pass div[role=heading] with text node, with leading/trailing non-breaking space
Pass button with text node, with leading/trailing non-breaking space
Pass heading with text node, with leading/trailing non-breaking space
Pass link with text node, with leading/trailing non-breaking space
Pass span[role=button] with text node, with mixed space and non-breaking space
Pass div[role=heading] with text node, with mixed space and non-breaking space
Pass button with text node, with mixed space and non-breaking space
Pass heading with text node, with mixed space and non-breaking space
Pass link with text node, with mixed space and non-breaking space
Pass span[role=button] with text node, with deeply nested space
Pass div[role=heading] with text node, with deeply nested space
Pass button with text node, with deeply nested space
Pass heading with text node, with deeply nested space
Pass link with text node, with deeply nested space
Pass span[role=button] with text node, with single line break
Pass div[role=heading] with text node, with single line break
Pass button with text node, with single line break
Pass heading with text node, with single line break
Pass link with text node, with single line break

View file

@ -0,0 +1,141 @@
<!DOCTYPE html>
<html>
<head>
<title>Name Comp: Text Node</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>
<p>Tests the <a href="https://w3c.github.io/accname/#comp_text_node">#comp_text_node</a> portions of the AccName <em>Name Computation</em> algorithm.</p>
<!--
Note: some overlap with the tests in:
- /accname/name/comp_label.html
- /accname/name/comp_name_from_content.html
-->
<h1>text/element/text nodes, no space</h1>
<span class="ex" data-expectedlabel="buttonlabel" data-testname="span[role=button] with text/element/text nodes, no space" role="button" tabindex="0">button<span></span>label</span>
<div class="ex" data-expectedlabel="headinglabel" data-testname="div[role=heading] with text/element/text nodes, no space" role="heading">heading<span></span>label</div>
<button class="ex" data-expectedlabel="buttonlabel" data-testname="button with text/element/text nodes, no space">button<span></span>label</button>
<h3 class="ex" data-expectedlabel="headinglabel" data-testname="heading with text/element/text nodes, no space">heading<span></span>label</h3>
<a class="ex" data-expectedlabel="linklabel" data-testname="link with text/element/text nodes, no space" href="#">link<span></span>label</a>
<br/>
<h1>text/comment/text nodes, no space</h1>
<!-- Note: This set is not currently to spec until https://github.com/w3c/accname/issues/193 is resolved. -->
<span class="ex" data-expectedlabel="buttonlabel" data-testname="span[role=button] with text/comment/text nodes, no space" role="button" tabindex="0">
button<!-- with non-text node splitting concatenated text nodes -->label<!-- [sic] no extra spaces around first comment -->
</span>
<div class="ex" data-expectedlabel="headinglabel" data-testname="div[role=heading] with text/comment/text nodes, no space" role="heading">
heading<!-- with non-text node splitting concatenated text nodes -->label<!-- [sic] no extra spaces around first comment -->
</div>
<button class="ex" data-expectedlabel="buttonlabel" data-testname="button with text/comment/text nodes, no space">
button<!-- with non-text node splitting concatenated text nodes -->label<!-- [sic] no extra spaces around first comment -->
</button>
<h3 class="ex" data-expectedlabel="headinglabel" data-testname="heading with text/comment/text nodes, no space">
heading<!-- with non-text node splitting concatenated text nodes -->label<!-- [sic] no extra spaces around first comment -->
</h3>
<a class="ex" data-expectedlabel="linklabel" data-testname="link with text/comment/text nodes, no space" href="#">
link<!-- with non-text node splitting concatenated text nodes -->label<!-- [sic] no extra spaces around first comment -->
</a>
<br/>
<h1>text/comment/text nodes, with space</h1>
<span class="ex" data-expectedlabel="button label" data-testname="span[role=button] with text/comment/text nodes, with space" role="button" tabindex="0">
button
<!-- comment node between text nodes with leading/trailing whitespace -->
label
</span>
<div class="ex" data-expectedlabel="heading label" data-testname="div[role=heading] with text/comment/text nodes, with space" role="heading">
heading
<!-- comment node between text nodes with leading/trailing whitespace -->
label
</div>
<button class="ex" data-expectedlabel="button label" data-testname="button with text/comment/text nodes, with space">
button
<!-- comment node between text nodes with leading/trailing whitespace -->
label
</button>
<h3 class="ex" data-expectedlabel="heading label" data-testname="heading with text/comment/text nodes, with space">
heading
<!-- comment node between text nodes with leading/trailing whitespace -->
label
</h3>
<a class="ex" data-expectedlabel="link label" data-testname="link with text/comment/text nodes, with space" href="#">
link
<!-- comment node between text nodes with leading/trailing whitespace -->
label
</a>
<br/>
<h1>text node, with tab char</h1>
<span class="ex" data-expectedlabel="button label" data-testname="span[role=button] with text node, with tab char" role="button" tabindex="0">button label</span>
<div class="ex" data-expectedlabel="heading label" data-testname="div[role=heading] with text node, with tab char" role="heading">heading label</div>
<button class="ex" data-expectedlabel="button label" data-testname="button with text node, with tab char">button label</button>
<h3 class="ex" data-expectedlabel="heading label" data-testname="heading with text node, with tab char">heading label</h3>
<a class="ex" data-expectedlabel="link label" data-testname="link with text node, with tab char" href="#">link label</a>
<br/>
<h1>text node, with non-breaking space</h1>
<span class="ex" data-expectedlabel="button label" data-testname="span[role=button] with text node, with non-breaking space" role="button" tabindex="0">button label</span>
<div class="ex" data-expectedlabel="heading label" data-testname="div[role=heading] with text node, with non-breaking space" role="heading">heading label</div>
<button class="ex" data-expectedlabel="button label" data-testname="button with text node, with non-breaking space">button label</button>
<h3 class="ex" data-expectedlabel="heading label" data-testname="heading with text node, with non-breaking space">heading label</h3>
<a class="ex" data-expectedlabel="link label" data-testname="link with text node, with non-breaking space" href="#">link label</a>
<br/>
<h1>text node, with extra non-breaking space</h1>
<span class="ex" data-expectedlabel="button   label" data-testname="span[role=button] with text node, with extra non-breaking space" role="button" tabindex="0">button   label</span>
<div class="ex" data-expectedlabel="heading   label" data-testname="div[role=heading] with text node, with extra non-breaking space" role="heading">heading   label</div>
<button class="ex" data-expectedlabel="button   label" data-testname="button with text node, with extra non-breaking space">button   label</button>
<h3 class="ex" data-expectedlabel="heading   label" data-testname="heading with text node, with extra non-breaking space">heading   label</h3>
<a class="ex" data-expectedlabel="link   label" data-testname="link with text node, with extra non-breaking space" href="#">link   label</a>
<br/>
<h1>text node, with leading/trailing non-breaking space</h1>
<span class="ex" data-expectedlabel=" button label " data-testname="span[role=button] with text node, with leading/trailing non-breaking space" role="button" tabindex="0"> button label </span>
<div class="ex" data-expectedlabel=" heading label " data-testname="div[role=heading] with text node, with leading/trailing non-breaking space" role="heading"> heading label </div>
<button class="ex" data-expectedlabel=" button label " data-testname="button with text node, with leading/trailing non-breaking space"> button label </button>
<h3 class="ex" data-expectedlabel=" heading label " data-testname="heading with text node, with leading/trailing non-breaking space"> heading label </h3>
<a class="ex" data-expectedlabel=" link label " data-testname="link with text node, with leading/trailing non-breaking space" href="#"> link label </a>
<br/>
<h1>text node, with mixed space and non-breaking space</h1>
<span class="ex" data-expectedlabel="button   label" data-testname="span[role=button] with text node, with mixed space and non-breaking space" role="button" tabindex="0">button   label</span>
<div class="ex" data-expectedlabel="heading   label" data-testname="div[role=heading] with text node, with mixed space and non-breaking space" role="heading">heading   label</div>
<button class="ex" data-expectedlabel="button   label" data-testname="button with text node, with mixed space and non-breaking space">button   label</button>
<h3 class="ex" data-expectedlabel="heading   label" data-testname="heading with text node, with mixed space and non-breaking space">heading   label</h3>
<a class="ex" data-expectedlabel="link   label" data-testname="link with text node, with mixed space and non-breaking space" href="#">link   label</a>
<br/>
<h1>text nodes, with deeply nested space</h1>
<span class="ex" data-expectedlabel="button label" data-testname="span[role=button] with text node, with deeply nested space" role="button" tabindex="0">
button<span><span><span><span><span><span><span> </span></span></span></span></span></span></span>label
</span>
<div class="ex" data-expectedlabel="heading label" data-testname="div[role=heading] with text node, with deeply nested space" role="heading">
heading<span><span><span><span><span><span><span> </span></span></span></span></span></span></span>label
</div>
<button class="ex" data-expectedlabel="button label" data-testname="button with text node, with deeply nested space">
button<span><span><span><span><span><span><span> </span></span></span></span></span></span></span>label
</button>
<h3 class="ex" data-expectedlabel="heading label" data-testname="heading with text node, with deeply nested space">
heading<span><span><span><span><span><span><span> </span></span></span></span></span></span></span>label
</h3>
<a class="ex" data-expectedlabel="link label" data-testname="link with text node, with deeply nested space" href="#">
link<span><span><span><span><span><span><span> </span></span></span></span></span></span></span>label
</a>
<br/>
<h1>text nodes, with single line break</h1>
<span class="ex" data-expectedlabel="button label" data-testname="span[role=button] with text node, with single line break" role="button" tabindex="0">button
label</span>
<div class="ex" data-expectedlabel="heading label" data-testname="div[role=heading] with text node, with single line break" role="heading">heading
label</div>
<button class="ex" data-expectedlabel="button label" data-testname="button with text node, with single line break">button
label</button>
<h3 class="ex" data-expectedlabel="heading label" data-testname="heading with text node, with single line break">heading
label</h3>
<a class="ex" data-expectedlabel="link label" data-testname="link with text node, with single line break" href="#">link
label</a>
<br/>
<script>
AriaUtils.verifyLabelsBySelector(".ex");
</script>
</body>
</html>

View file

@ -2181,7 +2181,7 @@ void Node::build_accessibility_tree(AccessibilityTreeNode& parent)
} }
// https://www.w3.org/TR/accname-1.2/#mapping_additional_nd_te // https://www.w3.org/TR/accname-1.2/#mapping_additional_nd_te
ErrorOr<String> Node::name_or_description(NameOrDescription target, Document const& document, HashTable<UniqueNodeID>& visited_nodes) const ErrorOr<String> Node::name_or_description(NameOrDescription target, Document const& document, HashTable<UniqueNodeID>& visited_nodes, IsDescendant is_descendant) const
{ {
// The text alternative for a given element is computed as follows: // The text alternative for a given element is computed as follows:
// 1. Set the root node to the given element, the current node to the root node, and the total accumulated text to the empty string (""). If the root node's role prohibits naming, return the empty string (""). // 1. Set the root node to the given element, the current node to the root node, and the total accumulated text to the empty string (""). If the root node's role prohibits naming, return the empty string ("").
@ -2321,7 +2321,7 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
// F. Otherwise, if the current node's role allows name from content, or if the current node is referenced by aria-labelledby, aria-describedby, or is a native host language text alternative element (e.g. label in HTML), or is a descendant of a native host language text alternative element: // F. Otherwise, if the current node's role allows name from content, or if the current node is referenced by aria-labelledby, aria-describedby, or is a native host language text alternative element (e.g. label in HTML), or is a descendant of a native host language text alternative element:
auto role = element->role_or_default(); auto role = element->role_or_default();
if (role.has_value() && ARIA::allows_name_from_content(role.value())) { if ((role.has_value() && ARIA::allows_name_from_content(role.value())) || is_descendant == IsDescendant::Yes) {
// i. Set the accumulated text to the empty string. // i. Set the accumulated text to the empty string.
total_accumulated_text.clear(); total_accumulated_text.clear();
// ii. Check for CSS generated textual content associated with the current node and include it in the accumulated text. The CSS :before and :after pseudo elements [CSS2] can provide textual content for elements that have a content model. // ii. Check for CSS generated textual content associated with the current node and include it in the accumulated text. The CSS :before and :after pseudo elements [CSS2] can provide textual content for elements that have a content model.
@ -2345,7 +2345,7 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
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)); auto result = MUST(current_node->name_or_description(target, document, visited_nodes, IsDescendant::Yes));
// 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);

View file

@ -53,6 +53,11 @@ enum class FragmentSerializationMode {
Outer, Outer,
}; };
enum class IsDescendant {
No,
Yes,
};
#define ENUMERATE_STYLE_INVALIDATION_REASONS(X) \ #define ENUMERATE_STYLE_INVALIDATION_REASONS(X) \
X(AdoptedStyleSheetsList) \ X(AdoptedStyleSheetsList) \
X(CSSFontLoaded) \ X(CSSFontLoaded) \
@ -761,7 +766,7 @@ protected:
void build_accessibility_tree(AccessibilityTreeNode& parent); void build_accessibility_tree(AccessibilityTreeNode& parent);
ErrorOr<String> name_or_description(NameOrDescription, Document const&, HashTable<UniqueNodeID>&) const; ErrorOr<String> name_or_description(NameOrDescription, Document const&, HashTable<UniqueNodeID>&, IsDescendant = IsDescendant::No) const;
private: private:
void queue_tree_mutation_record(Vector<JS::Handle<Node>> added_nodes, Vector<JS::Handle<Node>> removed_nodes, Node* previous_sibling, Node* next_sibling); void queue_tree_mutation_record(Vector<JS::Handle<Node>> added_nodes, Vector<JS::Handle<Node>> removed_nodes, Node* previous_sibling, Node* next_sibling);