Skip to content

Commit 229e59f

Browse files
committed
feat(aria): support role=image as an alias for role=img
Add the ARIA 1.3 image role (w3c/aria#1370) as an alias of img: it is a valid role sharing img's definition (spread from a common object) and is allowed wherever img is allowed in the html-elms spec (embed, iframe, svg). The role-img-alt selector matches both roles via :is(). This avoids a synonym-resolution mechanism, so the role the author used is preserved when reporting an unallowed role. Closes #4656
1 parent 19aacea commit 229e59f

12 files changed

Lines changed: 103 additions & 13 deletions

File tree

lib/rules/role-img-alt.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "role-img-alt",
33
"impact": "serious",
4-
"selector": "[role='img']:not(img, area, input, object)",
4+
"selector": ":is([role='img'], [role='image']):not(img, area, input, object)",
55
"matches": "html-namespace-matches",
66
"tags": [
77
"cat.text-alternatives",

lib/standards/aria-roles.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@
1616
1717
globalRoleAttrs.forEach(li => li.style.display = 'none');
1818
*/
19+
// Shared definition for the img role and its ARIA 1.3 alias image, so the
20+
// two entries stay in sync.
21+
const imgRole = {
22+
type: 'structure',
23+
// Spec difference: Aria-expanded removed in 1.2
24+
allowedAttrs: ['aria-expanded'],
25+
superclassRole: ['section'],
26+
accessibleNameRequired: true,
27+
childrenPresentational: true
28+
};
29+
1930
const ariaRoles = {
2031
alert: {
2132
type: 'structure',
@@ -267,13 +278,13 @@ const ariaRoles = {
267278
accessibleNameRequired: false,
268279
nameFromContent: true
269280
},
281+
// image is an ARIA 1.3 alias of img (w3c/aria#1370) with an identical
282+
// mapping. Spread the shared definition so the two entries stay in sync.
283+
image: {
284+
...imgRole
285+
},
270286
img: {
271-
type: 'structure',
272-
// Spec difference: Aria-expanded removed in 1.2
273-
allowedAttrs: ['aria-expanded'],
274-
superclassRole: ['section'],
275-
accessibleNameRequired: true,
276-
childrenPresentational: true
287+
...imgRole
277288
},
278289
input: {
279290
type: 'abstract',

lib/standards/html-elms.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,14 @@ const htmlElms = {
235235
},
236236
embed: {
237237
contentTypes: ['interactive', 'embedded', 'phrasing', 'flow'],
238-
allowedRoles: ['application', 'document', 'img', 'presentation', 'none'],
238+
allowedRoles: [
239+
'application',
240+
'document',
241+
'img',
242+
'image',
243+
'presentation',
244+
'none'
245+
],
239246
chromiumRole: 'EmbeddedObject'
240247
},
241248
fieldset: {
@@ -340,7 +347,14 @@ const htmlElms = {
340347
},
341348
iframe: {
342349
contentTypes: ['interactive', 'embedded', 'phrasing', 'flow'],
343-
allowedRoles: ['application', 'document', 'img', 'none', 'presentation'],
350+
allowedRoles: [
351+
'application',
352+
'document',
353+
'img',
354+
'image',
355+
'none',
356+
'presentation'
357+
],
344358
chromiumRole: 'Iframe'
345359
},
346360
img: {
@@ -666,7 +680,7 @@ const htmlElms = {
666680
contentTypes: ['embedded', 'phrasing', 'flow']
667681
}
668682
},
669-
allowedRoles: ['application', 'document', 'img'],
683+
allowedRoles: ['application', 'document', 'img', 'image'],
670684
chromiumRole: 'PluginObject'
671685
},
672686
ol: {

test/commons/aria/get-element-unallowed-roles.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,26 @@ describe('aria.getElementUnallowedRoles', () => {
214214
assert.isNotEmpty(actual);
215215
assert.include(actual, 'application');
216216
});
217+
218+
it('returns empty for role=image on an svg elm', () => {
219+
const node = document.createElement('svg');
220+
node.setAttribute('role', 'image');
221+
flatTreeSetup(node);
222+
assert.isEmpty(getElementUnallowedRoles(node));
223+
});
224+
225+
it('returns empty for role=image on an embed elm', () => {
226+
const node = document.createElement('embed');
227+
node.setAttribute('role', 'image');
228+
flatTreeSetup(node);
229+
assert.isEmpty(getElementUnallowedRoles(node));
230+
});
231+
232+
it('reports role=image (as authored) when it is not allowed on the element', () => {
233+
const node = document.createElement('hr');
234+
node.setAttribute('role', 'image');
235+
flatTreeSetup(node);
236+
const actual = getElementUnallowedRoles(node);
237+
assert.include(actual, 'image');
238+
});
217239
});

test/commons/aria/is-valid-role.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,8 @@ describe('aria.isValidRole', () => {
2727
axe.commons.aria.isValidRole('input', { allowAbstract: true })
2828
);
2929
});
30+
31+
it('returns true for the synonym role `image`', () => {
32+
assert.isTrue(axe.commons.aria.isValidRole('image'));
33+
});
3034
});

test/integration/rules/aria-allowed-role/aria-allowed-role.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ <h1 id="pass-h1-role-doc-subtitle" role="doc-subtitle"></h1>
3838
<header id="pass-header-valid-role" role="group"></header>
3939
<footer id="pass-footer-valid-role" role="group"></footer>
4040
<embed id="pass-embed-valid-role" role="img" />
41+
<embed id="pass-embed-synonym-role-image" role="image" />
4142
<input type="text" role="textbox" id="pass-input-text-redundant-role" />
4243
<input type="text" role="textbox combobox" id="pass-input-multiple-roles" />
4344
<input

test/integration/rules/aria-allowed-role/aria-allowed-role.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
["#pass-header-valid-role"],
3232
["#pass-footer-valid-role"],
3333
["#pass-embed-valid-role"],
34+
["#pass-embed-synonym-role-image"],
3435
["#pass-div-has-any-role"],
3536
["#pass-input-text-redundant-role"],
3637
["#pass-input-multiple-roles"],

test/integration/rules/aria-roles/aria-roles.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<div role="group" id="pass20">ok</div>
2121
<div role="heading" id="pass21">ok</div>
2222
<div role="img" id="pass22">ok</div>
23+
<div role="image" id="pass121">ok</div>
2324
<div role="link" id="pass23">ok</div>
2425
<div role="list" id="pass24">ok</div>
2526
<div role="listbox" id="pass25">ok</div>

test/integration/rules/aria-roles/aria-roles.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
["#pass117"],
133133
["#pass118"],
134134
["#pass119"],
135-
["#pass120"]
135+
["#pass120"],
136+
["#pass121"]
136137
]
137138
}

test/integration/rules/role-img-alt/role-img-alt.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
<div role="img" aria-labelledby="match" id="pass2"></div>
66
<div role="img" aria-labelledby="hidden-match" id="pass3"></div>
77
<div role="img" title="title" id="pass4"></div>
8+
<div role="image" aria-label="blah" id="pass5"></div>
89

910
<div role="img" id="violation1"></div>
1011
<div role="img" aria-label="" id="violation2"></div>
1112
<div role="img" alt="blah" id="violation3"></div>
1213
<div role="img" aria-labelledby="no-match" id="violation4"></div>
1314
<div role="img" title="" id="violation5"></div>
15+
<div role="image" id="violation6"></div>
1416

1517
<svg
1618
id="inapplicable1"

0 commit comments

Comments
 (0)