@angular-eslint/eslint-plugin-template 22.3.1-alpha.0 → 22.3.1-alpha.2

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.
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ declare const _default: {
22
22
  "button-has-type": import("@typescript-eslint/utils/ts-eslint").RuleModule<import("./rules/button-has-type").MessageIds, import("./rules/button-has-type").Options, import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
23
23
  name: string;
24
24
  };
25
- "click-events-have-key-events": import("@typescript-eslint/utils/ts-eslint").RuleModule<"clickEventsHaveKeyEvents", import("./rules/click-events-have-key-events").Options, import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
25
+ "click-events-have-key-events": import("@typescript-eslint/utils/ts-eslint").RuleModule<import("./rules/click-events-have-key-events").MessageIds, import("./rules/click-events-have-key-events").Options, import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
26
26
  name: string;
27
27
  };
28
28
  "conditional-complexity": import("@typescript-eslint/utils/ts-eslint").RuleModule<"conditionalComplexity", import("./rules/conditional-complexity").Options, import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
@@ -1 +1 @@
1
- {"version":3,"file":"button-has-type.d.ts","sourceRoot":"","sources":["../../src/rules/button-has-type.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC1C;CACF,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,aAAa,CAAC;AACvD,eAAO,MAAM,SAAS,oBAAoB,CAAC;AAI3C,eAAO,MAAM,qBAAqB,SAAS,CAAC;;;;AAW5C,wBA2DG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
1
+ {"version":3,"file":"button-has-type.d.ts","sourceRoot":"","sources":["../../src/rules/button-has-type.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC1C;CACF,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,aAAa,CAAC;AACvD,eAAO,MAAM,SAAS,oBAAoB,CAAC;AAI3C,eAAO,MAAM,qBAAqB,SAAS,CAAC;;;;AAW5C,wBA+DG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
@@ -4,6 +4,7 @@ exports.RULE_DOCS_EXTENSION = exports.INVALID_TYPE_DATA_KEY = exports.RULE_NAME
4
4
  const bundled_angular_compiler_1 = require("@angular-eslint/bundled-angular-compiler");
5
5
  const utils_1 = require("@angular-eslint/utils");
6
6
  const create_eslint_rule_1 = require("../utils/create-eslint-rule");
7
+ const has_ignored_directive_1 = require("../utils/has-ignored-directive");
7
8
  exports.RULE_NAME = 'button-has-type';
8
9
  const DEFAULT_OPTIONS = {
9
10
  ignoreWithDirectives: [],
@@ -27,6 +28,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
27
28
  type: 'array',
28
29
  items: { type: 'string' },
29
30
  uniqueItems: true,
31
+ description: 'Directive names that, when present on the element, cause it to be ignored. Entries wrapped in slashes, e.g. `/^tui/`, are treated as regular expressions.',
30
32
  default: DEFAULT_OPTIONS.ignoreWithDirectives,
31
33
  },
32
34
  },
@@ -41,10 +43,11 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
41
43
  },
42
44
  create(context, [{ ignoreWithDirectives }]) {
43
45
  const parserServices = (0, utils_1.getTemplateParserServices)(context);
46
+ const hasIgnoredDirective = (0, has_ignored_directive_1.createIgnoredDirectiveMatcher)(ignoreWithDirectives);
44
47
  return {
45
48
  [`Element[name=/^(button)$/i]`](element) {
46
49
  if (!isTypeAttributePresentInElement(element)) {
47
- if (!isIgnored(ignoreWithDirectives, element)) {
50
+ if (!hasIgnoredDirective(element)) {
48
51
  context.report({
49
52
  loc: parserServices.convertNodeSourceSpanToLoc(element.sourceSpan),
50
53
  messageId: 'missingType',
@@ -71,21 +74,6 @@ exports.RULE_DOCS_EXTENSION = {
71
74
  function isTypeAttributePresentInElement({ inputs, attributes, }) {
72
75
  return [...inputs, ...attributes].some(({ name }) => name === TYPE_ATTRIBUTE_NAME);
73
76
  }
74
- function isIgnored(ignoreWithDirectives, { inputs, attributes }) {
75
- if (ignoreWithDirectives && ignoreWithDirectives.length > 0) {
76
- for (const input of inputs) {
77
- if (ignoreWithDirectives.includes(input.name)) {
78
- return true;
79
- }
80
- }
81
- for (const attribute of attributes) {
82
- if (ignoreWithDirectives.includes(attribute.name)) {
83
- return true;
84
- }
85
- }
86
- }
87
- return false;
88
- }
89
77
  function getInvalidButtonTypeIfPresent(element) {
90
78
  const invalidTextAttribute = element.attributes.find(({ name, value }) => name === TYPE_ATTRIBUTE_NAME && !VALID_BUTTON_TYPES.includes(value));
91
79
  if (invalidTextAttribute) {
@@ -1,11 +1,13 @@
1
1
  export type Options = [
2
2
  {
3
3
  readonly ignoreWithDirectives?: string[];
4
+ readonly requireKeyCode?: boolean;
5
+ readonly allowedKeyCodes?: string[];
4
6
  }
5
7
  ];
6
- export type MessageIds = 'clickEventsHaveKeyEvents';
8
+ export type MessageIds = 'clickEventsHaveKeyEvents' | 'clickEventsHaveKeyCode' | 'clickEventsHaveAllowedKeyCode';
7
9
  export declare const RULE_NAME = "click-events-have-key-events";
8
- declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"clickEventsHaveKeyEvents", Options, import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
10
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, Options, import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
9
11
  name: string;
10
12
  };
11
13
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"click-events-have-key-events.d.ts","sourceRoot":"","sources":["../../src/rules/click-events-have-key-events.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC1C;CACF,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,0BAA0B,CAAC;AACpD,eAAO,MAAM,SAAS,iCAAiC,CAAC;;;;AAKxD,wBA8EG;AAsBH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
1
+ {"version":3,"file":"click-events-have-key-events.d.ts","sourceRoot":"","sources":["../../src/rules/click-events-have-key-events.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;QACzC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;QAClC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;KACrC;CACF,CAAC;AACF,MAAM,MAAM,UAAU,GAClB,0BAA0B,GAC1B,wBAAwB,GACxB,+BAA+B,CAAC;AACpC,eAAO,MAAM,SAAS,iCAAiC,CAAC;;;;AASxD,wBAmIG;AAqBH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RULE_DOCS_EXTENSION = exports.RULE_NAME = void 0;
4
4
  const utils_1 = require("@angular-eslint/utils");
5
5
  const create_eslint_rule_1 = require("../utils/create-eslint-rule");
6
+ const has_ignored_directive_1 = require("../utils/has-ignored-directive");
6
7
  const get_dom_elements_1 = require("../utils/get-dom-elements");
7
8
  const is_hidden_from_screen_reader_1 = require("../utils/is-hidden-from-screen-reader");
8
9
  const is_interactive_element_1 = require("../utils/is-interactive-element");
@@ -11,7 +12,10 @@ const get_attribute_value_1 = require("../utils/get-attribute-value");
11
12
  exports.RULE_NAME = 'click-events-have-key-events';
12
13
  const DEFAULT_OPTIONS = {
13
14
  ignoreWithDirectives: [],
15
+ requireKeyCode: false,
16
+ allowedKeyCodes: [],
14
17
  };
18
+ const KEY_EVENTS = ['keyup', 'keydown', 'keypress'];
15
19
  exports.default = (0, create_eslint_rule_1.createESLintRule)({
16
20
  name: exports.RULE_NAME,
17
21
  meta: {
@@ -27,24 +31,41 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
27
31
  type: 'array',
28
32
  items: { type: 'string' },
29
33
  uniqueItems: true,
34
+ description: 'Directive names that, when present on the element, cause it to be ignored. Entries wrapped in slashes, e.g. `/^tui/`, are treated as regular expressions.',
30
35
  default: DEFAULT_OPTIONS.ignoreWithDirectives,
31
36
  },
37
+ requireKeyCode: {
38
+ type: 'boolean',
39
+ description: 'Require key events to specify a key via a pseudo-event, e.g. `(keydown.enter)`. A bare `(keydown)` will not satisfy the rule.',
40
+ default: DEFAULT_OPTIONS.requireKeyCode,
41
+ },
42
+ allowedKeyCodes: {
43
+ type: 'array',
44
+ items: { type: 'string' },
45
+ uniqueItems: true,
46
+ description: 'Only key events whose key (the last segment of the pseudo-event, e.g. `enter` in `(keydown.shift.enter)`) is in this list satisfy the rule. Implies `requireKeyCode`.',
47
+ default: DEFAULT_OPTIONS.allowedKeyCodes,
48
+ },
32
49
  },
33
50
  additionalProperties: false,
34
51
  },
35
52
  ],
36
53
  messages: {
37
54
  clickEventsHaveKeyEvents: 'click must be accompanied by either keyup, keydown or keypress event for accessibility.',
55
+ clickEventsHaveKeyCode: 'click must be accompanied by a keyup, keydown or keypress event that specifies a key (e.g. `(keydown.enter)`) for accessibility.',
56
+ clickEventsHaveAllowedKeyCode: 'click must be accompanied by a keyup, keydown or keypress event for one of the allowed keys ({{allowedKeyCodes}}) for accessibility.',
38
57
  },
39
58
  defaultOptions: [DEFAULT_OPTIONS],
40
59
  },
41
- create(context, [{ ignoreWithDirectives }]) {
60
+ create(context, [{ ignoreWithDirectives, requireKeyCode, allowedKeyCodes }]) {
61
+ const normalizedAllowedKeyCodes = (allowedKeyCodes ?? []).map((keyCode) => keyCode.toLowerCase());
62
+ const hasIgnoredDirective = (0, has_ignored_directive_1.createIgnoredDirectiveMatcher)(ignoreWithDirectives);
42
63
  return {
43
64
  Element(node) {
44
65
  if (!(0, get_dom_elements_1.getDomElements)().has(node.name.toLowerCase())) {
45
66
  return;
46
67
  }
47
- if (isIgnored(ignoreWithDirectives, node) ||
68
+ if (hasIgnoredDirective(node) ||
48
69
  (0, is_presentation_role_1.isPresentationRole)(node) ||
49
70
  (0, is_hidden_from_screen_reader_1.isHiddenFromScreenReader)(node) ||
50
71
  (0, is_interactive_element_1.isInherentlyInteractiveElement)(node)) {
@@ -56,42 +77,70 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
56
77
  if (role === 'button') {
57
78
  return;
58
79
  }
59
- let hasClick = false, hasKeyEvent = false;
80
+ let hasClick = false, hasKeyEvent = false, hasSatisfyingKeyEvent = false;
60
81
  for (const output of node.outputs) {
61
82
  hasClick = hasClick || output.name === 'click';
62
- hasKeyEvent =
63
- hasKeyEvent ||
64
- output.name.startsWith('keyup') ||
65
- output.name.startsWith('keydown') ||
66
- output.name.startsWith('keypress');
83
+ if (!isKeyEvent(output)) {
84
+ continue;
85
+ }
86
+ hasKeyEvent = true;
87
+ const keyCode = getKeyCode(output);
88
+ if (normalizedAllowedKeyCodes.length > 0) {
89
+ hasSatisfyingKeyEvent =
90
+ hasSatisfyingKeyEvent ||
91
+ (keyCode !== undefined &&
92
+ normalizedAllowedKeyCodes.includes(keyCode));
93
+ }
94
+ else if (requireKeyCode) {
95
+ hasSatisfyingKeyEvent =
96
+ hasSatisfyingKeyEvent || keyCode !== undefined;
97
+ }
98
+ else {
99
+ hasSatisfyingKeyEvent = true;
100
+ }
67
101
  }
68
- if (!hasClick || hasKeyEvent) {
102
+ if (!hasClick || hasSatisfyingKeyEvent) {
69
103
  return;
70
104
  }
71
105
  const parserServices = (0, utils_1.getTemplateParserServices)(context);
72
106
  const loc = parserServices.convertNodeSourceSpanToLoc(node.sourceSpan);
73
- context.report({
74
- loc,
75
- messageId: 'clickEventsHaveKeyEvents',
76
- });
107
+ if (!hasKeyEvent) {
108
+ context.report({
109
+ loc,
110
+ messageId: 'clickEventsHaveKeyEvents',
111
+ });
112
+ }
113
+ else if (normalizedAllowedKeyCodes.length > 0) {
114
+ context.report({
115
+ loc,
116
+ messageId: 'clickEventsHaveAllowedKeyCode',
117
+ data: { allowedKeyCodes: normalizedAllowedKeyCodes.join(', ') },
118
+ });
119
+ }
120
+ else {
121
+ context.report({
122
+ loc,
123
+ messageId: 'clickEventsHaveKeyCode',
124
+ });
125
+ }
77
126
  },
78
127
  };
79
128
  },
80
129
  });
81
- function isIgnored(ignoreWithDirectives, { inputs, attributes }) {
82
- if (ignoreWithDirectives && ignoreWithDirectives.length > 0) {
83
- for (const input of inputs) {
84
- if (ignoreWithDirectives.includes(input.name)) {
85
- return true;
86
- }
87
- }
88
- for (const attribute of attributes) {
89
- if (ignoreWithDirectives.includes(attribute.name)) {
90
- return true;
91
- }
92
- }
130
+ function isKeyEvent({ name }) {
131
+ return KEY_EVENTS.some((keyEvent) => name === keyEvent || name.startsWith(`${keyEvent}.`));
132
+ }
133
+ /**
134
+ * Returns the key targeted by a key pseudo-event, e.g. `enter` for
135
+ * `(keydown.shift.enter)`, or `undefined` for a bare `(keydown)`.
136
+ */
137
+ function getKeyCode({ name }) {
138
+ const segments = name.split('.');
139
+ if (segments.length < 2) {
140
+ return undefined;
93
141
  }
94
- return false;
142
+ const keyCode = segments[segments.length - 1].toLowerCase();
143
+ return keyCode.length > 0 ? keyCode : undefined;
95
144
  }
96
145
  exports.RULE_DOCS_EXTENSION = {
97
146
  rationale: 'Elements with click handlers must also be operable via keyboard for users who cannot use a mouse. This includes users with motor disabilities, users of assistive technologies, and keyboard-only users. While native interactive elements like buttons and links are keyboard-accessible by default (activated by Enter or Space keys), non-interactive elements with click handlers (like divs or spans) are not. For such elements, you must add keyboard event handlers (keyup, keydown, or keypress) that perform the same action as the click handler. Alternatively, use a semantic button element which handles this automatically. This is a WCAG Level A requirement for keyboard accessibility.',
@@ -0,0 +1,11 @@
1
+ import type { TmplAstElement } from '@angular-eslint/bundled-angular-compiler';
2
+ /**
3
+ * Compiles an `ignoreWithDirectives` option into a predicate that reports
4
+ * whether an element carries one of the ignored directives.
5
+ *
6
+ * Entries wrapped in slashes, e.g. `/^tui/`, are treated as regular
7
+ * expressions (an optional flags suffix is supported, e.g. `/^tui/i`).
8
+ * All other entries are matched exactly against the directive name.
9
+ */
10
+ export declare function createIgnoredDirectiveMatcher(ignoreWithDirectives: readonly string[] | undefined): (element: TmplAstElement) => boolean;
11
+ //# sourceMappingURL=has-ignored-directive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"has-ignored-directive.d.ts","sourceRoot":"","sources":["../../src/utils/has-ignored-directive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0CAA0C,CAAC;AAM/E;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC3C,oBAAoB,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAClD,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAsBtC"}
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createIgnoredDirectiveMatcher = createIgnoredDirectiveMatcher;
4
+ const REGEX_LITERAL_PATTERN = /^\/(.+)\/([a-z]*)$/;
5
+ /**
6
+ * Compiles an `ignoreWithDirectives` option into a predicate that reports
7
+ * whether an element carries one of the ignored directives.
8
+ *
9
+ * Entries wrapped in slashes, e.g. `/^tui/`, are treated as regular
10
+ * expressions (an optional flags suffix is supported, e.g. `/^tui/i`).
11
+ * All other entries are matched exactly against the directive name.
12
+ */
13
+ function createIgnoredDirectiveMatcher(ignoreWithDirectives) {
14
+ if (!ignoreWithDirectives || ignoreWithDirectives.length === 0) {
15
+ return () => false;
16
+ }
17
+ const matchers = ignoreWithDirectives.map((entry) => {
18
+ const regexLiteral = REGEX_LITERAL_PATTERN.exec(entry);
19
+ if (regexLiteral) {
20
+ const pattern = new RegExp(regexLiteral[1], regexLiteral[2]);
21
+ return (name) => pattern.test(name);
22
+ }
23
+ return (name) => name === entry;
24
+ });
25
+ const isIgnoredName = (name) => matchers.some((matcher) => matcher(name));
26
+ return ({ inputs, attributes }) => inputs.some(({ name }) => isIgnoredName(name)) ||
27
+ attributes.some(({ name }) => isIgnoredName(name));
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-eslint/eslint-plugin-template",
3
- "version": "22.3.1-alpha.0",
3
+ "version": "22.3.1-alpha.2",
4
4
  "description": "ESLint plugin for Angular Templates",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -19,17 +19,17 @@
19
19
  "LICENSE"
20
20
  ],
21
21
  "dependencies": {
22
- "@angular-eslint/bundled-angular-compiler": "22.3.1-alpha.0",
23
- "@angular-eslint/utils": "22.3.1-alpha.0",
22
+ "@angular-eslint/bundled-angular-compiler": "22.3.1-alpha.2",
23
+ "@angular-eslint/utils": "22.3.1-alpha.2",
24
24
  "aria-query": "5.3.2",
25
25
  "axobject-query": "4.1.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@angular-eslint/test-utils": "22.3.1-alpha.0",
28
+ "@angular-eslint/test-utils": "22.3.1-alpha.2",
29
29
  "@types/aria-query": "5.0.4"
30
30
  },
31
31
  "peerDependencies": {
32
- "@angular-eslint/template-parser": "22.3.1-alpha.0",
32
+ "@angular-eslint/template-parser": "22.3.1-alpha.2",
33
33
  "@typescript-eslint/types": "^8.0.0",
34
34
  "@typescript-eslint/utils": "^8.0.0",
35
35
  "eslint": "^9.0.0 || ^10.0.0",