@db-ux/core-eslint-plugin 4.4.2-eslint-plugin-28ea614 → 4.4.2-eslint-plugin2-696cb23

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.
Files changed (52) hide show
  1. package/README.md +89 -0
  2. package/build/index.d.ts +308 -55
  3. package/build/index.js +17 -14
  4. package/build/rules/accordion/accordion-item-headline-required.d.ts +16 -0
  5. package/build/rules/accordion/accordion-item-headline-required.js +61 -0
  6. package/build/rules/accordion/no-nested-accordion.d.ts +14 -3
  7. package/build/rules/accordion/no-nested-accordion.js +40 -23
  8. package/build/rules/badge/badge-corner-placement-rules.d.ts +16 -3
  9. package/build/rules/badge/badge-corner-placement-rules.js +96 -58
  10. package/build/rules/badge/badge-no-inline-in-interactive.d.ts +15 -3
  11. package/build/rules/badge/badge-no-inline-in-interactive.js +102 -40
  12. package/build/rules/button/button-no-text-requires-tooltip.d.ts +16 -3
  13. package/build/rules/button/button-no-text-requires-tooltip.js +85 -35
  14. package/build/rules/button/button-single-icon-attribute.d.ts +14 -3
  15. package/build/rules/button/button-single-icon-attribute.js +37 -23
  16. package/build/rules/button/button-type-required.d.ts +15 -3
  17. package/build/rules/button/button-type-required.js +45 -32
  18. package/build/rules/close-button/close-button-text-required.d.ts +14 -3
  19. package/build/rules/close-button/close-button-text-required.js +21 -23
  20. package/build/rules/content/text-or-children-required.d.ts +14 -3
  21. package/build/rules/content/text-or-children-required.js +26 -28
  22. package/build/rules/form/form-label-required.d.ts +14 -3
  23. package/build/rules/form/form-label-required.js +23 -25
  24. package/build/rules/form/form-validation-message-required.d.ts +14 -3
  25. package/build/rules/form/form-validation-message-required.js +65 -67
  26. package/build/rules/header/header-burger-menu-label-required.d.ts +14 -3
  27. package/build/rules/header/header-burger-menu-label-required.js +31 -20
  28. package/build/rules/icon/prefer-icon-attribute.d.ts +15 -3
  29. package/build/rules/icon/prefer-icon-attribute.js +48 -33
  30. package/build/rules/input/input-file-type-validation.d.ts +16 -3
  31. package/build/rules/input/input-file-type-validation.js +36 -38
  32. package/build/rules/input/input-type-required.d.ts +15 -3
  33. package/build/rules/input/input-type-required.js +41 -27
  34. package/build/rules/link/link-external-security.d.ts +17 -3
  35. package/build/rules/link/link-external-security.js +148 -32
  36. package/build/rules/navigation/navigation-item-back-button-text-required.d.ts +14 -3
  37. package/build/rules/navigation/navigation-item-back-button-text-required.js +31 -20
  38. package/build/rules/select/custom-select-tags-remove-text-required.d.ts +14 -3
  39. package/build/rules/select/custom-select-tags-remove-text-required.js +21 -23
  40. package/build/rules/select/select-requires-options.d.ts +14 -3
  41. package/build/rules/select/select-requires-options.js +23 -22
  42. package/build/rules/tag/tag-removable-remove-button-required.d.ts +14 -3
  43. package/build/rules/tag/tag-removable-remove-button-required.js +37 -23
  44. package/build/rules/tooltip/no-interactive-tooltip-content.d.ts +13 -3
  45. package/build/rules/tooltip/no-interactive-tooltip-content.js +28 -26
  46. package/build/rules/tooltip/tooltip-requires-interactive-parent.d.ts +13 -3
  47. package/build/rules/tooltip/tooltip-requires-interactive-parent.js +30 -30
  48. package/build/shared/constants.d.ts +58 -0
  49. package/build/shared/constants.js +98 -0
  50. package/build/shared/utils.d.ts +53 -4
  51. package/build/shared/utils.js +145 -28
  52. package/package.json +8 -3
@@ -1,37 +1,54 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
2
- import { isDBComponent } from '../../shared/utils.js';
3
- const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#${name}`);
4
- export default createRule({
5
- name: 'no-nested-accordion',
1
+ import { createAngularVisitors, defineTemplateBodyVisitor, isDBComponent } from '../../shared/utils.js';
2
+ import { COMPONENTS, MESSAGES, MESSAGE_IDS } from '../../shared/constants.js';
3
+ export default {
6
4
  meta: {
7
5
  type: 'problem',
8
6
  docs: {
9
- description: 'Prevent nesting DBAccordion components'
7
+ description: 'Prevent nesting DBAccordion components',
8
+ url: 'https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#no-nested-accordion'
10
9
  },
11
10
  messages: {
12
- noNested: 'DBAccordion must not be nested inside another DBAccordion as it confuses users'
11
+ [MESSAGE_IDS.ACCORDION_NO_NESTED]: MESSAGES.ACCORDION_NO_NESTED
13
12
  },
14
13
  schema: []
15
14
  },
16
- defaultOptions: [],
17
15
  create(context) {
18
- return {
19
- JSXElement(node) {
20
- if (!isDBComponent(node.openingElement, 'DBAccordion'))
16
+ const angularHandler = (node, parserServices) => {
17
+ let parent = node.parent;
18
+ while (parent) {
19
+ if (parent.type === 'Element' && isDBComponent(parent, COMPONENTS.DBAccordion)) {
20
+ const loc = parserServices.convertNodeSourceSpanToLoc(node.sourceSpan);
21
+ context.report({
22
+ loc,
23
+ messageId: MESSAGE_IDS.ACCORDION_NO_NESTED
24
+ });
21
25
  return;
22
- let parent = node.parent;
23
- while (parent) {
24
- if (parent.type === 'JSXElement' &&
25
- isDBComponent(parent.openingElement, 'DBAccordion')) {
26
- context.report({
27
- node: node.openingElement,
28
- messageId: 'noNested'
29
- });
30
- return;
31
- }
32
- parent = parent.parent;
33
26
  }
27
+ parent = parent.parent;
34
28
  }
35
29
  };
30
+ const angularVisitors = createAngularVisitors(context, COMPONENTS.DBAccordion, angularHandler);
31
+ if (angularVisitors)
32
+ return angularVisitors;
33
+ const checkAccordion = (node) => {
34
+ const openingElement = node.openingElement || node;
35
+ if (!isDBComponent(openingElement, COMPONENTS.DBAccordion))
36
+ return;
37
+ let parent = node.parent;
38
+ while (parent) {
39
+ const parentOpening = parent.openingElement || parent;
40
+ if ((parent.type === 'JSXElement' ||
41
+ parent.type === 'VElement') &&
42
+ isDBComponent(parentOpening, COMPONENTS.DBAccordion)) {
43
+ context.report({
44
+ node: openingElement,
45
+ messageId: MESSAGE_IDS.ACCORDION_NO_NESTED
46
+ });
47
+ return;
48
+ }
49
+ parent = parent.parent;
50
+ }
51
+ };
52
+ return defineTemplateBodyVisitor(context, { VElement: checkAccordion, Element: checkAccordion }, { JSXElement: checkAccordion });
36
53
  }
37
- });
54
+ };
@@ -1,5 +1,18 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
2
- declare const _default: ESLintUtils.RuleModule<"textTooLong" | "missingLabel", [], unknown, ESLintUtils.RuleListener> & {
3
- name: string;
1
+ import { MESSAGE_IDS } from '../../shared/constants.js';
2
+ declare const _default: {
3
+ meta: {
4
+ type: string;
5
+ docs: {
6
+ description: string;
7
+ url: string;
8
+ };
9
+ fixable: string;
10
+ messages: {
11
+ [MESSAGE_IDS.BADGE_CORNER_TEXT_TOO_LONG]: string;
12
+ [MESSAGE_IDS.BADGE_CORNER_MISSING_LABEL]: string;
13
+ };
14
+ schema: never[];
15
+ };
16
+ create(context: any): any;
4
17
  };
5
18
  export default _default;
@@ -1,76 +1,114 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
2
- import { getAttributeValue, isDBComponent } from '../../shared/utils.js';
3
- const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#${name}`);
1
+ import { COMPONENTS, MESSAGES, MESSAGE_IDS } from '../../shared/constants.js';
2
+ import { createAngularFix, createAngularVisitors, defineTemplateBodyVisitor, getAttributeValue, isDBComponent } from '../../shared/utils.js';
4
3
  function getTextContent(node) {
5
- const textChild = node.children.find((child) => child.type === 'JSXText');
6
- return textChild && textChild.type === 'JSXText'
7
- ? textChild.value.trim()
8
- : null;
4
+ if (node.children) {
5
+ for (const child of node.children) {
6
+ if (child.type === 'JSXText') {
7
+ return child.value.trim();
8
+ }
9
+ if (child.type === 'Text') {
10
+ return child.value?.trim() || null;
11
+ }
12
+ if (child.type === 'VText') {
13
+ return child.value?.trim() || null;
14
+ }
15
+ }
16
+ }
17
+ return null;
9
18
  }
10
- export default createRule({
11
- name: 'badge-corner-placement-rules',
19
+ export default {
12
20
  meta: {
13
21
  type: 'problem',
14
22
  docs: {
15
- description: 'Ensure DBBadge with corner placement has max 3 characters and label'
23
+ description: 'Ensure DBBadge with corner placement has max 3 characters and label',
24
+ url: 'https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#badge-corner-placement-rules'
16
25
  },
17
26
  fixable: 'code',
18
27
  messages: {
19
- textTooLong: 'DBBadge with corner placement must have max 3 characters in text/children',
20
- missingLabel: 'DBBadge with corner placement must have a label attribute for accessibility'
28
+ [MESSAGE_IDS.BADGE_CORNER_TEXT_TOO_LONG]: MESSAGES.BADGE_CORNER_TEXT_TOO_LONG,
29
+ [MESSAGE_IDS.BADGE_CORNER_MISSING_LABEL]: MESSAGES.BADGE_CORNER_MISSING_LABEL
21
30
  },
22
31
  schema: []
23
32
  },
24
- defaultOptions: [],
25
33
  create(context) {
26
- return {
27
- JSXElement(node) {
28
- if (!isDBComponent(node.openingElement, 'DBBadge'))
29
- return;
30
- const placement = getAttributeValue(node.openingElement, 'placement');
31
- if (!placement || placement === 'inline')
32
- return;
33
- const text = getAttributeValue(node.openingElement, 'text');
34
- const children = getTextContent(node);
35
- const content = (typeof text === 'string' ? text : children) || '';
36
- const label = getAttributeValue(node.openingElement, 'label');
37
- if (content.length > 3) {
38
- context.report({
39
- node: node.openingElement,
40
- messageId: 'textTooLong',
41
- fix(fixer) {
42
- const fixes = [];
43
- const shortText = content.slice(0, 3);
44
- if (text && typeof text === 'string') {
45
- const textAttr = node.openingElement.attributes.find((a) => a.type === 'JSXAttribute' &&
46
- a.name.name === 'text');
47
- if (textAttr) {
48
- fixes.push(fixer.replaceText(textAttr, `text="${shortText}" label="${content}"`));
49
- }
34
+ const angularHandler = (node, parserServices) => {
35
+ const placement = getAttributeValue(node, 'placement');
36
+ if (!placement || placement === 'inline')
37
+ return;
38
+ const text = getAttributeValue(node, 'text');
39
+ const children = getTextContent(node);
40
+ const content = (typeof text === 'string' ? text : children) || '';
41
+ const label = getAttributeValue(node, 'label');
42
+ const loc = parserServices.convertNodeSourceSpanToLoc(node.sourceSpan);
43
+ if (content.length > 3) {
44
+ context.report({
45
+ loc,
46
+ messageId: MESSAGE_IDS.BADGE_CORNER_TEXT_TOO_LONG
47
+ });
48
+ }
49
+ if (!label) {
50
+ context.report({
51
+ loc,
52
+ messageId: MESSAGE_IDS.BADGE_CORNER_MISSING_LABEL,
53
+ fix(fixer) {
54
+ const fixData = createAngularFix(context, node, ` label="${content || 'Badge'}"`);
55
+ if (!fixData)
56
+ return null;
57
+ return fixer.insertTextBeforeRange([fixData.insertPos, fixData.insertPos], fixData.attributeText);
58
+ }
59
+ });
60
+ }
61
+ };
62
+ const angularVisitors = createAngularVisitors(context, COMPONENTS.DBBadge, angularHandler);
63
+ if (angularVisitors)
64
+ return angularVisitors;
65
+ const checkBadge = (node) => {
66
+ const openingElement = node.openingElement || node;
67
+ if (!isDBComponent(openingElement, COMPONENTS.DBBadge))
68
+ return;
69
+ const placement = getAttributeValue(openingElement, 'placement');
70
+ if (!placement || placement === 'inline')
71
+ return;
72
+ const text = getAttributeValue(openingElement, 'text');
73
+ const children = getTextContent(node);
74
+ const content = (typeof text === 'string' ? text : children) || '';
75
+ const label = getAttributeValue(openingElement, 'label');
76
+ if (content.length > 3) {
77
+ context.report({
78
+ node: openingElement,
79
+ messageId: MESSAGE_IDS.BADGE_CORNER_TEXT_TOO_LONG,
80
+ fix(fixer) {
81
+ const fixes = [];
82
+ const shortText = content.slice(0, 3);
83
+ if (text && typeof text === 'string') {
84
+ const textAttr = openingElement.attributes.find((a) => a.type === 'JSXAttribute' &&
85
+ a.name.name === 'text');
86
+ if (textAttr) {
87
+ fixes.push(fixer.replaceText(textAttr, `text="${shortText}" label="${content}"`));
50
88
  }
51
- else if (children) {
52
- const textChild = node.children.find((c) => c.type === 'JSXText');
53
- if (textChild) {
54
- fixes.push(fixer.replaceText(textChild, shortText));
55
- const lastAttr = node.openingElement.attributes[node.openingElement.attributes
56
- .length - 1];
57
- const insertPos = lastAttr
58
- ? lastAttr.range[1]
59
- : node.openingElement.name.range[1];
60
- fixes.push(fixer.insertTextAfterRange([insertPos, insertPos], ` label="${content}"`));
61
- }
89
+ }
90
+ else if (children) {
91
+ const textChild = node.children.find((c) => c.type === 'JSXText');
92
+ if (textChild) {
93
+ fixes.push(fixer.replaceText(textChild, shortText));
94
+ const lastAttr = openingElement.attributes[openingElement.attributes.length - 1];
95
+ const insertPos = lastAttr
96
+ ? lastAttr.range[1]
97
+ : openingElement.name.range[1];
98
+ fixes.push(fixer.insertTextAfterRange([insertPos, insertPos], ` label="${content}"`));
62
99
  }
63
- return fixes;
64
100
  }
65
- });
66
- }
67
- if (!label) {
68
- context.report({
69
- node: node.openingElement,
70
- messageId: 'missingLabel'
71
- });
72
- }
101
+ return fixes;
102
+ }
103
+ });
104
+ }
105
+ if (!label) {
106
+ context.report({
107
+ node: openingElement,
108
+ messageId: MESSAGE_IDS.BADGE_CORNER_MISSING_LABEL
109
+ });
73
110
  }
74
111
  };
112
+ return defineTemplateBodyVisitor(context, { VElement: checkBadge, Element: checkBadge }, { JSXElement: checkBadge });
75
113
  }
76
- });
114
+ };
@@ -1,5 +1,17 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
2
- declare const _default: ESLintUtils.RuleModule<"noInline", [], unknown, ESLintUtils.RuleListener> & {
3
- name: string;
1
+ import { MESSAGE_IDS } from '../../shared/constants.js';
2
+ declare const _default: {
3
+ meta: {
4
+ type: string;
5
+ docs: {
6
+ description: string;
7
+ url: string;
8
+ };
9
+ fixable: string;
10
+ messages: {
11
+ [MESSAGE_IDS.BADGE_NO_INLINE_IN_INTERACTIVE]: string;
12
+ };
13
+ schema: never[];
14
+ };
15
+ create(context: any): any;
4
16
  };
5
17
  export default _default;
@@ -1,67 +1,129 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
2
- import { getAttributeValue, isDBComponent } from '../../shared/utils.js';
3
- const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#${name}`);
1
+ import { COMPONENTS, MESSAGES, MESSAGE_IDS } from '../../shared/constants.js';
2
+ import { createAngularFix, createAngularVisitors, defineTemplateBodyVisitor, getAttributeValue, isDBComponent } from '../../shared/utils.js';
4
3
  const INTERACTIVE_PARENTS = ['DBButton', 'DBLink', 'button', 'a'];
5
- export default createRule({
6
- name: 'badge-no-inline-in-interactive',
4
+ export default {
7
5
  meta: {
8
6
  type: 'problem',
9
7
  docs: {
10
- description: 'Prevent inline placement for DBBadge inside interactive elements'
8
+ description: 'Prevent inline placement for DBBadge inside interactive elements',
9
+ url: 'https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#badge-no-inline-in-interactive'
11
10
  },
12
11
  fixable: 'code',
13
12
  messages: {
14
- noInline: 'DBBadge inside {{parent}} cannot have placement="inline". Use corner placement instead'
13
+ [MESSAGE_IDS.BADGE_NO_INLINE_IN_INTERACTIVE]: MESSAGES.BADGE_NO_INLINE_IN_INTERACTIVE
15
14
  },
16
15
  schema: []
17
16
  },
18
- defaultOptions: [],
19
17
  create(context) {
20
- return {
21
- JSXElement(node) {
22
- if (!isDBComponent(node.openingElement, 'DBBadge'))
23
- return;
24
- const placement = getAttributeValue(node.openingElement, 'placement');
25
- if (placement && placement !== 'inline')
26
- return;
27
- let parent = node.parent;
28
- while (parent) {
29
- if (parent.type === 'JSXElement') {
30
- const parentName = parent.openingElement.name;
31
- if (parentName.type === 'JSXIdentifier') {
32
- const name = parentName.name;
33
- const matchedParent = INTERACTIVE_PARENTS.find((p) => name === p ||
34
- name ===
35
- p.toLowerCase().replace('db', 'db-'));
36
- if (matchedParent) {
37
- context.report({
38
- node: node.openingElement,
39
- messageId: 'noInline',
40
- data: { parent: matchedParent },
41
- fix(fixer) {
42
- const placementAttr = node.openingElement.attributes.find((a) => a.type === 'JSXAttribute' &&
18
+ const angularHandler = (node, parserServices) => {
19
+ const placement = getAttributeValue(node, 'placement');
20
+ if (placement && placement !== 'inline')
21
+ return;
22
+ let parent = node.parent;
23
+ while (parent) {
24
+ if (parent.type === 'Element') {
25
+ const matchedParent = INTERACTIVE_PARENTS.find((p) => parent.name === p ||
26
+ parent.name === p.toLowerCase().replace('db', 'db-'));
27
+ if (matchedParent) {
28
+ const loc = parserServices.convertNodeSourceSpanToLoc(node.sourceSpan);
29
+ context.report({
30
+ loc,
31
+ messageId: MESSAGE_IDS.BADGE_NO_INLINE_IN_INTERACTIVE,
32
+ data: { parent: matchedParent },
33
+ fix(fixer) {
34
+ const fixData = createAngularFix(context, node, ' placement="corner-top-right"');
35
+ if (!fixData)
36
+ return null;
37
+ return fixer.insertTextBeforeRange([fixData.insertPos, fixData.insertPos], fixData.attributeText);
38
+ }
39
+ });
40
+ return;
41
+ }
42
+ }
43
+ parent = parent.parent;
44
+ }
45
+ };
46
+ const angularVisitors = createAngularVisitors(context, COMPONENTS.DBBadge, angularHandler);
47
+ if (angularVisitors)
48
+ return angularVisitors;
49
+ const checkBadge = (node) => {
50
+ const openingElement = node.openingElement || node;
51
+ if (!isDBComponent(openingElement, COMPONENTS.DBBadge))
52
+ return;
53
+ const placement = getAttributeValue(openingElement, 'placement');
54
+ if (placement && placement !== 'inline')
55
+ return;
56
+ let parent = node.parent;
57
+ while (parent) {
58
+ if (parent.type === 'JSXElement' ||
59
+ parent.type === 'VElement') {
60
+ const parentOpening = parent.openingElement || parent;
61
+ const parentName = parentOpening.name || parentOpening.rawName;
62
+ const name = typeof parentName === 'string'
63
+ ? parentName
64
+ : parentName.type === 'JSXIdentifier'
65
+ ? parentName.name
66
+ : null;
67
+ if (name) {
68
+ const matchedParent = INTERACTIVE_PARENTS.find((p) => name === p ||
69
+ name === p.toLowerCase().replace('db', 'db-'));
70
+ if (matchedParent) {
71
+ context.report({
72
+ node: openingElement,
73
+ messageId: MESSAGE_IDS.BADGE_NO_INLINE_IN_INTERACTIVE,
74
+ data: { parent: matchedParent },
75
+ fix(fixer) {
76
+ if (node.openingElement) {
77
+ // JSX
78
+ const placementAttr = openingElement.attributes.find((a) => a.type === 'JSXAttribute' &&
43
79
  a.name.name === 'placement');
44
80
  if (placementAttr) {
45
81
  return fixer.replaceText(placementAttr, 'placement="corner-top-right"');
46
82
  }
47
83
  else {
48
- const lastAttr = node.openingElement.attributes[node.openingElement
49
- .attributes.length - 1];
84
+ const lastAttr = openingElement.attributes[openingElement.attributes
85
+ .length - 1];
50
86
  const insertPos = lastAttr
51
87
  ? lastAttr.range[1]
52
- : node.openingElement.name
53
- .range[1];
88
+ : openingElement.name.range[1];
54
89
  return fixer.insertTextAfterRange([insertPos, insertPos], ' placement="corner-top-right"');
55
90
  }
56
91
  }
57
- });
58
- return;
59
- }
92
+ else {
93
+ // Vue
94
+ const placementAttr = openingElement.startTag.attributes.find((a) => a.key.name === 'placement');
95
+ if (placementAttr) {
96
+ return fixer.replaceText(placementAttr, 'placement="corner-top-right"');
97
+ }
98
+ else {
99
+ const attrs = openingElement.startTag
100
+ .attributes;
101
+ if (attrs.length > 0) {
102
+ const lastAttr = attrs[attrs.length - 1];
103
+ return fixer.insertTextAfterRange([
104
+ lastAttr.range[1],
105
+ lastAttr.range[1]
106
+ ], ' placement="corner-top-right"');
107
+ }
108
+ else {
109
+ const insertPos = openingElement.startTag
110
+ .range[0] +
111
+ 1 +
112
+ openingElement.rawName
113
+ .length;
114
+ return fixer.insertTextAfterRange([insertPos, insertPos], ' placement="corner-top-right"');
115
+ }
116
+ }
117
+ }
118
+ }
119
+ });
120
+ return;
60
121
  }
61
122
  }
62
- parent = parent.parent;
63
123
  }
124
+ parent = parent.parent;
64
125
  }
65
126
  };
127
+ return defineTemplateBodyVisitor(context, { VElement: checkBadge, Element: checkBadge }, { JSXElement: checkBadge });
66
128
  }
67
- });
129
+ };
@@ -1,5 +1,18 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
2
- declare const _default: ESLintUtils.RuleModule<"missingIcon" | "missingTooltip", [], unknown, ESLintUtils.RuleListener> & {
3
- name: string;
1
+ import { MESSAGE_IDS } from '../../shared/constants.js';
2
+ declare const _default: {
3
+ meta: {
4
+ type: string;
5
+ docs: {
6
+ description: string;
7
+ url: string;
8
+ };
9
+ fixable: string;
10
+ messages: {
11
+ [MESSAGE_IDS.BUTTON_NO_TEXT_MISSING_ICON]: string;
12
+ [MESSAGE_IDS.BUTTON_NO_TEXT_MISSING_TOOLTIP]: string;
13
+ };
14
+ schema: never[];
15
+ };
16
+ create(context: any): any;
4
17
  };
5
18
  export default _default;
@@ -1,45 +1,81 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
2
- import { getAttributeValue, hasChildOfType, isDBComponent } from '../../shared/utils.js';
3
- const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#${name}`);
4
- export default createRule({
5
- name: 'button-no-text-requires-tooltip',
1
+ import { COMPONENTS, MESSAGES, MESSAGE_IDS } from '../../shared/constants.js';
2
+ import { createAngularVisitors, defineTemplateBodyVisitor, getAttributeValue, hasChildOfType, isDBComponent } from '../../shared/utils.js';
3
+ export default {
6
4
  meta: {
7
5
  type: 'problem',
8
6
  docs: {
9
- description: 'Ensure DBButton with noText has icon and DBTooltip child'
7
+ description: 'Ensure DBButton with noText has icon and DBTooltip child',
8
+ url: 'https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#button-no-text-requires-tooltip'
10
9
  },
11
10
  fixable: 'code',
12
11
  messages: {
13
- missingIcon: 'DBButton with noText must have an icon prop',
14
- missingTooltip: 'DBButton with noText must have a DBTooltip child for accessibility'
12
+ [MESSAGE_IDS.BUTTON_NO_TEXT_MISSING_ICON]: MESSAGES.BUTTON_NO_TEXT_MISSING_ICON,
13
+ [MESSAGE_IDS.BUTTON_NO_TEXT_MISSING_TOOLTIP]: MESSAGES.BUTTON_NO_TEXT_MISSING_TOOLTIP
15
14
  },
16
15
  schema: []
17
16
  },
18
- defaultOptions: [],
19
17
  create(context) {
20
- return {
21
- JSXElement(node) {
22
- const openingElement = node.openingElement;
23
- if (!isDBComponent(openingElement, 'DBButton'))
24
- return;
25
- const noText = getAttributeValue(openingElement, 'noText');
26
- if (!noText)
27
- return;
28
- const icon = getAttributeValue(openingElement, 'icon') ||
29
- getAttributeValue(openingElement, 'iconLeading') ||
30
- getAttributeValue(openingElement, 'iconTrailing');
31
- if (!icon) {
32
- context.report({
33
- node: openingElement,
34
- messageId: 'missingIcon'
35
- });
36
- }
37
- const hasTooltip = hasChildOfType(node, 'DBTooltip');
38
- if (!hasTooltip) {
39
- context.report({
40
- node: openingElement,
41
- messageId: 'missingTooltip',
42
- fix(fixer) {
18
+ const angularHandler = (node, parserServices) => {
19
+ const noText = getAttributeValue(node, 'noText');
20
+ if (!noText)
21
+ return;
22
+ const icon = getAttributeValue(node, 'icon') ||
23
+ getAttributeValue(node, 'iconLeading') ||
24
+ getAttributeValue(node, 'iconTrailing');
25
+ const loc = parserServices.convertNodeSourceSpanToLoc(node.sourceSpan);
26
+ if (!icon) {
27
+ context.report({
28
+ loc,
29
+ messageId: MESSAGE_IDS.BUTTON_NO_TEXT_MISSING_ICON
30
+ });
31
+ }
32
+ const hasTooltip = hasChildOfType(node, COMPONENTS.DBTooltip);
33
+ if (!hasTooltip) {
34
+ context.report({
35
+ loc,
36
+ messageId: MESSAGE_IDS.BUTTON_NO_TEXT_MISSING_TOOLTIP,
37
+ fix(fixer) {
38
+ const sourceCode = context.sourceCode || context.getSourceCode();
39
+ const text = sourceCode.getText();
40
+ const startOffset = node.sourceSpan.start.offset;
41
+ const endOffset = node.sourceSpan.end.offset;
42
+ const tagText = text.substring(startOffset, endOffset);
43
+ const closeTagIndex = tagText.lastIndexOf('</db-button>');
44
+ if (closeTagIndex === -1)
45
+ return null;
46
+ const insertPos = startOffset + closeTagIndex;
47
+ return fixer.insertTextBeforeRange([insertPos, insertPos], '\n <db-tooltip>Describe action</db-tooltip>');
48
+ }
49
+ });
50
+ }
51
+ };
52
+ const angularVisitors = createAngularVisitors(context, COMPONENTS.DBButton, angularHandler);
53
+ if (angularVisitors)
54
+ return angularVisitors;
55
+ const checkButton = (node) => {
56
+ const openingElement = node.openingElement || node;
57
+ if (!isDBComponent(openingElement, COMPONENTS.DBButton))
58
+ return;
59
+ const noText = getAttributeValue(openingElement, 'noText');
60
+ if (!noText)
61
+ return;
62
+ const icon = getAttributeValue(openingElement, 'icon') ||
63
+ getAttributeValue(openingElement, 'iconLeading') ||
64
+ getAttributeValue(openingElement, 'iconTrailing');
65
+ if (!icon) {
66
+ context.report({
67
+ node: openingElement,
68
+ messageId: MESSAGE_IDS.BUTTON_NO_TEXT_MISSING_ICON
69
+ });
70
+ }
71
+ const hasTooltip = hasChildOfType(node, COMPONENTS.DBTooltip);
72
+ if (!hasTooltip) {
73
+ context.report({
74
+ node: openingElement,
75
+ messageId: MESSAGE_IDS.BUTTON_NO_TEXT_MISSING_TOOLTIP,
76
+ fix(fixer) {
77
+ if (node.openingElement) {
78
+ // JSX
43
79
  const closingTag = node.closingElement;
44
80
  if (!closingTag)
45
81
  return null;
@@ -51,9 +87,23 @@ export default createRule({
51
87
  : 'DBTooltip';
52
88
  return fixer.insertTextBefore(closingTag, `\n <${tooltipName}>Describe action</${tooltipName}>`);
53
89
  }
54
- });
55
- }
90
+ else {
91
+ // Vue
92
+ if (!node.endTag || !node.startTag?.range)
93
+ return null;
94
+ const componentName = openingElement.rawName;
95
+ const tooltipName = componentName.includes('-')
96
+ ? 'db-tooltip'
97
+ : 'DBTooltip';
98
+ return fixer.insertTextAfterRange([
99
+ node.startTag.range[1],
100
+ node.startTag.range[1]
101
+ ], `\n <${tooltipName}>Describe action</${tooltipName}>`);
102
+ }
103
+ }
104
+ });
56
105
  }
57
106
  };
107
+ return defineTemplateBodyVisitor(context, { VElement: checkButton, Element: checkButton }, { JSXElement: checkButton });
58
108
  }
59
- });
109
+ };