LibWeb: Compute default ARIA roles for SVG elements

This change adds support for computing default ARIA roles for (selected)
SVG elements, per the requirements in the SVG Accessibility API Mappings
spec at https://w3c.github.io/svg-aam/#mapping_role_table.

This only computes roles for the elements which are covered in the WPT
test at https://wpt.fyi/results/svg-aam/role/roles.html — that is, the
“a”, “g”, and “image” elements.
This commit is contained in:
sideshowbarker 2024-12-24 17:16:10 +09:00 committed by Tim Ledbetter
commit f2ac591614
Notes: github-actions[bot] 2024-12-25 10:59:46 +00:00
4 changed files with 153 additions and 0 deletions

View file

@ -11,8 +11,10 @@
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/SVG/SVGDescElement.h>
#include <LibWeb/SVG/SVGElement.h>
#include <LibWeb/SVG/SVGSVGElement.h>
#include <LibWeb/SVG/SVGTitleElement.h>
#include <LibWeb/SVG/SVGUseElement.h>
namespace Web::SVG {
@ -28,6 +30,38 @@ void SVGElement::initialize(JS::Realm& realm)
WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGElement);
}
bool SVGElement::should_include_in_accessibility_tree() const
{
bool has_title_or_desc = false;
auto role = role_from_role_attribute_value();
for_each_child_of_type<SVGElement>([&has_title_or_desc](auto& child) {
if ((is<SVGTitleElement>(child) || is<SVGDescElement>(child)) && !child.text_content()->trim_ascii_whitespace().value().is_empty()) {
has_title_or_desc = true;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
// https://w3c.github.io/svg-aam/#include_elements
// TODO: Add support for the SVG tabindex attribute, and include a check for it here.
return has_title_or_desc
|| (aria_label().has_value() && !aria_label().value().trim_ascii_whitespace().value().is_empty())
|| (aria_labelled_by().has_value() && !aria_labelled_by().value().trim_ascii_whitespace().value().is_empty())
|| (aria_described_by().has_value() && !aria_described_by().value().trim_ascii_whitespace().value().is_empty())
|| (role.has_value() && ARIA::is_abstract_role(role.value()) && role != ARIA::Role::none && role != ARIA::Role::presentation);
}
Optional<ARIA::Role> SVGElement::default_role() const
{
// https://w3c.github.io/svg-aam/#mapping_role_table
if (local_name() == TagNames::a && (has_attribute(SVG::AttributeNames::href) || has_attribute(AttributeNames::xlink_href)))
return ARIA::Role::link;
if (local_name() == TagNames::g && should_include_in_accessibility_tree())
return ARIA::Role::group;
if (local_name() == TagNames::image && should_include_in_accessibility_tree())
return ARIA::Role::image;
return {};
}
void SVGElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);

View file

@ -25,6 +25,9 @@ public:
GC::Ref<SVGAnimatedString> class_name();
GC::Ptr<SVGSVGElement> owner_svg_element();
bool should_include_in_accessibility_tree() const;
virtual Optional<ARIA::Role> default_role() const override;
protected:
SVGElement(DOM::Document&, DOM::QualifiedName);

View file

@ -0,0 +1,9 @@
Harness status: OK
Found 4 tests
4 Pass
Pass el-a[href]
Pass el-a[xlink:href]
Pass el-g
Pass el-image

View file

@ -0,0 +1,107 @@
<!doctype html>
<html>
<head>
<title>SVG-AAM Role Verification Tests</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 mappings defined in <a href="https://w3c.github.io/svg-aam/#mapping_role_table">SVG-AAM: 6.2 Element Mapping</a>.<p>
<h2>Simple Elements With aria-label to Ensure Tree Inclusion</h2>
<svg>
<!-- Some elements skipped: never-rendered elements can return unpredicable/undefined/unspecified values for computedrole. -->
<a href="#" data-testname="el-a[href]" data-expectedrole="link" aria-label="label" class="ex">x</a>
<a xlink:href="#" data-testname="el-a[xlink:href]" data-expectedrole="link" aria-label="label" class="ex">x</a>
<!-- skipped: animate -->
<!-- skipped: animateMotion -->
<!-- skipped: animateTransform -->
<!-- blocked: audio -> https://github.com/w3c/html-aam/issues/511 -->
<!-- todo: canvas -> follow HTML -->
<!-- blocked: circle -> https://github.com/w3c/svg-aam/issues/24 -->
<!-- n/a: clipPath -->
<!-- n/a: cursor -->
<!-- n/a: defs -->
<!-- n/a: desc -->
<!-- n/a: discard -->
<!-- blocked: ellipse -> https://github.com/w3c/svg-aam/issues/24 -->
<!-- n/a: feBlend -->
<!-- n/a: feColorMatrix -->
<!-- n/a: feComponentTransfer -->
<!-- n/a: feComposite -->
<!-- n/a: feConvolveMatrix -->
<!-- n/a: feDiffuseLighting -->
<!-- n/a: feDisplacementMap -->
<!-- n/a: feDistantLight -->
<!-- n/a: feDropShadow -->
<!-- n/a: feFlood -->
<!-- n/a: feFuncA -->
<!-- n/a: feFuncB -->
<!-- n/a: feFuncG -->
<!-- n/a: feFuncR -->
<!-- n/a: feGaussianBlur -->
<!-- n/a: feImage -->
<!-- n/a: feMerge -->
<!-- n/a: feMergeNode -->
<!-- n/a: feMorphology -->
<!-- n/a: feOffset -->
<!-- n/a: fePointLight -->
<!-- n/a: feSpecularLighting -->
<!-- n/a: feSpotLight -->
<!-- n/a: feTile -->
<!-- n/a: feTurbulence -->
<!-- n/a: filter -->
<!-- todo: foreignObject (spec says `group` role if rendered and labeled) -->
<g fill="white" stroke="green" stroke-width="2" data-testname="el-g" data-expectedrole="group" aria-label="label" class="ex">
<circle cx="40" cy="40" r="25" />
</g>
<!-- n/a: hatch -->
<!-- n/a: hatchPath -->
<!-- todo: iframe -> follow HTML -->
<image data-testname="el-image" data-expectedrole="image" aria-label="label" class="ex" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="></image>
<!-- blocked: line -> https://github.com/w3c/svg-aam/issues/24 -->
<!-- n/a: linearGradient -->
<!-- n/a: marker -->
<!-- n/a: mask -->
<!-- todo: mesh (spec says `image` role if rendered and labeled) -->
<!-- n/a: meshPatch -->
<!-- n/a: meshRow -->
<!-- n/a: metadata -->
<!-- n/a: mpath -->
<!-- blocked: path -> https://github.com/w3c/svg-aam/issues/24 -->
<!-- n/a: pattern -->
<!-- blocked: polygon -> https://github.com/w3c/svg-aam/issues/24 -->
<!-- blocked: polyline -> https://github.com/w3c/svg-aam/issues/24 -->
<!-- n/a: radialGradient -->
<!-- blocked: rect -> https://github.com/w3c/svg-aam/issues/24 -->
<!-- n/a: script -->
<!-- n/a: set -->
<!-- n/a: solidColor -->
<!-- todo: source -> follow HTML -->
<!-- n/a: stop -->
<!-- n/a: style -->
<!-- blocked: svg -> https://github.com/w3c/svg-aam/issues/18 -->
<!-- n/a: switch -->
<!-- blocked: symbol -> https://github.com/w3c/svg-aam/issues/24 -->
<!-- blocked: text -> https://github.com/w3c/svg-aam/issues/33 -->
<!-- blocked: textPath -> https://w3c.github.io/svg-aam/#textpath-tspan-mappings-issue-->
<!-- n/a: title -->
<!-- todo: track -> follow HTML -->
<!-- blocked: tspan -> https://w3c.github.io/svg-aam/#textpath-tspan-mappings-issue -->
<!-- blocked: use -> https://github.com/w3c/svg-aam/issues/24 -->
<!-- todo: video -> follow HTML -->
<!-- n/a: view -->
</svg>
<script>
AriaUtils.verifyRolesBySelector(".ex");
</script>
</body>
</html>