Skip to content

Commit

Permalink
Merge pull request #1909 from IBMa/dev-1471
Browse files Browse the repository at this point in the history
newrule(`svg_graphics_labelled`): Create a new rule to check the accessible name of an SVG element
  • Loading branch information
ErickRenteria authored Jun 24, 2024
2 parents 12c026d + 559de3e commit 7a88182
Show file tree
Hide file tree
Showing 55 changed files with 2,815 additions and 1,111 deletions.
121 changes: 121 additions & 0 deletions accessibility-checker-engine/help-v4/en-US/svg_graphics_labelled.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<!--
/******************************************************************************
Copyright:: 2022- IBM, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*****************************************************************************/
-->
<!-- Title and messages generated at build time -->
<link rel="icon" href="https://ibm.com/able/favicon-32x32.png" type="image/png">
<link rel="icon" href="https://ibm.com/able/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="../common/help.css" />
<script type="module">
import "https://1.www.s81c.com/common/carbon/web-components/version/v1.35.0/code-snippet.min.js";
import "https://1.www.s81c.com/common/carbon/web-components/version/v1.35.0/list.min.js";
</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="../common/help.js"></script>
</head>
<body>
<div class="bx--grid toolHelp">
<div class="bx--row">
<div class="bx--col-sm-4 bx--col-md-8 bx--col-lg-16 toolHead">
<!-- Group message injected here -->
<h3 id="ruleMessage"></h3>
<!-- Severity level injected here -->
<div id="locLevel"></div>
<!-- Rule specific message injected here -->
<p id="groupLabel"></p>
</div>
</div>
<div class="bx--row">
<div class="bx--col-sm-4 bx--col-md-5 bx--col-lg-8 toolMain">
<!-- Start main panel -->
<mark-down><script type="text/plain">

### Why is this important?

An SVG component with a graphics role can contain essential information as well as multiple shapes.
When viewed together, these elements give the impression of a single image.
Providing a text alternative (accessible name and description) makes the equivalent information available through assistive technologies.
With the explicit use of roles and attributes, the author provides a clear indication that the content should convey meaning via the accessibility name and description to assistive technology users.

<!-- This is where the code snippet is injected -->
<div id="locSnippet"></div>

### What to do

Ensure the _non-decorative_ SVG element has an _accessible name_ that is not empty:
* Add an `aria-labelledby` attribute to the element that points to visible text on the page that is a meaningful label.
* **Or**, add an `aria-label` attribute to the element.
* **Or**, add a direct child `<title>` element.
* **Or**, add an `xlink:title` attribute on a link.
* **Or**, for text container elements, add the text content.
* **Or**, only if the design cannot have a visible label, use the `title` attribute to provide a label.

As appropriate, ensure the _non-decorative_ SVG element has an _accessible description_ that is not empty, in the following priority:
* Add an `aria-describedby` attribute to the element that points to visible text on the page that is a meaningful description.
* **Or**, add a direct child `<desc>` element.
* **Or**, for text container elements, add the text content.
* **Or**, add a direct child `<title>` element that provides a tooltip, when ARIA label attributes are used to provide the accessible name.
* **Or**, add a `xlink:title` attribute on a link, if not used to provide the accessible name.

Ensure the _decorative_ SVG element use `aria-hidden` or `role=’none | presentation’` to provides a clear indication that the element is not visible,
perceivable, or interactive to users.

Note: The `aria-labelledby` and `aria-describedby` properties can reference the element on which they are given,
in order to concatenate one of the other text alternatives with text from a separate element.


Example:

```
<p>How many circles are there?</p>
<svg xmlns="http://www.w3.org/2000/svg" aria-label="shapes from which to choose">
<circle role="graphics-symbol" cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow"
aria-label="1 circle"></circle>
...
</svg>
```

</script></mark-down>
<!-- End main panel -->
<!-- This is where the rule id is injected -->
<div id="ruleInfo"></div>
</div>
<div class="bx--col-sm-4 bx--col-md-3 bx--col-lg-4 toolSide">
<!-- Start side panel -->
<mark-down><script type="text/plain">

### About this requirement

* [IBM 1.1.1 Non-text Content](https://www.ibm.com/able/requirements/requirements/#1_1_1)
* [ARIA Graphics Module](https://www.w3.org/TR/graphics-aria/)
* [Accessible Name and Description Computation](https://w3c.github.io/accname/#computation-steps)
* [Graphics Accessibility API Mappings](https://w3c.github.io/graphics-aam/#mapping_role_table)
* [SVG Accessibility API Mappings](https://www.w3.org/TR/svg-aam-1.0/#mapping_role_table)


### Who does this affect?

* People using a screen reader, including blind, low vision, and neurodivergent people
* People who turn off image-loading on their web browsers
* People using text-based browsers (e.g., Lynx) or audio interfaces

</script></mark-down>
<!-- End side panel -->
</div>
</div>
</div>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -2403,6 +2403,28 @@ export class RPTUtil {
return "";
}

public static getAriaDescription(ele) {
if (!ele) return "";
let normalizedLabel = "";
if (ele.hasAttribute("aria-describedby")) {
let labelIDs = ele.getAttribute("aria-labelledby").trim().split(" ");
for (let j = 0, length = labelIDs.length; j < length; ++j) {
let labelID = labelIDs[j];
let labelNode = FragmentUtil.getById(ele, labelID);
let label = labelNode && !DOMUtil.sameNode(labelNode, ele) ? RPTUtil.getInnerText(labelNode) : "";
normalizedLabel += RPTUtil.normalizeSpacing(label).toLowerCase();
}
if (normalizedLabel.trim().length > 0)
return normalizedLabel.trim();
}
if (ele.hasAttribute("aria-description")) {
normalizedLabel = ele.getAttribute("aria-description");
if (normalizedLabel.trim().length > 0)
return normalizedLabel.trim().toLowerCase();
}
return "";
}

/**
* @param element
* @param idStr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { getDeprecatedAriaRoles, getDeprecatedAriaAttributes, getRolesUndefinedB

export let aria_accessiblename_exists: Rule = {
id: "aria_accessiblename_exists",
context: "aria:columnheader, aria:form, aria:heading, aria:rowheader, aria:table, doc-backlink, doc-biblioentry, doc-biblioref, doc-glossref, doc-noteref, doc-pagebreak",
context: "aria:columnheader, aria:form, aria:heading, aria:rowheader, aria:table, aria:graphics-document,aria:graphics-symbol, aria:img, doc-backlink, doc-biblioentry, doc-biblioref, doc-glossref, doc-noteref, doc-pagebreak",
help: {
"en-US": {
"pass": "aria_accessiblename_exists.html",
Expand Down Expand Up @@ -48,8 +48,12 @@ export let aria_accessiblename_exists: Rule = {
//skip the rule
if (VisUtil.isNodeHiddenFromAT(ruleContext)) return null;

let nodeName = ruleContext.nodeName.toLocaleLowerCase();
// svg element is handled in svg_graphics)labbelled rule
if (nodeName === 'svg') return;

// when table element with a caption as first child
if (ruleContext.nodeName.toLocaleLowerCase() === 'table'
if (nodeName === 'table'
&& ruleContext.firstElementChild && ruleContext.firstElementChild.nodeName.toLowerCase() === 'caption'
&& ruleContext.firstElementChild.textContent && ruleContext.firstElementChild.textContent.trim().length > 0)
return RulePass("pass");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,7 @@ export let aria_graphic_labelled: Rule = {
"Fail_3": "Element with \"{0}\" graphics role missing non-empty 'aria-label' or 'aria-labelledby'"
}
},
rulesets: [{
"id": ["IBM_Accessibility", "IBM_Accessibility_next", "WCAG_2_1", "WCAG_2_0", "WCAG_2_2"],
"num": ["1.1.1"], /*Change mapping to 1.1.1 from 4.1.2 */
"level": eRulePolicy.VIOLATION,
"toolkitLevel": eToolkitLevel.LEVEL_ONE
}],
rulesets: [],
act: [{
"7d6734": {
"Pass_0": "pass",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,7 @@ export let aria_img_labelled: Rule = {
"Fail_3": "Element with \"img\" role missing non-empty 'aria-label' or 'aria-labelledby'"
}
},
rulesets: [{
"id": ["IBM_Accessibility", "IBM_Accessibility_next", "WCAG_2_1", "WCAG_2_0", "WCAG_2_2"],
"num": ["1.1.1"], /*Change mapping to 1.1.1 from 4.1.2 */
"level": eRulePolicy.VIOLATION,
"toolkitLevel": eToolkitLevel.LEVEL_ONE
}],
rulesets: [],
act: ["23a2a8", {
"7d6734": {
"Pass_0": "pass",
Expand Down
100 changes: 100 additions & 0 deletions accessibility-checker-engine/src/v4/rules/svg_graphics_labelled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/******************************************************************************
Copyright:: 2022- IBM, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*****************************************************************************/

import { Rule, RuleResult, RuleFail, RuleContext, RulePass, RuleContextHierarchy } from "../api/IRule";
import { eRulePolicy, eToolkitLevel } from "../api/IRule";
import { RPTUtil } from "../../v2/checker/accessibility/util/legacy";
import { VisUtil } from "../../v2/dom/VisUtil";

export let svg_graphics_labelled: Rule = {
id: "svg_graphics_labelled",
context: "dom:svg",
help: {
"en-US": {
"group": "svg_graphics_labelled.html",
"pass": "svg_graphics_labelled.html",
"fail_acc_name": "svg_graphics_labelled.html"
}
},
messages: {
"en-US": {
"group": "A none decorative SVG element must have an accessible name",
"pass": "The SVG element has an accessible name",
"fail_acc_name": "The SVG element has no accessible name"
}
},
rulesets: [{
"id": ["IBM_Accessibility", "WCAG_2_1", "WCAG_2_0", "WCAG_2_2"],
"num": ["1.1.1"],
"level": eRulePolicy.VIOLATION,
"toolkitLevel": eToolkitLevel.LEVEL_ONE
}],
act: [{
"7d6734": {
"pass": "pass",
"fail_acc_name": "fail"
}
}],
run: (context: RuleContext, options?: {}, contextHierarchies?: RuleContextHierarchy): RuleResult | RuleResult[] => {
const ruleContext = context["dom"].node as Element;

//skip the rule
if (VisUtil.isNodeHiddenFromAT(ruleContext)) return null;

// 1. aria-labelledby or aria-label
let label = RPTUtil.getAriaLabel(ruleContext);
if (label && label.length > 0)
return RulePass("pass");

// 2. a direct child title element
let svgTitle = ruleContext.querySelector(":scope > title");
if (svgTitle && RPTUtil.hasInnerContent(svgTitle))
return RulePass("pass");

// 3. xlink:title attribute on a link
let linkTitle = ruleContext.querySelector("a");
if (linkTitle && linkTitle.hasAttribute("xlink:title") && linkTitle.getAttribute("xlink:title").trim().length > 0)
return RulePass("pass");

/** 4. for text container elements, the text content.
* note the SVG text content elements are: ‘text’, ‘textPath’ and ‘tspan’.
* svg element can be nested. One of the purposes is to to group SVG shapes together as a collection for responsive design.
*
* select text content excluded the text from the nested svg elements and their children
*/
let text = "";
ruleContext.querySelectorAll(":scope > *").forEach((element) => {
if (element.nodeName.toLowerCase() !== 'svg' && RPTUtil.hasInnerContent(element))
text += RPTUtil.getInnerText(element);
});
if (text !== '')
return RulePass("pass");

// 5. aria-describedby or aria-description
let descby = RPTUtil.getAriaDescription(ruleContext);
if (descby && descby.length > 0)
return RulePass("pass");

// 6. a direct child desc element
let desc = ruleContext.querySelector(":scope > desc");
if (desc && RPTUtil.hasInnerContent(desc))
return RulePass("pass");

// 7. title attribue that provides a tooltip
// not clear from the svg mapping spec yet, but Chrome uses svg title attribute in the accessible name, but Firefox doesn't
if (ruleContext.hasAttribute("title") && ruleContext.getAttribute("title").trim().length > 0)
return RulePass("pass");

return RuleFail("fail_acc_name")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,7 @@
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow"></circle>
</svg>

<script type="text/javascript">
UnitTest = {
ruleIds: ["aria_img_labelled"],
results: [
{
"ruleId": "aria_img_labelled",
"value": [
"INFORMATION",
"FAIL"
],
"path": {
"dom": "/html[1]/body[1]/svg[1]",
"aria": "/document[1]/generic[1]/img[1]"
},
"reasonId": "Fail_2",
"message": "Element with \"img\" role has no label or an empty label",
"messageArgs": [],
"apiArgs": [],
"category": "Accessibility"
}
]
}
</script>

</body>

</html>
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,7 @@
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<script type="text/javascript">
UnitTest = {
ruleIds: ["aria_img_labelled"],
results: [
{
"ruleId": "aria_img_labelled",
"value": [
"INFORMATION",
"FAIL"
],
"path": {
"dom": "/html[1]/body[1]/svg[1]",
"aria": "/document[1]/generic[1]/img[1]"
},
"reasonId": "Fail_2",
"message": "Element with \"img\" role has no label or an empty label",
"messageArgs": [],
"apiArgs": [],
"category": "Accessibility"
}
]
}
</script>

</body>

</html>
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,7 @@
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<script type="text/javascript">
UnitTest = {
ruleIds: ["aria_img_labelled"],
results: [
{
"ruleId": "aria_img_labelled",
"value": [
"INFORMATION",
"FAIL"
],
"path": {
"dom": "/html[1]/body[1]/svg[1]",
"aria": "/document[1]/generic[1]/img[1]"
},
"reasonId": "Fail_2",
"message": "Element with \"img\" role has no label or an empty label",
"messageArgs": [],
"apiArgs": [],
"category": "Accessibility"
}
]
}
</script>

</body>

</html>
Loading

0 comments on commit 7a88182

Please sign in to comment.