@dialpad/eslint-plugin-dialtone 1.12.0-next.1 → 1.12.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Detect deprecated physical direction names in Dialtone component
|
|
3
|
+
* slots, props, prop values, and events. Suggests logical replacements.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
//------------------------------------------------------------------------------
|
|
8
|
+
// Constants
|
|
9
|
+
//------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Maps component names to their deprecated slot names and replacements.
|
|
13
|
+
* null value = special handling (ambiguous, e.g. #icon on dt-button).
|
|
14
|
+
*/
|
|
15
|
+
const DEPRECATED_SLOTS = {
|
|
16
|
+
'dt-badge': { leftIcon: 'startIcon', rightIcon: 'endIcon' },
|
|
17
|
+
'dt-button': { icon: null },
|
|
18
|
+
'dt-input': { leftIcon: 'startIcon', rightIcon: 'endIcon' },
|
|
19
|
+
'dt-tab': { leftIcon: 'startIcon' },
|
|
20
|
+
'dt-split-button': { alphaIcon: 'startIcon', omegaIcon: 'endIcon', omega: 'end' },
|
|
21
|
+
'dt-item-layout': { left: 'start', right: 'end', bottom: 'blockEnd' },
|
|
22
|
+
'dt-recipe-callbox': { right: 'end', bottom: 'blockEnd' },
|
|
23
|
+
'dt-recipe-contact-centers-row': { right: 'end' },
|
|
24
|
+
'dt-recipe-general-row': { left: 'start' },
|
|
25
|
+
'dt-recipe-top-banner-info': { left: 'start', right: 'end' },
|
|
26
|
+
'dt-recipe-grouped-chip': {
|
|
27
|
+
leftIcon: 'startIcon',
|
|
28
|
+
rightIcon: 'endIcon',
|
|
29
|
+
leftContent: 'startContent',
|
|
30
|
+
rightContent: 'endContent',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maps component names to their deprecated prop names (kebab-case) and replacements.
|
|
36
|
+
*/
|
|
37
|
+
const DEPRECATED_PROPS = {
|
|
38
|
+
'dt-item-layout': {
|
|
39
|
+
'left-class': 'start-class',
|
|
40
|
+
'right-class': 'end-class',
|
|
41
|
+
'bottom-class': 'block-end-class',
|
|
42
|
+
},
|
|
43
|
+
'dt-split-button': {
|
|
44
|
+
'alpha-active': 'start-active',
|
|
45
|
+
'alpha-aria-label': 'start-aria-label',
|
|
46
|
+
'alpha-icon-position': 'start-icon-position',
|
|
47
|
+
'alpha-leading-class': 'start-leading-class',
|
|
48
|
+
'alpha-trailing-class': 'start-trailing-class',
|
|
49
|
+
'alpha-label-class': 'start-label-class',
|
|
50
|
+
'alpha-disabled': 'start-disabled',
|
|
51
|
+
'alpha-loading': 'start-loading',
|
|
52
|
+
'alpha-tooltip-text': 'start-tooltip-text',
|
|
53
|
+
'omega-active': 'end-active',
|
|
54
|
+
'omega-aria-label': 'end-aria-label',
|
|
55
|
+
'omega-disabled': 'end-disabled',
|
|
56
|
+
'omega-id': 'end-id',
|
|
57
|
+
'omega-tooltip-text': 'end-tooltip-text',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Maps component names to props whose specific values are deprecated.
|
|
63
|
+
*/
|
|
64
|
+
const DEPRECATED_PROP_VALUES = {
|
|
65
|
+
'dt-button': {
|
|
66
|
+
'icon-position': { left: 'start', right: 'end', top: 'blockStart', bottom: 'blockEnd' },
|
|
67
|
+
},
|
|
68
|
+
'dt-root-layout': {
|
|
69
|
+
'sidebar-position': { left: 'start', right: 'end' },
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Maps component names to their deprecated event names and replacements.
|
|
75
|
+
*/
|
|
76
|
+
const DEPRECATED_EVENTS = {
|
|
77
|
+
'dt-split-button': { 'alpha-clicked': 'start-clicked', 'omega-clicked': 'end-clicked' },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
//------------------------------------------------------------------------------
|
|
81
|
+
// Helpers
|
|
82
|
+
//------------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Converts PascalCase or camelCase to kebab-case.
|
|
86
|
+
* e.g. 'DtBadge' → 'dt-badge', 'DtRecipeCallbox' → 'dt-recipe-callbox'
|
|
87
|
+
*/
|
|
88
|
+
function toKebabCase (str) {
|
|
89
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//------------------------------------------------------------------------------
|
|
93
|
+
// Rule Definition
|
|
94
|
+
//------------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
meta: {
|
|
98
|
+
type: 'suggestion',
|
|
99
|
+
docs: {
|
|
100
|
+
description: 'Detect deprecated physical direction names in Dialtone component slots, props, prop values, and events',
|
|
101
|
+
recommended: false,
|
|
102
|
+
url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-physical-naming.md',
|
|
103
|
+
},
|
|
104
|
+
fixable: null,
|
|
105
|
+
schema: [],
|
|
106
|
+
messages: {
|
|
107
|
+
deprecatedSlot:
|
|
108
|
+
'#{{ oldSlot }} on <{{ component }}> is deprecated. Use #{{ newSlot }} instead.',
|
|
109
|
+
deprecatedIconSlot:
|
|
110
|
+
'The #icon slot on <dt-button> is deprecated. Use #startIcon, #endIcon, #blockStartIcon, or #blockEndIcon instead.',
|
|
111
|
+
deprecatedProp:
|
|
112
|
+
'{{ oldProp }} on <{{ component }}> is deprecated. Use {{ newProp }} instead.',
|
|
113
|
+
deprecatedPropValue:
|
|
114
|
+
'{{ prop }}="{{ oldValue }}" on <{{ component }}> is deprecated. Use {{ prop }}="{{ newValue }}" instead.',
|
|
115
|
+
deprecatedEvent:
|
|
116
|
+
'@{{ oldEvent }} on <{{ component }}> is deprecated. Use @{{ newEvent }} instead.',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
create (context) {
|
|
121
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
122
|
+
return sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
123
|
+
|
|
124
|
+
VElement (node) {
|
|
125
|
+
const rawName = node.rawName || node.name;
|
|
126
|
+
const elementName = rawName.includes('-') ? rawName : toKebabCase(rawName);
|
|
127
|
+
|
|
128
|
+
const slotsMap = DEPRECATED_SLOTS[elementName];
|
|
129
|
+
const propsMap = DEPRECATED_PROPS[elementName];
|
|
130
|
+
const propValuesMap = DEPRECATED_PROP_VALUES[elementName];
|
|
131
|
+
const eventsMap = DEPRECATED_EVENTS[elementName];
|
|
132
|
+
|
|
133
|
+
// Check attributes in a single pass (props, prop values, events)
|
|
134
|
+
if (propsMap || propValuesMap || eventsMap) {
|
|
135
|
+
for (const attr of node.startTag.attributes) {
|
|
136
|
+
if (!attr.directive) {
|
|
137
|
+
// Static props: alpha-active, left-class="x", icon-position="left"
|
|
138
|
+
const attrName = attr.key && (attr.key.rawName || attr.key.name);
|
|
139
|
+
if (attrName && propsMap && propsMap[attrName]) {
|
|
140
|
+
context.report({
|
|
141
|
+
node: attr,
|
|
142
|
+
messageId: 'deprecatedProp',
|
|
143
|
+
data: {
|
|
144
|
+
component: elementName,
|
|
145
|
+
oldProp: attrName,
|
|
146
|
+
newProp: propsMap[attrName],
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (attrName && propValuesMap && propValuesMap[attrName] && attr.value) {
|
|
151
|
+
const val = attr.value.value;
|
|
152
|
+
if (propValuesMap[attrName][val]) {
|
|
153
|
+
context.report({
|
|
154
|
+
node: attr,
|
|
155
|
+
messageId: 'deprecatedPropValue',
|
|
156
|
+
data: {
|
|
157
|
+
component: elementName,
|
|
158
|
+
prop: attrName,
|
|
159
|
+
oldValue: val,
|
|
160
|
+
newValue: propValuesMap[attrName][val],
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} else if (attr.directive && attr.key.name.name === 'bind') {
|
|
166
|
+
// Dynamic props: :alpha-active="x", v-bind:omega-disabled="y"
|
|
167
|
+
const bindName = attr.key.argument && (attr.key.argument.rawName || attr.key.argument.name);
|
|
168
|
+
if (bindName && propsMap && propsMap[bindName]) {
|
|
169
|
+
context.report({
|
|
170
|
+
node: attr,
|
|
171
|
+
messageId: 'deprecatedProp',
|
|
172
|
+
data: {
|
|
173
|
+
component: elementName,
|
|
174
|
+
oldProp: bindName,
|
|
175
|
+
newProp: propsMap[bindName],
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
} else if (eventsMap && attr.key.name.name === 'on') {
|
|
180
|
+
const eventName = attr.key.argument && (attr.key.argument.rawName || attr.key.argument.name);
|
|
181
|
+
if (eventName && eventsMap[eventName]) {
|
|
182
|
+
context.report({
|
|
183
|
+
node: attr,
|
|
184
|
+
messageId: 'deprecatedEvent',
|
|
185
|
+
data: {
|
|
186
|
+
component: elementName,
|
|
187
|
+
oldEvent: eventName,
|
|
188
|
+
newEvent: eventsMap[eventName],
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check child <template> elements for deprecated slot names
|
|
197
|
+
if (slotsMap) {
|
|
198
|
+
for (const child of node.children) {
|
|
199
|
+
if (child.type === 'VElement' && child.name === 'template') {
|
|
200
|
+
for (const attr of child.startTag.attributes) {
|
|
201
|
+
if (attr.directive && attr.key.name.name === 'slot') {
|
|
202
|
+
const slotName = attr.key.argument && (attr.key.argument.rawName || attr.key.argument.name);
|
|
203
|
+
if (slotName && slotName in slotsMap) {
|
|
204
|
+
if (slotsMap[slotName] === null) {
|
|
205
|
+
// #icon on dt-button is ambiguous — consumer must choose replacement
|
|
206
|
+
context.report({ node: attr, messageId: 'deprecatedIconSlot' });
|
|
207
|
+
} else {
|
|
208
|
+
context.report({
|
|
209
|
+
node: attr,
|
|
210
|
+
messageId: 'deprecatedSlot',
|
|
211
|
+
data: {
|
|
212
|
+
component: elementName,
|
|
213
|
+
oldSlot: slotName,
|
|
214
|
+
newSlot: slotsMap[slotName],
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Detects usage of deprecated t-shirt size props (xs, sm, md, lg, xl) on Dialtone
|
|
3
|
+
* components and suggests numeric equivalents (100, 200, 300, 400, 500).
|
|
4
|
+
* @author Dialtone Team
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// ------------------------------------------------------------------------------
|
|
9
|
+
// Rule Definition
|
|
10
|
+
// ------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const SIZE_MAP = {
|
|
13
|
+
xs: '100',
|
|
14
|
+
sm: '200',
|
|
15
|
+
md: '300',
|
|
16
|
+
lg: '400',
|
|
17
|
+
xl: '500',
|
|
18
|
+
'2xl': '600',
|
|
19
|
+
'3xl': '700',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const TSHIRT_VALUES = new Set(Object.keys(SIZE_MAP));
|
|
23
|
+
|
|
24
|
+
// Props that accept the component size scale
|
|
25
|
+
const SIZE_PROPS = ['size', 'label-size', 'labelSize'];
|
|
26
|
+
|
|
27
|
+
// Speed prop on motion-text also uses the same scale
|
|
28
|
+
const SPEED_PROPS = ['speed'];
|
|
29
|
+
|
|
30
|
+
// All size-related prop names
|
|
31
|
+
const ALL_SIZE_PROPS = [...SIZE_PROPS, ...SPEED_PROPS];
|
|
32
|
+
|
|
33
|
+
// Only flag on Dialtone components (dt-* or Dt*)
|
|
34
|
+
function isDialtoneComponent (node) {
|
|
35
|
+
const parent = node.parent;
|
|
36
|
+
if (!parent || parent.type !== 'VStartTag') return false;
|
|
37
|
+
const element = parent.parent;
|
|
38
|
+
if (!element || element.type !== 'VElement') return false;
|
|
39
|
+
const name = element.rawName || element.name || '';
|
|
40
|
+
return name.startsWith('dt-') || name.startsWith('Dt');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
meta: {
|
|
45
|
+
type: 'suggestion',
|
|
46
|
+
docs: {
|
|
47
|
+
description: 'T-shirt sizes (xs, sm, md, lg, xl) are deprecated. Use numeric scale (100, 200, 300, 400, 500) instead.',
|
|
48
|
+
recommended: false,
|
|
49
|
+
url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-tshirt-sizes.md',
|
|
50
|
+
},
|
|
51
|
+
fixable: 'code',
|
|
52
|
+
schema: [],
|
|
53
|
+
messages: {
|
|
54
|
+
deprecatedSize: 'Size "{{oldSize}}" is deprecated. Use :{{prop}}="{{newSize}}" instead.',
|
|
55
|
+
deprecatedSizeInBinding: 'T-shirt size "{{oldSize}}" in dynamic binding is deprecated. Use numeric {{newSize}} instead.',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
create (context) {
|
|
60
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
61
|
+
return sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
62
|
+
VAttribute (node) {
|
|
63
|
+
if (!isDialtoneComponent(node)) return;
|
|
64
|
+
|
|
65
|
+
// Get the prop name and check if it's a size-related prop
|
|
66
|
+
const isDirective = node.directive;
|
|
67
|
+
const propName = isDirective
|
|
68
|
+
? (node.key.argument && node.key.argument.name)
|
|
69
|
+
: node.key.name;
|
|
70
|
+
|
|
71
|
+
if (!propName || !ALL_SIZE_PROPS.includes(propName)) return;
|
|
72
|
+
|
|
73
|
+
// --- Static attributes: size="sm" → auto-fixable ---
|
|
74
|
+
if (!isDirective && node.value && node.value.value) {
|
|
75
|
+
const sizeValue = node.value.value;
|
|
76
|
+
if (SIZE_MAP[sizeValue]) {
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
messageId: 'deprecatedSize',
|
|
80
|
+
data: {
|
|
81
|
+
oldSize: sizeValue,
|
|
82
|
+
newSize: SIZE_MAP[sizeValue],
|
|
83
|
+
prop: propName,
|
|
84
|
+
},
|
|
85
|
+
fix (fixer) {
|
|
86
|
+
const newAttr = `:${propName}="${SIZE_MAP[sizeValue]}"`;
|
|
87
|
+
return fixer.replaceTextRange(
|
|
88
|
+
[node.range[0], node.range[1]],
|
|
89
|
+
newAttr,
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Dynamic bindings: :size="'sm'" or :size="x ? 'sm' : 'md'" ---
|
|
98
|
+
if (isDirective && node.value && node.value.expression) {
|
|
99
|
+
// Walk the expression tree for string literals with t-shirt values
|
|
100
|
+
const expression = node.value.expression;
|
|
101
|
+
const literals = [];
|
|
102
|
+
|
|
103
|
+
(function findLiterals (n) {
|
|
104
|
+
if (!n) return;
|
|
105
|
+
if (n.type === 'Literal' && typeof n.value === 'string' && TSHIRT_VALUES.has(n.value)) {
|
|
106
|
+
literals.push(n);
|
|
107
|
+
}
|
|
108
|
+
// Walk child nodes
|
|
109
|
+
for (const key of Object.keys(n)) {
|
|
110
|
+
if (key === 'parent') continue;
|
|
111
|
+
const child = n[key];
|
|
112
|
+
if (child && typeof child === 'object') {
|
|
113
|
+
if (Array.isArray(child)) {
|
|
114
|
+
child.forEach(c => { if (c && c.type) findLiterals(c); });
|
|
115
|
+
} else if (child.type) {
|
|
116
|
+
findLiterals(child);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
})(expression);
|
|
121
|
+
|
|
122
|
+
for (const literal of literals) {
|
|
123
|
+
context.report({
|
|
124
|
+
node: literal,
|
|
125
|
+
messageId: 'deprecatedSizeInBinding',
|
|
126
|
+
data: {
|
|
127
|
+
oldSize: literal.value,
|
|
128
|
+
newSize: SIZE_MAP[literal.value],
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Warns when v-dt-focusgroup is used without an accessible label on the same element.
|
|
3
|
+
* @author Dialtone
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description:
|
|
12
|
+
'Warns when v-dt-focusgroup is used on an element without aria-label or aria-labelledby. ' +
|
|
13
|
+
'Screen readers need an accessible name to identify the widget.',
|
|
14
|
+
recommended: false,
|
|
15
|
+
url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/focusgroup-requires-label.md',
|
|
16
|
+
},
|
|
17
|
+
fixable: null,
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
missingLabel:
|
|
21
|
+
'v-dt-focusgroup requires an accessible name via "aria-label" or "aria-labelledby" ' +
|
|
22
|
+
'so screen readers can identify the widget.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
create (context) {
|
|
27
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
28
|
+
return sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
29
|
+
VAttribute (node) {
|
|
30
|
+
if (!node.directive) return;
|
|
31
|
+
if (node.key.name.name !== 'dt-focusgroup') return;
|
|
32
|
+
|
|
33
|
+
const element = node.parent;
|
|
34
|
+
const hasLabel = element.attributes.some(
|
|
35
|
+
attr =>
|
|
36
|
+
(!attr.directive && (
|
|
37
|
+
attr.key.name === 'aria-label' ||
|
|
38
|
+
attr.key.name === 'aria-labelledby'
|
|
39
|
+
)) ||
|
|
40
|
+
(attr.directive && attr.key.name.name === 'bind' && (
|
|
41
|
+
attr.key.argument?.name === 'aria-label' ||
|
|
42
|
+
attr.key.argument?.name === 'aria-labelledby'
|
|
43
|
+
)),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (!hasLabel) {
|
|
47
|
+
context.report({ node, messageId: 'missingLabel' });
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Warns when v-dt-focusgroup is used without a role attribute on the same element.
|
|
3
|
+
* @author Dialtone
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description:
|
|
12
|
+
'Warns when v-dt-focusgroup is used on an element without a role attribute. ' +
|
|
13
|
+
'Screen readers need a role to announce the widget correctly.',
|
|
14
|
+
recommended: false,
|
|
15
|
+
url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/focusgroup-requires-role.md',
|
|
16
|
+
},
|
|
17
|
+
fixable: null,
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
missingRole:
|
|
21
|
+
'v-dt-focusgroup requires a "role" attribute (e.g. toolbar, tablist, listbox, radiogroup, menu) ' +
|
|
22
|
+
'so screen readers can announce the widget correctly.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
create (context) {
|
|
27
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
28
|
+
return sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
29
|
+
VAttribute (node) {
|
|
30
|
+
if (!node.directive) return;
|
|
31
|
+
if (node.key.name.name !== 'dt-focusgroup') return;
|
|
32
|
+
|
|
33
|
+
const element = node.parent;
|
|
34
|
+
const hasRole = element.attributes.some(
|
|
35
|
+
attr =>
|
|
36
|
+
(!attr.directive && attr.key.name === 'role') ||
|
|
37
|
+
(attr.directive && attr.key.name.name === 'bind' && attr.key.argument?.name === 'role'),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (!hasRole) {
|
|
41
|
+
context.report({ node, messageId: 'missingRole' });
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|