Skip to content

Commit

Permalink
LibWeb: Compute default ARIA roles for SVG elements
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sideshowbarker committed Dec 25, 2024
1 parent 910ff8b commit b0790e2
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 0 deletions.
35 changes: 35 additions & 0 deletions Libraries/LibWeb/SVG/SVGElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -28,6 +30,39 @@ 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);
Expand Down
3 changes: 3 additions & 0 deletions Libraries/LibWeb/SVG/SVGElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class SVGElement
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);

Expand Down
9 changes: 9 additions & 0 deletions Tests/LibWeb/Text/expected/wpt-import/svg-aam/role/roles.txt
Original file line number Diff line number Diff line change
@@ -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
107 changes: 107 additions & 0 deletions Tests/LibWeb/Text/input/wpt-import/svg-aam/role/roles.html
Original file line number Diff line number Diff line change
@@ -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>

0 comments on commit b0790e2

Please sign in to comment.