Skip to content

Commit

Permalink
feat(svg-img-alt): rule for when svg needs a title (#1953)
Browse files Browse the repository at this point in the history
* feat(svg-img-alt): rule for when svg needs a title

* chore: update from feedback
  • Loading branch information
WilcoFiers committed Jan 10, 2020
1 parent 0cd4037 commit 9491e09
Show file tree
Hide file tree
Showing 13 changed files with 471 additions and 1 deletion.
1 change: 1 addition & 0 deletions doc/rule-descriptions.md
Expand Up @@ -77,6 +77,7 @@
| scrollable-region-focusable | Elements that have scrollable content should be accessible by keyboard | Moderate | wcag2a, wcag211 | true | true | false |
| server-side-image-map | Ensures that server-side image maps are not used | Minor | cat.text-alternatives, wcag2a, wcag211, section508, section508.22.f | true | false | true |
| skip-link | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice | true | true | true |
| svg-img-alt | Ensures svg elements with an img, graphics-document or graphics-symbol role have an accessible text | Serious | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true | true | false |
| tabindex | Ensures tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | true | true | false |
| table-duplicate-name | Ensure that tables do not have the same summary and caption | Minor | cat.tables, best-practice | true | true | false |
| table-fake-caption | Ensure that tables with a caption use the <caption> element. | Serious | cat.tables, experimental, wcag2a, wcag131, section508, section508.22.g | true | true | false |
Expand Down
4 changes: 4 additions & 0 deletions lib/checks/shared/svg-non-empty-title.js
@@ -0,0 +1,4 @@
const titleNode = virtualNode.children.find(({ props }) => {
return props.nodeName === 'title';
});
return !!titleNode && titleNode.actualNode.textContent.trim() !== '';
11 changes: 11 additions & 0 deletions lib/checks/shared/svg-non-empty-title.json
@@ -0,0 +1,11 @@
{
"id": "svg-non-empty-title",
"evaluate": "svg-non-empty-title.js",
"metadata": {
"impact": "serious",
"messages": {
"pass": "element has a child that is a title",
"fail": "element has no child that is a title"
}
}
}
1 change: 1 addition & 0 deletions lib/rules/html-namespace-matches.js
@@ -0,0 +1 @@
return node.namespaceURI === 'http://www.w3.org/1999/xhtml';
3 changes: 2 additions & 1 deletion lib/rules/role-img-alt.json
@@ -1,6 +1,7 @@
{
"id": "role-img-alt",
"selector": "[role='img']:not(svg):not(img):not(area):not(input):not(object)",
"selector": "[role='img']:not(img):not(area):not(input):not(object)",
"matches": "html-namespace-matches.js",
"tags": [
"cat.text-alternatives",
"wcag2a",
Expand Down
24 changes: 24 additions & 0 deletions lib/rules/svg-img-alt.json
@@ -0,0 +1,24 @@
{
"id": "svg-img-alt",
"selector": "[role=\"img\"], [role=\"graphics-symbol\"], svg[role=\"graphics-document\"]",
"matches": "svg-namespace-matches.js",
"tags": [
"cat.text-alternatives",
"wcag2a",
"wcag111",
"section508",
"section508.22.a"
],
"metadata": {
"description": "Ensures svg elements with an img, graphics-document or graphics-symbol role have an accessible text",
"help": "svg elements with an img role have an alternative text"
},
"all": [],
"any": [
"svg-non-empty-title",
"aria-label",
"aria-labelledby",
"non-empty-title"
],
"none": []
}
1 change: 1 addition & 0 deletions lib/rules/svg-namespace-matches.js
@@ -0,0 +1 @@
return node.namespaceURI === 'http://www.w3.org/2000/svg';
56 changes: 56 additions & 0 deletions test/checks/shared/svg-non-empty-title.js
@@ -0,0 +1,56 @@
describe('svg-non-empty-title tests', function() {
'use strict';

var fixture = document.getElementById('fixture');
var checkSetup = axe.testUtils.checkSetup;
var check = checks['svg-non-empty-title'];

afterEach(function() {
fixture.innerHTML = '';
});

it('returns true if the element has a `title` child', function() {
var checkArgs = checkSetup(
'<svg id="target"><title>Time II: Party</title></svg>'
);
assert.isTrue(check.evaluate.apply(null, checkArgs));
});

it('returns true if the `title` child has text nested in another element', function() {
var checkArgs = checkSetup(
'<svg id="target"><title><g>Time II: Party</g></title></svg>'
);
assert.isTrue(check.evaluate.apply(null, checkArgs));
});

it('returns false if the element has no `title` child', function() {
var checkArgs = checkSetup('<svg id="target"></svg>');
assert.isFalse(check.evaluate.apply(null, checkArgs));
});

it('returns false if the `title` child is empty', function() {
var checkArgs = checkSetup('<svg id="target"><title></title></svg>');
assert.isFalse(check.evaluate.apply(null, checkArgs));
});

it('returns false if the `title` is a grandchild', function() {
var checkArgs = checkSetup(
'<svg id="target"><circle><title>Time II: Party</title></circle></svg>'
);
assert.isFalse(check.evaluate.apply(null, checkArgs));
});

it('returns false if the `title` child has only whitespace', function() {
var checkArgs = checkSetup(
'<svg id="target"><title> \t\r\n </title></svg>'
);
assert.isFalse(check.evaluate.apply(null, checkArgs));
});

it('returns false if there are multiple titles, and the first is empty', function() {
var checkArgs = checkSetup(
'<svg id="target"><title></title><title>Time II: Party</title></svg>'
);
assert.isFalse(check.evaluate.apply(null, checkArgs));
});
});
11 changes: 11 additions & 0 deletions test/integration/rules/role-img-alt/role-img-alt.html
Expand Up @@ -11,3 +11,14 @@
<div role="img" alt="blah" id="violation3"></div>
<div role="img" aria-labelledby="no-match" id="violation4"></div>
<div role="img" title="" id="violation5"></div>

<svg
id="inapplicable1"
xmlns="http://www.w3.org/2000/svg"
role="img"
width="100"
height="100"
>
<title>I am a circle</title>
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>
236 changes: 236 additions & 0 deletions test/integration/rules/svg-img-alt/svg-img-alt.html
@@ -0,0 +1,236 @@
<h2>Passed</h2>

<svg
id="pass1"
xmlns="http://www.w3.org/2000/svg"
role="img"
width="100"
height="100"
>
<title>I am a circle</title>
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<svg
id="pass2"
xmlns="http://www.w3.org/2000/svg"
role="img"
width="100"
height="100"
aria-label="I am a circle"
>
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<svg
id="pass3"
xmlns="http://www.w3.org/2000/svg"
role="img"
width="100"
height="100"
title="I am a circle"
>
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<span id="circle-label1">I am a circle</span>
<svg
id="pass4"
xmlns="http://www.w3.org/2000/svg"
role="img"
width="100"
height="100"
aria-labelledby="circle-label1"
>
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<svg
xmlns="http://www.w3.org/2000/svg"
id="pass5"
role="graphics-document"
width="100"
height="100"
>
<title>I am a circle</title>
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<svg xmlns="https://www.w3.org/2000/svg">
<circle
id="pass6"
role="graphics-symbol"
cx="50"
cy="50"
r="40"
fill="yellow"
aria-label="I am a circle"
></circle>
</svg>

<svg xmlns="https://www.w3.org/2000/svg">
<circle
id="pass7"
role="graphics-symbol"
cx="50"
cy="50"
r="40"
fill="yellow"
aria-labelledby="circle-label1"
></circle>
</svg>

<svg xmlns="https://www.w3.org/2000/svg">
<circle
id="pass8"
role="graphics-symbol"
cx="50"
cy="50"
r="40"
fill="yellow"
title="I am a circle"
></circle>
</svg>

<svg xmlns="https://www.w3.org/2000/svg">
<circle
id="pass9"
role="graphics-symbol"
cx="50"
cy="50"
r="40"
fill="yellow"
>
<title>I am a circle</title>
</circle>
</svg>

<svg xmlns="https://www.w3.org/2000/svg">
<circle id="pass10" role="img" cx="50" cy="50" r="40" fill="yellow">
<title>I am a circle</title>
</circle>
</svg>

<h2>Failed</h2>

<svg xmlns="http://www.w3.org/2000/svg" role="img" id="violation1">
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<svg xmlns="http://www.w3.org/2000/svg" role="img" id="violation2">
<title></title>
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<svg
xmlns="http://www.w3.org/2000/svg"
role="img"
id="violation3"
aria-label=" "
>
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<svg
xmlns="http://www.w3.org/2000/svg"
role="img"
id="violation4"
aria-labelledby="not-an-id"
>
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<svg
id="violation5"
xmlns="http://www.w3.org/2000/svg"
role="img"
width="100"
height="100"
>
<text>I am a circle</text>
</svg>

<svg
id="violation6"
xmlns="http://www.w3.org/2000/svg"
role="img"
width="100"
height="100"
>
<circle cx="50" cy="50" r="40" fill="yellow">
<!-- Title must be a child -->
<title>I am a circle</title>
</circle>
</svg>

<svg
id="violation7"
xmlns="http://www.w3.org/2000/svg"
role="img"
width="100"
height="100"
>
<metadata>
<!-- Title must be a child -->
<title>I am a circle</title>
</metadata>
</svg>

<svg
id="violation8"
xmlns="http://www.w3.org/2000/svg"
role="img"
width="100"
height="100"
>
<desc>I am a circle</desc>
</svg>

<svg
id="violation9"
xmlns="http://www.w3.org/2000/svg"
role="graphics-document"
width="100"
height="100"
></svg>

<svg xmlns="https://www.w3.org/2000/svg">
<circle
id="violation10"
role="img"
cx="50"
cy="50"
r="40"
fill="yellow"
></circle>
</svg>

<h2>Inapplicable</h2>

<svg xmlns="http://www.w3.org/2000/svg">
<circle id="inapplicable1" cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<svg
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-hidden="true"
id="inapplicable2"
>
<circle cx="50" cy="50" r="40" fill="yellow"></circle>
</svg>

<svg xmlns="http://www.w3.org/2000/svg">
<circle
role="graphics-object"
cx="50"
cy="50"
r="40"
fill="yellow"
id="inapplicable3"
></circle>
</svg>

<div id="inapplicable4" role="img">
<h1>Hello world</h1>
</div>

0 comments on commit 9491e09

Please sign in to comment.