Skip to content

Commit e54a98b

Browse files
committed
VIV-2390 add no-idref-aria-attribute (eslint-plugin)
1 parent beb3230 commit e54a98b

File tree

6 files changed

+277
-3
lines changed

6 files changed

+277
-3
lines changed

apps/docs/content/_data/eslintRules.json

+4
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,9 @@
2626
{
2727
"name": "no-current-value-attribute",
2828
"markdown": "./libs/eslint-plugin/src/rules/no-current-value-attribute.md"
29+
},
30+
{
31+
"name": "no-idref-aria-attribute",
32+
"markdown": "./libs/eslint-plugin/src/rules/no-idref-aria-attribute.md"
2933
}
3034
]

libs/eslint-plugin/src/index.spec.ts

+15
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe('eslint-plugin', () => {
1515
"@vonage/vivid/no-anchor-attribute": "error",
1616
"@vonage/vivid/no-current-value-attribute": "error",
1717
"@vonage/vivid/no-deprecated-apis": "error",
18+
"@vonage/vivid/no-idref-aria-attribute": "error",
1819
"@vonage/vivid/no-inaccessible-events": "error",
1920
"@vonage/vivid/no-slot-attribute": "error",
2021
"@vonage/vivid/no-value-attribute": "error",
@@ -30,6 +31,7 @@ describe('eslint-plugin', () => {
3031
"@vonage/vivid/no-anchor-attribute": "error",
3132
"@vonage/vivid/no-current-value-attribute": "error",
3233
"@vonage/vivid/no-deprecated-apis": "error",
34+
"@vonage/vivid/no-idref-aria-attribute": "error",
3335
"@vonage/vivid/no-inaccessible-events": "error",
3436
"@vonage/vivid/no-slot-attribute": "error",
3537
"@vonage/vivid/no-value-attribute": "error",
@@ -80,6 +82,19 @@ describe('eslint-plugin', () => {
8082
"type": "problem",
8183
},
8284
},
85+
"no-idref-aria-attribute": {
86+
"create": [Function],
87+
"meta": {
88+
"docs": {
89+
"description": "Do not use IDREF ARIA attributes on components.",
90+
},
91+
"messages": {
92+
"noIdrefAriaAttribute": "IDREF ARIA attributes (like {{attribute}}) should not be used on components that delegate ARIA attributes, as they will not work correctly with shadow DOM.",
93+
},
94+
"schema": [],
95+
"type": "problem",
96+
},
97+
},
8398
"no-inaccessible-events": {
8499
"create": [Function],
85100
"meta": {

libs/eslint-plugin/src/index.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { ESLint } from 'eslint';
2-
import { noDeprecatedAPIs } from './rules/no-deprecated-apis';
32
import { accessibleNames } from './rules/accessible-names';
4-
import { noInaccessibleEvents } from './rules/no-inaccessible-events';
53
import { noAnchorAttribute } from './rules/no-anchor-attribute';
4+
import { noCurrentValueAttribute } from './rules/no-current-value-attribute';
5+
import { noDeprecatedAPIs } from './rules/no-deprecated-apis';
6+
import { noIdrefAriaAttribute } from './rules/no-idref-aria-attribute';
7+
import { noInaccessibleEvents } from './rules/no-inaccessible-events';
68
import { noSlotAttribute } from './rules/no-slot-attribute';
79
import { noValueAttribute } from './rules/no-value-attribute';
8-
import { noCurrentValueAttribute } from './rules/no-current-value-attribute';
910

1011
const rules = {
1112
'@vonage/vivid/no-deprecated-apis': 'error',
@@ -15,6 +16,7 @@ const rules = {
1516
'@vonage/vivid/no-slot-attribute': 'error',
1617
'@vonage/vivid/no-value-attribute': 'error',
1718
'@vonage/vivid/no-current-value-attribute': 'error',
19+
'@vonage/vivid/no-idref-aria-attribute': 'error',
1820
} as const;
1921

2022
const eslintPluginVivid: ESLint.Plugin = {
@@ -32,6 +34,7 @@ const eslintPluginVivid: ESLint.Plugin = {
3234
'no-slot-attribute': noSlotAttribute,
3335
'no-value-attribute': noValueAttribute,
3436
'no-current-value-attribute': noCurrentValueAttribute,
37+
'no-idref-aria-attribute': noIdrefAriaAttribute,
3538
},
3639
};
3740

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
This rule prevents the use of IDREF ARIA attributes on components. These attributes will not work correctly with shadow DOM. Why? When these components are used with shadow DOM, IDREF ARIA attributes will not work correctly because they cannot reference elements across shadow DOM boundaries.
2+
3+
The following ARIA attributes are IDREF attributes and should not be used on components.
4+
5+
- `aria-activedescendant`
6+
- `aria-controls`
7+
- `aria-describedby`
8+
- `aria-details`
9+
- `aria-errormessage`
10+
- `aria-flowto`
11+
- `aria-labelledby`
12+
- `aria-owns`
13+
14+
#### Example
15+
16+
```html
17+
<!-- ❌ BAD -->
18+
<VButton aria-labelledby="button-label">Click me</VButton>
19+
<div id="button-label">This is a button</div>
20+
<VTextField aria-describedby="field-description"></VTextField>
21+
<div id="field-description">Enter your name</div>
22+
<VMenu aria-controls="menu-trigger"></VMenu>
23+
<button id="menu-trigger">Open menu</button>
24+
25+
<!-- ✅ GOOD -->
26+
<VButton aria-label="This is a button">Click me</VButton>
27+
<VTextField aria-label="Name" aria-description="Enter your name"></VTextField>
28+
<VMenu aria-label="Menu"></VMenu>
29+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { RuleTester } from 'eslint';
2+
import { noIdrefAriaAttribute } from './no-idref-aria-attribute';
3+
import { convertAnnotatedSourceToFailureCase } from '../utils/testing';
4+
5+
const ruleTester = new RuleTester({
6+
parser: require.resolve('vue-eslint-parser'),
7+
parserOptions: {
8+
sourceType: 'module',
9+
},
10+
});
11+
12+
ruleTester.run('no-idref-aria-attribute', noIdrefAriaAttribute, {
13+
valid: [
14+
{
15+
code: '<template><VBreadcrumb aria-label="breadcrumb"></VBreadcrumb></template>',
16+
},
17+
{
18+
code: '<template><VButton aria-label="button"></VButton></template>',
19+
},
20+
{
21+
code: '<template><VNav aria-label="navigation"></VNav></template>',
22+
},
23+
{
24+
code: '<template><VTagGroup aria-label="tags"></VTagGroup></template>',
25+
},
26+
{
27+
code: '<template><VTag aria-label="tag"></VTag></template>',
28+
},
29+
{
30+
code: '<template><VActionGroup aria-label="actions"></VActionGroup></template>',
31+
},
32+
{
33+
code: '<template><VHeader aria-label="header"></VHeader></template>',
34+
},
35+
{
36+
code: '<template><VSwitch aria-label="switch"></VSwitch></template>',
37+
},
38+
{
39+
code: '<template><VDivider aria-label="divider"></VDivider></template>',
40+
},
41+
{
42+
code: '<template><VTextArea aria-label="textarea"></VTextArea></template>',
43+
},
44+
{
45+
code: '<template><VCheckbox aria-label="checkbox"></VCheckbox></template>',
46+
},
47+
{
48+
code: '<template><VSearchableSelect aria-label="select"></VSearchableSelect></template>',
49+
},
50+
{
51+
code: '<template><VFilePicker aria-label="file picker"></VFilePicker></template>',
52+
},
53+
{
54+
code: '<template><VCalendarEvent aria-label="event"></VCalendarEvent></template>',
55+
},
56+
{
57+
code: '<template><VProgress aria-label="progress"></VProgress></template>',
58+
},
59+
{
60+
code: '<template><VProgressRing aria-label="progress"></VProgressRing></template>',
61+
},
62+
{
63+
code: '<template><VSelectableBox aria-label="box"></VSelectableBox></template>',
64+
},
65+
{
66+
code: '<template><VBanner aria-label="banner"></VBanner></template>',
67+
},
68+
{
69+
code: '<template><VMenu aria-label="menu"></VMenu></template>',
70+
},
71+
{
72+
code: '<template><VTextField aria-label="text field"></VTextField></template>',
73+
},
74+
{
75+
code: '<template><VDialog aria-label="dialog"></VDialog></template>',
76+
},
77+
{
78+
code: '<template><VSlider aria-label="slider"></VSlider></template>',
79+
},
80+
{
81+
code: '<template><VTextAnchor aria-label="anchor"></VTextAnchor></template>',
82+
},
83+
{
84+
code: '<template><VSplitButton aria-label="split button"></VSplitButton></template>',
85+
},
86+
{
87+
code: '<template><VNumberField aria-label="number field"></VNumberField></template>',
88+
},
89+
{
90+
code: '<template><VNavDisclosure aria-label="disclosure"></VNavDisclosure></template>',
91+
},
92+
],
93+
invalid: [
94+
convertAnnotatedSourceToFailureCase({
95+
annotatedSource: `
96+
<template><VMenu aria-controls="id"></VMenu></template>
97+
~~~~~~~~~~~~~
98+
`,
99+
message:
100+
'IDREF ARIA attributes (like aria-controls) should not be used on components that delegate ARIA attributes, as they will not work correctly with shadow DOM.',
101+
}),
102+
convertAnnotatedSourceToFailureCase({
103+
annotatedSource: `
104+
<template><VButton aria-labelledby="id"></VButton></template>
105+
~~~~~~~~~~~~~~~
106+
`,
107+
message:
108+
'IDREF ARIA attributes (like aria-labelledby) should not be used on components that delegate ARIA attributes, as they will not work correctly with shadow DOM.',
109+
}),
110+
convertAnnotatedSourceToFailureCase({
111+
annotatedSource: `
112+
<template><VTextField aria-owns="id"></VTextField></template>
113+
~~~~~~~~~
114+
`,
115+
message:
116+
'IDREF ARIA attributes (like aria-owns) should not be used on components that delegate ARIA attributes, as they will not work correctly with shadow DOM.',
117+
}),
118+
convertAnnotatedSourceToFailureCase({
119+
annotatedSource: `
120+
<template><VDialog aria-details="id"></VDialog></template>
121+
~~~~~~~~~~~~
122+
`,
123+
message:
124+
'IDREF ARIA attributes (like aria-details) should not be used on components that delegate ARIA attributes, as they will not work correctly with shadow DOM.',
125+
}),
126+
convertAnnotatedSourceToFailureCase({
127+
annotatedSource: `
128+
<template><VMenu aria-errormessage="id"></VMenu></template>
129+
~~~~~~~~~~~~~~~~~
130+
`,
131+
message:
132+
'IDREF ARIA attributes (like aria-errormessage) should not be used on components that delegate ARIA attributes, as they will not work correctly with shadow DOM.',
133+
}),
134+
convertAnnotatedSourceToFailureCase({
135+
annotatedSource: `
136+
<template><VButton aria-flowto="id"></VButton></template>
137+
~~~~~~~~~~~
138+
`,
139+
message:
140+
'IDREF ARIA attributes (like aria-flowto) should not be used on components that delegate ARIA attributes, as they will not work correctly with shadow DOM.',
141+
}),
142+
],
143+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Rule } from 'eslint';
2+
import * as utils from 'eslint-plugin-vue/lib/utils/index.js';
3+
import { getAttributes } from '../utils/attributes';
4+
import { ComponentMetadata } from '../utils/ComponentMetadata';
5+
import { normalizeTag } from '../utils/components';
6+
7+
const IDREF_ARIA_ATTRIBUTES = [
8+
'aria-activedescendant',
9+
'aria-controls',
10+
'aria-describedby',
11+
'aria-details',
12+
'aria-errormessage',
13+
'aria-flowto',
14+
'aria-labelledby',
15+
'aria-owns',
16+
];
17+
18+
const components = new ComponentMetadata<null>();
19+
20+
// Add all components that use DelegatesAria
21+
components.add('VBreadcrumb', null);
22+
components.add('VBreadcrumbItem', null);
23+
components.add('VButton', null);
24+
components.add('VNav', null);
25+
components.add('VTagGroup', null);
26+
components.add('VTag', null);
27+
components.add('VActionGroup', null);
28+
components.add('VHeader', null);
29+
components.add('VSwitch', null);
30+
components.add('VDivider', null);
31+
components.add('VTextArea', null);
32+
components.add('VCheckbox', null);
33+
components.add('VSearchableSelect', null);
34+
components.add('VFilePicker', null);
35+
components.add('VCalendarEvent', null);
36+
components.add('VProgress', null);
37+
components.add('VProgressRing', null);
38+
components.add('VSelectableBox', null);
39+
components.add('VBanner', null);
40+
components.add('VMenu', null);
41+
components.add('VTextField', null);
42+
components.add('VDialog', null);
43+
components.add('VSlider', null);
44+
components.add('VTextAnchor', null);
45+
components.add('VSplitButton', null);
46+
components.add('VNumberField', null);
47+
components.add('VNavDisclosure', null);
48+
49+
export const noIdrefAriaAttribute: Rule.RuleModule = {
50+
meta: {
51+
type: 'problem',
52+
docs: {
53+
description: 'Do not use IDREF ARIA attributes on components.',
54+
},
55+
messages: {
56+
noIdrefAriaAttribute:
57+
'IDREF ARIA attributes (like {{attribute}}) should not be used on components that delegate ARIA attributes, as they will not work correctly with shadow DOM.',
58+
},
59+
schema: [],
60+
},
61+
create(context) {
62+
return utils.defineTemplateBodyVisitor(context, {
63+
VElement(node) {
64+
const tagName = normalizeTag(node.name);
65+
components.forTag(tagName, () => {
66+
const attrs = getAttributes(node.startTag);
67+
for (const attr of attrs) {
68+
if (IDREF_ARIA_ATTRIBUTES.includes(attr.node.rawName)) {
69+
context.report({
70+
loc: attr.node.loc,
71+
messageId: 'noIdrefAriaAttribute',
72+
data: { attribute: attr.node.rawName },
73+
});
74+
}
75+
}
76+
});
77+
},
78+
});
79+
},
80+
};

0 commit comments

Comments
 (0)