Skip to content

Commit 0c1d701

Browse files
committed
fix(template-no-invalid-role): source valid roles from aria-query; support DPUB-/Graphics-ARIA and role-fallback lists
Two related fixes, shared rewrite. 1. Replace the hand-maintained VALID_ROLES (~90 WAI-ARIA 1.2 tokens) with a derived list from aria-query (concrete — non-abstract — role keys), plus a small ARIA 1.3 draft-role allowlist that aria-query doesn't yet ship. Effect: DPUB-ARIA roles (doc-abstract, doc-chapter, …) and Graphics-ARIA roles (graphics-document, graphics-object, graphics-symbol) are no longer flagged as invalid. 2. Split the role value on whitespace before validating. A role attribute is a list of tokens per ARIA 1.2 §5.4 (role fallback). Each token must individually be valid. Effect: role="tabpanel row", role="doc-appendix doc-bibliography", and role="graphics-document document" now pass; role="tabpanel row foobar" flags the first invalid token ("foobar") instead of rejecting the whole string as one opaque role name. Error message now names the specific offending token. Three existing invalid tests updated accordingly (previously expected the whole string; now the specific token). Ten new valid tests cover DPUB/Graphics and the fallback-list shape.
1 parent 24882a3 commit 0c1d701

2 files changed

Lines changed: 58 additions & 103 deletions

File tree

lib/rules/template-no-invalid-role.js

Lines changed: 28 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,19 @@
1-
const VALID_ROLES = new Set([
2-
'alert',
3-
'alertdialog',
4-
'application',
5-
'article',
1+
const { roles } = require('aria-query');
2+
3+
// Valid ARIA roles = concrete (non-abstract) entries from aria-query, plus a
4+
// small set of WAI-ARIA 1.3 draft roles that aria-query doesn't yet ship. The
5+
// ARIA 1.2 base roles, DPUB-ARIA (doc-*), and Graphics-ARIA (graphics-*) all
6+
// come from aria-query.
7+
const ARIA_13_DRAFT_ROLES = [
68
'associationlist',
79
'associationlistitemkey',
810
'associationlistitemvalue',
9-
'banner',
10-
'blockquote',
11-
'button',
12-
'caption',
13-
'cell',
14-
'checkbox',
15-
'code',
16-
'columnheader',
17-
'combobox',
1811
'comment',
19-
'complementary',
20-
'contentinfo',
21-
'definition',
22-
'deletion',
23-
'dialog',
24-
'directory',
25-
'document',
26-
'emphasis',
27-
'feed',
28-
'figure',
29-
'form',
30-
'generic',
31-
'grid',
32-
'gridcell',
33-
'group',
34-
'heading',
35-
'img',
36-
'insertion',
37-
'link',
38-
'list',
39-
'listbox',
40-
'listitem',
41-
'log',
42-
'main',
43-
'mark',
44-
'marquee',
45-
'math',
46-
'menu',
47-
'menubar',
48-
'menuitem',
49-
'menuitemcheckbox',
50-
'menuitemradio',
51-
'meter',
52-
'navigation',
53-
'none',
54-
'note',
55-
'option',
56-
'paragraph',
57-
'presentation',
58-
'progressbar',
59-
'radio',
60-
'radiogroup',
61-
'region',
62-
'row',
63-
'rowgroup',
64-
'rowheader',
65-
'scrollbar',
66-
'search',
67-
'searchbox',
68-
'separator',
69-
'slider',
70-
'spinbutton',
71-
'status',
72-
'strong',
73-
'subscript',
7412
'suggestion',
75-
'superscript',
76-
'switch',
77-
'tab',
78-
'table',
79-
'tablist',
80-
'tabpanel',
81-
'term',
82-
'textbox',
83-
'time',
84-
'timer',
85-
'toolbar',
86-
'tooltip',
87-
'tree',
88-
'treegrid',
89-
'treeitem',
13+
];
14+
const VALID_ROLES = new Set([
15+
...[...roles.keys()].filter((role) => !roles.get(role).abstract),
16+
...ARIA_13_DRAFT_ROLES,
9017
]);
9118

9219
// Elements with semantic meaning that should not be given role="presentation" or role="none"
@@ -225,34 +152,38 @@ module.exports = {
225152
return;
226153
}
227154

228-
const role = roleAttr.value.chars.trim();
229-
if (!role) {
155+
const raw = roleAttr.value.chars.trim();
156+
if (!raw) {
230157
return;
231158
}
232159

233-
const roleLower = role.toLowerCase();
160+
// ARIA role attribute is a whitespace-separated list of tokens
161+
// (role-fallback pattern per ARIA 1.2 §5.4). Validate each token.
162+
const tokens = raw.split(/\s+/u).map((t) => t.toLowerCase());
234163

235-
// Check for nonexistent roles
236-
if (catchNonexistentRoles && !VALID_ROLES.has(roleLower)) {
237-
context.report({
238-
node: roleAttr,
239-
messageId: 'invalid',
240-
data: { role },
241-
});
242-
return;
164+
if (catchNonexistentRoles) {
165+
const invalidToken = tokens.find((token) => !VALID_ROLES.has(token));
166+
if (invalidToken) {
167+
context.report({
168+
node: roleAttr,
169+
messageId: 'invalid',
170+
data: { role: invalidToken },
171+
});
172+
return;
173+
}
243174
}
244175

245176
// Check for presentation/none role on semantic elements (case-insensitive per WAI-ARIA 1.2:
246177
// "Case-sensitivity of the comparison inherits from the case-sensitivity of the host language"
247178
// and HTML is case-insensitive — https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles)
248179
if (
249-
(roleLower === 'presentation' || roleLower === 'none') &&
180+
tokens.some((t) => t === 'presentation' || t === 'none') &&
250181
SEMANTIC_ELEMENTS.has(node.tag)
251182
) {
252183
context.report({
253184
node: roleAttr,
254185
messageId: 'presentationOnSemantic',
255-
data: { role, tag: node.tag },
186+
data: { role: raw, tag: node.tag },
256187
});
257188
}
258189
},

tests/lib/rules/template-no-invalid-role.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ ruleTester.run('template-no-invalid-role', rule, {
7171
code: '<template><div role="command interface"></div></template>',
7272
options: [{ catchNonexistentRoles: false }],
7373
},
74+
75+
// DPUB-ARIA (doc-*) and Graphics-ARIA (graphics-*) are in the WAI-ARIA
76+
// ecosystem via aria-query; previously flagged because our hand-maintained
77+
// VALID_ROLES didn't include them.
78+
'<template><div role="doc-abstract">Abstract</div></template>',
79+
'<template><section role="doc-chapter"></section></template>',
80+
'<template><svg role="graphics-document"></svg></template>',
81+
'<template><svg role="graphics-object"></svg></template>',
82+
83+
// Whitespace-separated role fallback list — ARIA 1.2 §5.4. Each token
84+
// must individually be valid.
85+
'<template><div role="tabpanel row"></div></template>',
86+
'<template><svg role="graphics-document document"></svg></template>',
87+
'<template><section role="doc-appendix doc-bibliography"></section></template>',
7488
],
7589

7690
invalid: [
@@ -164,18 +178,18 @@ ruleTester.run('template-no-invalid-role', rule, {
164178
{
165179
code: '<template><div role="command interface"></div></template>',
166180
output: null,
167-
errors: [{ message: "Invalid ARIA role 'command interface'. Must be a valid ARIA role." }],
181+
errors: [{ message: "Invalid ARIA role 'command'. Must be a valid ARIA role." }],
168182
},
169183
{
170184
code: '<template><div role="COMMAND INTERFACE"></div></template>',
171185
output: null,
172-
errors: [{ message: "Invalid ARIA role 'COMMAND INTERFACE'. Must be a valid ARIA role." }],
186+
errors: [{ message: "Invalid ARIA role 'command'. Must be a valid ARIA role." }],
173187
},
174188
{
175189
code: '<template><div role="command interface"></div></template>',
176190
output: null,
177191
options: [{ catchNonexistentRoles: true }],
178-
errors: [{ message: "Invalid ARIA role 'command interface'. Must be a valid ARIA role." }],
192+
errors: [{ message: "Invalid ARIA role 'command'. Must be a valid ARIA role." }],
179193
},
180194

181195
// Newly added SEMANTIC_ELEMENTS: presentation/none on iframe, video, audio
@@ -247,6 +261,16 @@ hbsRuleTester.run('template-no-invalid-role', rule, {
247261
code: '<div role="command interface"></div>',
248262
options: [{ catchNonexistentRoles: false }],
249263
},
264+
265+
// DPUB-ARIA (doc-*) and Graphics-ARIA (graphics-*) roles.
266+
'<div role="doc-abstract">Abstract</div>',
267+
'<section role="doc-chapter"></section>',
268+
'<svg role="graphics-document"></svg>',
269+
270+
// Whitespace-separated role fallback list.
271+
'<div role="tabpanel row"></div>',
272+
'<svg role="graphics-document document"></svg>',
273+
'<section role="doc-appendix doc-bibliography"></section>',
250274
],
251275
invalid: [
252276
{
@@ -302,18 +326,18 @@ hbsRuleTester.run('template-no-invalid-role', rule, {
302326
{
303327
code: '<div role="command interface"></div>',
304328
output: null,
305-
errors: [{ message: "Invalid ARIA role 'command interface'. Must be a valid ARIA role." }],
329+
errors: [{ message: "Invalid ARIA role 'command'. Must be a valid ARIA role." }],
306330
},
307331
{
308332
code: '<div role="command interface"></div>',
309333
output: null,
310334
options: [{ catchNonexistentRoles: true }],
311-
errors: [{ message: "Invalid ARIA role 'command interface'. Must be a valid ARIA role." }],
335+
errors: [{ message: "Invalid ARIA role 'command'. Must be a valid ARIA role." }],
312336
},
313337
{
314338
code: '<div role="COMMAND INTERFACE"></div>',
315339
output: null,
316-
errors: [{ message: "Invalid ARIA role 'COMMAND INTERFACE'. Must be a valid ARIA role." }],
340+
errors: [{ message: "Invalid ARIA role 'command'. Must be a valid ARIA role." }],
317341
},
318342
// Newly added SEMANTIC_ELEMENTS: presentation/none on iframe, video, audio, embed
319343
{

0 commit comments

Comments
 (0)