1
0
Fork 0
mirror of https://github.com/LadybirdBrowser/ladybird.git synced 2025-06-12 02:30:30 +09:00

LibWeb: Compute accessible names for hidden/hidden-but-referenced nodes

This change implements full support for the “A. Hidden Not Referenced”
step at https://w3c.github.io/accname/#step2A in the “Accessible Name
and Description Computation” spec — including handling all hidden nodes
that must be ignored, as well as handling hidden nodes that, for the
purposes of accessible-name computation, must not be ignored (due to
having aria-labelledby/aria-describedby references from other nodes).

Otherwise, without this change, not all cases of hidden nodes get
ignored as expected, while cases of nodes that are hidden but that have
aria-labelledby/aria-describedby references from other nodes get
unexpectedly ignored.
This commit is contained in:
sideshowbarker 2024-11-11 16:21:05 +09:00 committed by Sam Atkins
parent c00b97a1f0
commit 314e5d6bb7
Notes: github-actions[bot] 2024-11-29 12:19:31 +00:00
7 changed files with 489 additions and 21 deletions

View file

@ -1874,6 +1874,54 @@ void Element::invalidate_style_after_attribute_change(FlyString const& attribute
invalidate_style(StyleInvalidationReason::ElementAttributeChange);
}
bool Element::is_hidden() const
{
if (layout_node() == nullptr)
return true;
if (layout_node()->computed_values().visibility() == CSS::Visibility::Hidden || layout_node()->computed_values().visibility() == CSS::Visibility::Collapse || layout_node()->computed_values().content_visibility() == CSS::ContentVisibility::Hidden)
return true;
for (ParentNode const* self_or_ancestor = this; self_or_ancestor; self_or_ancestor = self_or_ancestor->parent_or_shadow_host()) {
if (self_or_ancestor->is_element() && static_cast<DOM::Element const*>(self_or_ancestor)->aria_hidden() == "true")
return true;
}
return false;
}
bool Element::has_hidden_ancestor() const
{
for (ParentNode const* self_or_ancestor = this; self_or_ancestor; self_or_ancestor = self_or_ancestor->parent_or_shadow_host()) {
if (self_or_ancestor->is_element() && static_cast<DOM::Element const*>(self_or_ancestor)->is_hidden())
return true;
}
return false;
}
bool Element::is_referenced() const
{
bool is_referenced = false;
if (id().has_value()) {
root().for_each_in_subtree_of_type<HTML::HTMLElement>([&](auto& element) {
auto aria_data = MUST(Web::ARIA::AriaData::build_data(element));
if (aria_data->aria_labelled_by_or_default().contains_slow(id().value())) {
is_referenced = true;
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
});
}
return is_referenced;
}
bool Element::has_referenced_and_hidden_ancestor() const
{
for (auto const* ancestor = parent_or_shadow_host(); ancestor; ancestor = ancestor->parent_or_shadow_host()) {
if (ancestor->is_element())
if (auto const* element = static_cast<DOM::Element const*>(ancestor); element->is_referenced() && element->is_hidden())
return true;
}
return false;
}
// https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
bool Element::exclude_from_accessibility_tree() const
{

View file

@ -348,6 +348,12 @@ public:
virtual bool include_in_accessibility_tree() const override;
bool is_hidden() const;
bool has_hidden_ancestor() const;
bool is_referenced() const;
bool has_referenced_and_hidden_ancestor() const;
void enqueue_a_custom_element_upgrade_reaction(HTML::CustomElementDefinition& custom_element_definition);
void enqueue_a_custom_element_callback_reaction(FlyString const& callback_name, GC::MarkedVector<JS::Value> arguments);

View file

@ -2216,23 +2216,26 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
if (is_element()) {
auto const* element = static_cast<DOM::Element const*>(this);
auto role = element->role_or_default();
bool is_referenced = false;
auto id = element->id();
if (id.has_value()) {
this->root().for_each_in_inclusive_subtree_of_type<HTML::HTMLElement>([&](auto& element) {
auto aria_data = MUST(Web::ARIA::AriaData::build_data(element));
if (aria_data->aria_labelled_by_or_default().contains_slow(id.value())) {
is_referenced = true;
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
});
}
// 2. Compute the text alternative for the current node:
// A. If the current node is hidden and is not directly referenced by aria-labelledby or aria-describedby, nor directly referenced by a native host language text alternative element (e.g. label in HTML) or attribute, return the empty string.
// FIXME: Check for references
if (element->aria_hidden() == "true")
return String {};
// A. Hidden Not Referenced: If the current node is hidden and is:
// i. Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that
// relation was hidden.
// ii. Nor part of a native host language text alternative element (e.g. label in HTML) or attribute traversal,
// where the root of that traversal was hidden.
// Return the empty string.
// NOTE: Nodes with CSS properties display:none, visibility:hidden, visibility:collapse or
// content-visibility:hidden: They are considered hidden, as they match the guidelines "not perceivable" and
// "explicitly hidden".
//
// AD-HOC: We dont implement this step here — because strictly implementing this would cause us to return early
// whenever encountering a node (element, actually) that “is hidden and is not directly referenced by
// aria-labelledby or aria-describedby”, without traversing down through that elements subtree to see if it has
// (1) any descendant elements that are directly referenced and/or (2) any un-hidden nodes. So we instead (in
// substep G below) traverse upward through ancestor nodes of every text node, and check in that way to do the
// equivalent of what this step seems to have been intended to do.
// https://github.com/w3c/aria/issues/2387
// B. Otherwise:
// - if computing a name, and the current node has an aria-labelledby attribute that contains at least one valid IDREF, and the current node is not already part of an aria-labelledby traversal,
// process its IDREFs in the order they occur:
@ -2260,6 +2263,7 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
// AD-HOC: The “For each IDREF” substep in the spec doesnt seem to explicitly require the following
// check for an aria-label value; but the “div group explicitly labelledby self and heading” subtest at
// https://wpt.fyi/results/accname/name/comp_labelledby.html wont pass unless we do this check.
// https://github.com/w3c/aria/issues/2388
if (target == NameOrDescription::Name && node->aria_label().has_value() && !node->aria_label()->is_empty() && !node->aria_label()->bytes_as_string_view().is_whitespace()) {
total_accumulated_text.append(' ');
total_accumulated_text.append(node->aria_label().value());
@ -2275,6 +2279,13 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
total_accumulated_text.append(result);
}
// iii. Return the accumulated text.
// AD-HOC: This substep in the spec doesnt seem to explicitly require the following check for an aria-label
// value; but the “button's hidden referenced name (visibility:hidden) with hidden aria-labelledby traversal
// falls back to aria-label” subtest at https://wpt.fyi/results/accname/name/comp_labelledby.html wont pass
// unless we do this check.
// https://github.com/w3c/aria/issues/2388
if (total_accumulated_text.string_view().is_whitespace() && target == NameOrDescription::Name && element->aria_label().has_value() && !element->aria_label()->is_empty() && !element->aria_label()->bytes_as_string_view().is_whitespace())
return element->aria_label().release_value();
return total_accumulated_text.to_string();
}
// C. Embedded Control: Otherwise, if the current node is a control embedded
@ -2297,6 +2308,7 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
// it ancestor <label> must instead be skipped and not included. The HTML-AAM spec seems to maybe
// be trying to achieve that result by expressing specific steps for each particular type of form
// control. But what all that reduces/optimizes/simplifies down to is just, “skip over self”.
// https://github.com/w3c/aria/issues/2389
if (node == this)
continue;
if (node->is_element()) {
@ -2427,8 +2439,10 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
return alt.release_value();
}
// 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:
if ((role.has_value() && ARIA::allows_name_from_content(role.value())) || is_referenced || is_descendant == IsDescendant::Yes) {
// F. Name From Content: 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:
if ((role.has_value() && ARIA::allows_name_from_content(role.value())) || element->is_referenced() || is_descendant == IsDescendant::Yes) {
// i. Set the accumulated text to the empty string.
total_accumulated_text.clear();
// ii. Name From Generated Content: Check for CSS generated textual content associated with the current node and include
@ -2500,13 +2514,24 @@ ErrorOr<String> Node::name_or_description(NameOrDescription target, Document con
}
// G. Text Node: Otherwise, if the current node is a Text Node, return its textual contents.
if (is_text()) {
// AD-HOC: The spec doesnt require ascending through the parent node and ancestor nodes of every text node we
// reach — the way were doing there. But we implement it this way because the spec algorithm as written doesnt
// appear to achieve what it seems to be intended to achieve. Specifically, the spec algorithm as written doesnt
// cause traversal through element subtrees in way thats necessary to check for descendants that are referenced by
// aria-labelledby or aria-describedby and/or un-hidden. See the comment for substep A above.
if (is_text() && (!parent_element() || (parent_element()->is_referenced() || !parent_element()->is_hidden() || !parent_element()->has_hidden_ancestor() || parent_element()->has_referenced_and_hidden_ancestor()))) {
if (layout_node() && layout_node()->is_text_node())
return verify_cast<Layout::TextNode>(layout_node())->text_for_rendering();
return text_content().value();
return text_content().release_value();
}
// TODO: H. Otherwise, if the current node is a descendant of an element whose Accessible Name or Accessible Description is being computed, and contains descendants, proceed to 2F.i.
// H. Otherwise, if the current node is a descendant of an element whose Accessible Name or Accessible Description
// is being computed, and contains descendants, proceed to 2F.i.
// AD-HOC: We dont implement this step here — because is essentially unreachable code in the spec algorithm.
// We could never get here without descending through every subtree of an element whose Accessible Name or
// Accessible Description is being computed. And in our implementation of substep F about, were anyway already
// recursively descending through all the child nodes of every element whose Accessible Name or Accessible
// Description is being computed, in a way that never leads to this substep H every being hit.
// I. Otherwise, if the current node has a Tooltip attribute, return its value.
// https://www.w3.org/TR/accname-1.2/#dfn-tooltip-attribute