Skip to content

Commit 3a012a1

Browse files
mehm8128WilcoFiers
andauthored
fix(label-content-name-mismatch): match visible text with aria-label and exclude invisible text (#5096)
Using `visibleVirtual` instead of `subtreeText`, I resolved issue. Because if I simple replace it and use `isIconLigature` existing two tests failed (`'returns true when visible text excluding ligature icon is part of accessible name'` and `'returns true when text contains <br/>'`), I changed `visible-virtual.js`, but I'm not sure this is proper changes (this is the first PR for me to axe-core). Closes: #5063 --------- Co-authored-by: Wilco Fiers <[email protected]>
1 parent d423974 commit 3a012a1

6 files changed

Lines changed: 165 additions & 20 deletions

File tree

lib/checks/label/label-content-name-mismatch-evaluate.js

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {
22
accessibleText,
33
isHumanInterpretable,
4-
subtreeText,
4+
removeUnicode,
55
sanitize,
6-
removeUnicode
6+
visibleVirtual
77
} from '../../commons/text';
88

99
/**
@@ -42,14 +42,11 @@ function labelContentNameMismatchEvaluate(node, options, virtualNode) {
4242
const occurrenceThreshold =
4343
options?.occurrenceThreshold ?? options?.occuranceThreshold;
4444
const accText = accessibleText(node).toLowerCase();
45-
const visibleText = sanitize(
46-
subtreeText(virtualNode, {
47-
subtreeDescendant: true,
48-
ignoreIconLigature: true,
49-
pixelThreshold,
50-
occurrenceThreshold
51-
})
52-
).toLowerCase();
45+
const visibleText = visibleVirtual(virtualNode, false, false, {
46+
ignoreIconLigature: true,
47+
pixelThreshold,
48+
occurrenceThreshold
49+
}).toLowerCase();
5350

5451
if (!visibleText) {
5552
return true;

lib/commons/text/visible-virtual.js

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import sanitize from './sanitize';
1+
import { nodeLookup } from '../../core/utils';
22
import isVisibleOnScreen from '../dom/is-visible-on-screen';
33
import isVisibleToScreenReaders from '../dom/is-visible-to-screenreader';
4-
import { nodeLookup } from '../../core/utils';
4+
import isIconLigature from './is-icon-ligature';
5+
import sanitize from './sanitize';
56

67
/**
78
* Returns the visible text of the virtual node
@@ -15,9 +16,13 @@ import { nodeLookup } from '../../core/utils';
1516
* @param {Boolean} screenReader When provided, will evaluate visibility from the perspective of a screen reader
1617
* @param {Boolean} noRecursing When False, the result will contain text from the element and it's children.
1718
* When True, the result will only contain text from the element
19+
* @param {Object} [options]
20+
* @param {Boolean} [options.ignoreIconLigature] When true, icon ligature text nodes are excluded
21+
* @param {number} [options.pixelThreshold] Pixel threshold for icon ligature detection
22+
* @param {number} [options.occurrenceThreshold] Occurrence threshold for icon ligature detection
1823
* @return {String}
1924
*/
20-
function visibleVirtual(element, screenReader, noRecursing) {
25+
function visibleVirtual(element, screenReader, noRecursing, options = {}) {
2126
const { vNode } = nodeLookup(element);
2227
const visibleMethod = screenReader
2328
? isVisibleToScreenReaders
@@ -28,16 +33,29 @@ function visibleVirtual(element, screenReader, noRecursing) {
2833
const visible =
2934
!element.actualNode || (element.actualNode && visibleMethod(element));
3035

36+
const { ignoreIconLigature, pixelThreshold, occurrenceThreshold } = options;
37+
3138
const result = vNode.children
3239
.map(child => {
33-
const { nodeType, nodeValue } = child.props;
40+
const { nodeType, nodeValue, nodeName } = child.props;
3441
if (nodeType === 3) {
3542
// filter on text nodes
36-
if (nodeValue && visible) {
37-
return nodeValue;
43+
if (!nodeValue || !visible) {
44+
return '';
45+
}
46+
if (
47+
ignoreIconLigature &&
48+
isIconLigature(child, pixelThreshold, occurrenceThreshold)
49+
) {
50+
return '';
3851
}
39-
} else if (!noRecursing) {
40-
return visibleVirtual(child, screenReader);
52+
return nodeValue;
53+
}
54+
if (nodeName === 'br') {
55+
return ' ';
56+
}
57+
if (!noRecursing) {
58+
return visibleVirtual(child, screenReader, false, options);
4159
}
4260
})
4361
.join('');

test/checks/label/label-content-name-mismatch.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,42 @@ describe('label-content-name-mismatch tests', () => {
186186
const actual = check.evaluate(vNode.actualNode, options, vNode);
187187
assert.isTrue(actual);
188188
});
189+
190+
it('returns true when aria-label and visible text match even though there is an image with alt text', function () {
191+
var vNode = queryFixture(
192+
'<button id="target" aria-label="button label"><img alt="button icon" src="button.png" />button label</button>'
193+
);
194+
var actual = check.evaluate(vNode.actualNode, options, vNode);
195+
assert.isTrue(actual);
196+
});
197+
198+
it('returns false when aria-label and visible text do not match even though there is an image with alt text', function () {
199+
var vNode = queryFixture(
200+
'<button id="target" aria-label="button label"><img alt="button icon" src="button.png" />this is a button label</button>'
201+
);
202+
var actual = check.evaluate(vNode.actualNode, options, vNode);
203+
assert.isFalse(actual);
204+
});
205+
206+
(fontApiSupport ? it : it.skip)(
207+
'returns true when aria-label and visible text match even though there is a ligature icon',
208+
function () {
209+
var vNode = queryFixture(
210+
'<button id="target" aria-label="button label"><span style="font-family: \'Material Icons\'">delete</span>button label</button>'
211+
);
212+
var actual = check.evaluate(vNode.actualNode, options, vNode);
213+
assert.isTrue(actual);
214+
}
215+
);
216+
217+
(fontApiSupport ? it : it.skip)(
218+
'returns false when aria-label and visible text do not match even though there is a ligature icon',
219+
function () {
220+
var vNode = queryFixture(
221+
'<button id="target" aria-label="button label"><span style="font-family: \'Material Icons\'">delete</span>this is a button label</button>'
222+
);
223+
var actual = check.evaluate(vNode.actualNode, options, vNode);
224+
assert.isFalse(actual);
225+
}
226+
);
189227
});

test/commons/text/visible-virtual.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@ describe('text.visible', () => {
33

44
const fixture = document.getElementById('fixture');
55
const visibleVirtual = axe.commons.text.visibleVirtual;
6+
const fontApiSupport = !!document.fonts;
7+
8+
before(done => {
9+
if (!fontApiSupport) {
10+
done();
11+
return;
12+
}
13+
const materialFont = new FontFace(
14+
'Material Icons',
15+
'url(https://fonts.gstatic.com/s/materialicons/v48/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2)'
16+
);
17+
materialFont.load().then(() => {
18+
document.fonts.add(materialFont);
19+
done();
20+
});
21+
});
622

723
afterEach(() => {
824
document.getElementById('fixture').innerHTML = '';
@@ -103,6 +119,12 @@ describe('text.visible', () => {
103119
const tree = axe.utils.getFlattenedTree(fixture.firstChild);
104120
assert.equal(visibleVirtual(tree[0]), 'Stuffhello');
105121
});
122+
123+
it('should treat <br> elements as a space', () => {
124+
fixture.innerHTML = '<button>button<br>label</button>';
125+
const tree = axe.utils.getFlattenedTree(fixture);
126+
assert.equal(visibleVirtual(tree[0]), 'button label');
127+
});
106128
});
107129

108130
describe('screen reader', () => {
@@ -175,4 +197,40 @@ describe('text.visible', () => {
175197
assert.equal(visibleVirtual(tree[0], true), 'Hello');
176198
});
177199
});
200+
201+
describe('options', () => {
202+
(fontApiSupport ? it : it.skip)(
203+
'should exclude icon ligature text when ignoreIconLigature is true',
204+
() => {
205+
fixture.innerHTML =
206+
'<button>next page <span style="font-family: \'Material Icons\'">delete</span></button>';
207+
const tree = axe.utils.getFlattenedTree(fixture);
208+
assert.equal(
209+
visibleVirtual(tree[0], false, false, {
210+
ignoreIconLigature: true,
211+
pixelThreshold: 0.1,
212+
occurrenceThreshold: 3
213+
}),
214+
'next page'
215+
);
216+
}
217+
);
218+
219+
(fontApiSupport ? it : it.skip)(
220+
'should not exclude icon ligature text when ignoreIconLigature is false',
221+
() => {
222+
fixture.innerHTML =
223+
'<button>next page <span style="font-family: \'Material Icons\'">delete</span></button>';
224+
const tree = axe.utils.getFlattenedTree(fixture);
225+
assert.equal(
226+
visibleVirtual(tree[0], false, false, {
227+
ignoreIconLigature: false,
228+
pixelThreshold: 0.1,
229+
occurrenceThreshold: 3
230+
}),
231+
'next page delete'
232+
);
233+
}
234+
);
235+
});
178236
});

test/integration/rules/label-content-name-mismatch/label-content-name-mismatch.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
Next Page
1616
</button>
1717

18+
<a id="pass8" aria-label="Deque" href="#">
19+
<img alt="Logo" width="100" src="logo.svg" />
20+
<span>Deque</span>
21+
</a>
22+
<a id="pass9" href="#" aria-label="Hello world">
23+
<svg width="14" height="14" viewBox="0 0 100 100">
24+
<title>Circle</title>
25+
<circle cx="50" cy="50" r="50" />
26+
</svg>
27+
Hello World
28+
</a>
29+
1830
<!-- Fail -->
1931
<div id="fail1" role="link" aria-label="OK">Next</div>
2032
<button id="fail2" name="link" aria-label="the full">The full label</button>
@@ -23,6 +35,18 @@
2335
<div id="labelForFail5">123</div>
2436
<div role="button" id="fail5" aria-labelledby="labelForFail5">some content</div>
2537

38+
<a id="fail6" aria-label="Deque" href="#">
39+
<img alt="Logo" width="100" src="logo.svg" />
40+
<span>Deque Systems</span>
41+
</a>
42+
<a id="fail7" href="#" aria-label="Hello world">
43+
<svg width="14" height="14" viewBox="0 0 100 100">
44+
<title>Circle</title>
45+
<circle cx="50" cy="50" r="50" />
46+
</svg>
47+
Hello Deque Systems
48+
</a>
49+
2650
<!-- incomplete -->
2751
<button id="incomplete1" aria-label="comet">☄️</button>
2852
<button id="incomplete2" aria-label="☄️">shooting star</button>

test/integration/rules/label-content-name-mismatch/label-content-name-mismatch.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
{
22
"description": "label-content-name-mismatch tests",
33
"rule": "label-content-name-mismatch",
4-
"violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"], ["#fail5"]],
4+
"violations": [
5+
["#fail1"],
6+
["#fail2"],
7+
["#fail3"],
8+
["#fail4"],
9+
["#fail5"],
10+
["#fail6"],
11+
["#fail7"]
12+
],
513
"passes": [
614
["#pass1"],
715
["#pass2"],
816
["#pass3"],
917
["#pass4"],
1018
["#pass5"],
1119
["#pass6"],
12-
["#pass7"]
20+
["#pass7"],
21+
["#pass8"],
22+
["#pass9"]
1323
],
1424
"incomplete": [
1525
["#incomplete1"],

0 commit comments

Comments
 (0)