@taiga-ui/eslint-plugin-experience-next 0.512.0 → 0.514.0

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/README.md CHANGED
@@ -95,6 +95,7 @@ from third-party plugins. The exact severities and file globs live in
95
95
  | [no-implicit-public](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-implicit-public.md) | Require explicit `public` modifier for class members and parameter properties | ✅ | 🔧 | |
96
96
  | [no-infinite-loop](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-infinite-loop.md) | Disallow `while (true)` and `for` loops without an explicit condition | ✅ | | |
97
97
  | [no-legacy-peer-deps](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-legacy-peer-deps.md) | Disallow `legacy-peer-deps=true` in `.npmrc` | ✅ | | |
98
+ | [no-nested-interactive](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-nested-interactive.md) | Disallow interactive HTML elements nested inside other interactive elements | ✅ | | |
98
99
  | [no-obsolete-attrs](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-obsolete-attrs.md) | Disallow obsolete HTML attributes | ✅ | | |
99
100
  | [no-obsolete-tags](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-obsolete-tags.md) | Disallow obsolete HTML tags | ✅ | | |
100
101
  | [no-playwright-empty-fill](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-playwright-empty-fill.md) | Enforce `clear()` over `fill('')` in Playwright tests | ✅ | 🔧 | |
package/index.d.ts CHANGED
@@ -99,6 +99,9 @@ declare const plugin: {
99
99
  'no-legacy-peer-deps': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noLegacyPeerDeps", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
100
100
  name: string;
101
101
  };
102
+ 'no-nested-interactive': import("eslint").Rule.RuleModule & {
103
+ name: string;
104
+ };
102
105
  'no-obsolete-attrs': import("eslint").Rule.RuleModule & {
103
106
  name: string;
104
107
  };
package/index.esm.js CHANGED
@@ -1309,6 +1309,7 @@ var recommended = defineConfig([
1309
1309
  '@taiga-ui/experience-next/no-duplicate-id': 'error',
1310
1310
  '@taiga-ui/experience-next/no-duplicate-in-head': 'error',
1311
1311
  '@taiga-ui/experience-next/no-href-with-router-link': 'error',
1312
+ '@taiga-ui/experience-next/no-nested-interactive': 'error',
1312
1313
  '@taiga-ui/experience-next/no-obsolete-attrs': 'error',
1313
1314
  '@taiga-ui/experience-next/no-obsolete-tags': 'error',
1314
1315
  '@taiga-ui/experience-next/no-project-as-in-ng-template': 'error',
@@ -33212,9 +33213,44 @@ __name(compileHmrUpdateCallback, "compileHmrUpdateCallback");
33212
33213
  var VERSION = new Version("20.3.12");
33213
33214
  publishFacade(_global);
33214
33215
 
33216
+ function getAttributeNames(name) {
33217
+ return typeof name === 'string'
33218
+ ? [name.toLowerCase()]
33219
+ : name.map((item) => item.toLowerCase());
33220
+ }
33221
+ function isMatchingAttributeName(name, names) {
33222
+ return names.includes(name.toLowerCase());
33223
+ }
33215
33224
  function getAttributeValueSpan(attr) {
33216
33225
  return attr instanceof dist$4.TmplAstBoundEvent ? attr.handlerSpan : attr.valueSpan;
33217
33226
  }
33227
+ function getStaticAttribute(element, name) {
33228
+ const attrName = name.toLowerCase();
33229
+ return element.attributes.find((attr) => attr.name.toLowerCase() === attrName);
33230
+ }
33231
+ function getStaticAttributeValue(element, name) {
33232
+ return getStaticAttribute(element, name)?.value;
33233
+ }
33234
+ function hasInputBinding(element, name) {
33235
+ const names = getAttributeNames(name);
33236
+ return element.inputs.some((input) => isMatchingAttributeName(input.name, names));
33237
+ }
33238
+ function hasElementAttribute(element, name) {
33239
+ const names = getAttributeNames(name);
33240
+ return (element.attributes.some((attr) => isMatchingAttributeName(attr.name, names)) ||
33241
+ element.inputs.some((input) => {
33242
+ if (isMatchingAttributeName(input.name, names)) {
33243
+ return true;
33244
+ }
33245
+ const details = input.keySpan.details?.toLowerCase();
33246
+ return (details?.startsWith('attr.') === true && names.includes(details.slice(5)));
33247
+ }));
33248
+ }
33249
+ function hasOutputBinding(element, name) {
33250
+ {
33251
+ return element.outputs.length > 0;
33252
+ }
33253
+ }
33218
33254
  function getElementAttributeLikes(element) {
33219
33255
  const seen = new Set();
33220
33256
  return [
@@ -46233,7 +46269,7 @@ function buildMultilineStartTag(node, sourceText) {
46233
46269
  closing,
46234
46270
  ].join('\n');
46235
46271
  }
46236
- const rule$T = createRule({
46272
+ const rule$U = createRule({
46237
46273
  name: 'attrs-newline',
46238
46274
  rule: {
46239
46275
  create(context) {
@@ -46413,7 +46449,7 @@ function sameOrder(a, b) {
46413
46449
  return a.length === b.length && a.every((value, index) => value === b[index]);
46414
46450
  }
46415
46451
 
46416
- const rule$S = createRule({
46452
+ const rule$T = createRule({
46417
46453
  create(context, [order]) {
46418
46454
  const decorators = new Set(Object.keys(order));
46419
46455
  return {
@@ -46525,7 +46561,7 @@ const INLINE_HTML_ELEMENTS = new Set([
46525
46561
  'wbr',
46526
46562
  ]);
46527
46563
 
46528
- const MESSAGE_ID$g = 'expectAfter';
46564
+ const MESSAGE_ID$h = 'expectAfter';
46529
46565
  const SKIP_CHILDREN = new Set(['code', 'pre']);
46530
46566
  function isChildNode(node) {
46531
46567
  return (node instanceof dist$4.TmplAstBoundText ||
@@ -46549,7 +46585,7 @@ function getNodeLabel(node) {
46549
46585
  }
46550
46586
  return node instanceof dist$4.TmplAstBoundText ? 'binding' : 'text';
46551
46587
  }
46552
- const rule$R = createRule({
46588
+ const rule$S = createRule({
46553
46589
  name: 'element-newline',
46554
46590
  rule: {
46555
46591
  create(context) {
@@ -46575,7 +46611,7 @@ const rule$R = createRule({
46575
46611
  firstChild.sourceSpan.start.offset,
46576
46612
  ], '\n'),
46577
46613
  loc: sourceSpanToLoc(firstChild.sourceSpan),
46578
- messageId: MESSAGE_ID$g,
46614
+ messageId: MESSAGE_ID$h,
46579
46615
  });
46580
46616
  return;
46581
46617
  }
@@ -46593,7 +46629,7 @@ const rule$R = createRule({
46593
46629
  child.sourceSpan.end.offset,
46594
46630
  ], '\n'),
46595
46631
  loc: sourceSpanToLoc(next.sourceSpan),
46596
- messageId: MESSAGE_ID$g,
46632
+ messageId: MESSAGE_ID$h,
46597
46633
  });
46598
46634
  return;
46599
46635
  }
@@ -46607,7 +46643,7 @@ const rule$R = createRule({
46607
46643
  lastChild.sourceSpan.end.offset,
46608
46644
  ], '\n'),
46609
46645
  loc: sourceSpanToLoc(lastChild.sourceSpan),
46610
- messageId: MESSAGE_ID$g,
46646
+ messageId: MESSAGE_ID$h,
46611
46647
  });
46612
46648
  }
46613
46649
  },
@@ -46616,7 +46652,7 @@ const rule$R = createRule({
46616
46652
  meta: {
46617
46653
  docs: { description: 'Enforce line breaks between block-level child nodes' },
46618
46654
  fixable: 'code',
46619
- messages: { [MESSAGE_ID$g]: 'There should be a linebreak after {{name}}.' },
46655
+ messages: { [MESSAGE_ID$h]: 'There should be a linebreak after {{name}}.' },
46620
46656
  schema: [],
46621
46657
  type: 'layout',
46622
46658
  },
@@ -46688,7 +46724,7 @@ const PRESETS = {
46688
46724
  $VUE: ['$CLASS', '$ID', '$VUE_ATTRIBUTE'],
46689
46725
  $VUE_ATTRIBUTE: /^v-/,
46690
46726
  };
46691
- const rule$Q = createRule({
46727
+ const rule$R = createRule({
46692
46728
  create(context, [options]) {
46693
46729
  const sourceCode = context.sourceCode;
46694
46730
  const settings = {
@@ -46968,7 +47004,7 @@ const DIRECTIONAL_TO_LOGICAL = {
46968
47004
  top: 'inset-block-start',
46969
47005
  };
46970
47006
  const STYLE_PREFIX = 'style.';
46971
- const MESSAGE_ID$f = 'html-logical-properties';
47007
+ const MESSAGE_ID$g = 'html-logical-properties';
46972
47008
  const MESSAGE = `
46973
47009
  Use logical CSS properties instead of directional properties. Replace:
46974
47010
  • left → inset-inline-start
@@ -47006,7 +47042,7 @@ const config$4 = {
47006
47042
  context.report({
47007
47043
  fix: (fixer) => fixer.replaceTextRange([propertyStart, propertyEnd], logicalProperty),
47008
47044
  loc: sourceSpanToLoc(keySpan),
47009
- messageId: MESSAGE_ID$f,
47045
+ messageId: MESSAGE_ID$g,
47010
47046
  });
47011
47047
  },
47012
47048
  };
@@ -47014,12 +47050,12 @@ const config$4 = {
47014
47050
  meta: {
47015
47051
  docs: { description: MESSAGE },
47016
47052
  fixable: 'code',
47017
- messages: { [MESSAGE_ID$f]: MESSAGE },
47053
+ messages: { [MESSAGE_ID$g]: MESSAGE },
47018
47054
  schema: [],
47019
47055
  type: 'suggestion',
47020
47056
  },
47021
47057
  };
47022
- const rule$P = createRule({
47058
+ const rule$Q = createRule({
47023
47059
  name: 'html-logical-properties',
47024
47060
  rule: config$4,
47025
47061
  });
@@ -248261,7 +248297,7 @@ function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248261
248297
  }
248262
248298
  return hasSafeRuntimeUsage;
248263
248299
  }
248264
- const rule$O = createRule({
248300
+ const rule$P = createRule({
248265
248301
  create(context) {
248266
248302
  const { checker, esTreeNodeToTSNodeMap, sourceCode, tsProgram } = getTypeAwareRuleContext(context);
248267
248303
  const checkCycles = context.options[0]?.checkCycles ?? true;
@@ -248923,7 +248959,7 @@ const rule$O = createRule({
248923
248959
  name: 'import-integrity',
248924
248960
  });
248925
248961
 
248926
- const MESSAGE_ID$e = 'invalid-injection-token-description';
248962
+ const MESSAGE_ID$f = 'invalid-injection-token-description';
248927
248963
  const ERROR_MESSAGE$3 = "InjectionToken's description should contain token's name";
248928
248964
  const NG_DEV_MODE = 'ngDevMode';
248929
248965
  function getVariableName(node) {
@@ -248986,7 +249022,7 @@ function getNgDevModeDeclarationFix(program, fixer) {
248986
249022
  ? fixer.insertTextBefore(firstStatement, 'declare const ngDevMode: boolean;\n\n')
248987
249023
  : fixer.insertTextBeforeRange([0, 0], 'declare const ngDevMode: boolean;\n');
248988
249024
  }
248989
- const rule$N = createRule({
249025
+ const rule$O = createRule({
248990
249026
  create(context) {
248991
249027
  const { sourceCode } = context;
248992
249028
  const program = sourceCode.ast;
@@ -249018,7 +249054,7 @@ const rule$N = createRule({
249018
249054
  }
249019
249055
  return fixes;
249020
249056
  },
249021
- messageId: MESSAGE_ID$e,
249057
+ messageId: MESSAGE_ID$f,
249022
249058
  node: description,
249023
249059
  });
249024
249060
  }
@@ -249028,14 +249064,14 @@ const rule$N = createRule({
249028
249064
  meta: {
249029
249065
  docs: { description: ERROR_MESSAGE$3 },
249030
249066
  fixable: 'code',
249031
- messages: { [MESSAGE_ID$e]: ERROR_MESSAGE$3 },
249067
+ messages: { [MESSAGE_ID$f]: ERROR_MESSAGE$3 },
249032
249068
  schema: [],
249033
249069
  type: 'problem',
249034
249070
  },
249035
249071
  name: 'injection-token-description',
249036
249072
  });
249037
249073
 
249038
- const rule$M = createRule({
249074
+ const rule$N = createRule({
249039
249075
  create(context) {
249040
249076
  const { sourceCode } = context;
249041
249077
  const namespaceImports = new Map();
@@ -249111,7 +249147,7 @@ const rule$M = createRule({
249111
249147
  name: 'no-commonjs-import-patterns',
249112
249148
  });
249113
249149
 
249114
- const MESSAGE_ID$d = 'no-deep-imports';
249150
+ const MESSAGE_ID$e = 'no-deep-imports';
249115
249151
  const ERROR_MESSAGE$2 = 'Deep imports of Taiga UI packages are prohibited';
249116
249152
  const CODE_EXTENSIONS = new Set([
249117
249153
  '.cjs',
@@ -249130,7 +249166,7 @@ const DEFAULT_OPTIONS = {
249130
249166
  importDeclaration: '^@taiga-ui*',
249131
249167
  projectName: String.raw `(?<=^@taiga-ui/)([-\w]+)`,
249132
249168
  };
249133
- const rule$L = createRule({
249169
+ const rule$M = createRule({
249134
249170
  create(context) {
249135
249171
  const { currentProject, deepImport, ignoreImports, importDeclaration, projectName, } = { ...DEFAULT_OPTIONS, ...context.options[0] };
249136
249172
  const hasNonCodeExtension = (source) => {
@@ -249172,7 +249208,7 @@ const rule$L = createRule({
249172
249208
  const [start, end] = node.source.range;
249173
249209
  return fixer.replaceTextRange([start + 1, end - 1], importSource.replaceAll(new RegExp(deepImport, 'g'), ''));
249174
249210
  },
249175
- messageId: MESSAGE_ID$d,
249211
+ messageId: MESSAGE_ID$e,
249176
249212
  node: node.source,
249177
249213
  });
249178
249214
  },
@@ -249182,7 +249218,7 @@ const rule$L = createRule({
249182
249218
  defaultOptions: [DEFAULT_OPTIONS],
249183
249219
  docs: { description: ERROR_MESSAGE$2 },
249184
249220
  fixable: 'code',
249185
- messages: { [MESSAGE_ID$d]: ERROR_MESSAGE$2 },
249221
+ messages: { [MESSAGE_ID$e]: ERROR_MESSAGE$2 },
249186
249222
  schema: [
249187
249223
  {
249188
249224
  additionalProperties: false,
@@ -249222,7 +249258,7 @@ const nearestFileUpCache = new Map();
249222
249258
  const markerCache = new Map();
249223
249259
  const indexFileCache = new Map();
249224
249260
  const indexExportsCache = new Map();
249225
- const rule$K = createRule({
249261
+ const rule$L = createRule({
249226
249262
  create(context) {
249227
249263
  const parserServices = dist$3.ESLintUtils.getParserServices(context);
249228
249264
  const program = parserServices.program;
@@ -249416,13 +249452,13 @@ const noDuplicateAttributesRule = angular.templatePlugin.rules?.['no-duplicate-a
249416
249452
  if (!noDuplicateAttributesRule) {
249417
249453
  throw new Error('angular-eslint template rule "no-duplicate-attributes" is not available');
249418
249454
  }
249419
- const rule$J = createRule({
249455
+ const rule$K = createRule({
249420
249456
  name: 'no-duplicate-attrs',
249421
249457
  rule: noDuplicateAttributesRule,
249422
249458
  });
249423
249459
 
249424
- const MESSAGE_ID$c = 'duplicateId';
249425
- const rule$I = createRule({
249460
+ const MESSAGE_ID$d = 'duplicateId';
249461
+ const rule$J = createRule({
249426
249462
  name: 'no-duplicate-id',
249427
249463
  rule: {
249428
249464
  create(context) {
@@ -249445,7 +249481,7 @@ const rule$I = createRule({
249445
249481
  context.report({
249446
249482
  data: { id },
249447
249483
  loc: sourceSpanToLoc(attr.sourceSpan),
249448
- messageId: MESSAGE_ID$c,
249484
+ messageId: MESSAGE_ID$d,
249449
249485
  });
249450
249486
  }
249451
249487
  }
@@ -249454,36 +249490,33 @@ const rule$I = createRule({
249454
249490
  },
249455
249491
  meta: {
249456
249492
  docs: { description: 'Disallow duplicate static id attributes' },
249457
- messages: { [MESSAGE_ID$c]: "The id '{{id}}' is duplicated." },
249493
+ messages: { [MESSAGE_ID$d]: "The id '{{id}}' is duplicated." },
249458
249494
  schema: [],
249459
249495
  type: 'problem',
249460
249496
  },
249461
249497
  },
249462
249498
  });
249463
249499
 
249464
- const MESSAGE_ID$b = 'duplicateTag';
249465
- function findAttr(node, attrName) {
249466
- return node.attributes.find((attr) => attr.name === attrName);
249467
- }
249500
+ const MESSAGE_ID$c = 'duplicateTag';
249468
249501
  function getTrackingKey(node) {
249469
249502
  if (node.name === 'title' || node.name === 'base') {
249470
249503
  return node.name;
249471
249504
  }
249472
249505
  if (node.name === 'meta') {
249473
- if (findAttr(node, 'charset')) {
249506
+ if (getStaticAttribute(node, 'charset')) {
249474
249507
  return 'meta[charset]';
249475
249508
  }
249476
- if (findAttr(node, 'name')?.value === 'viewport') {
249509
+ if (getStaticAttribute(node, 'name')?.value === 'viewport') {
249477
249510
  return 'meta[name=viewport]';
249478
249511
  }
249479
249512
  }
249480
249513
  return node.name === 'link' &&
249481
- findAttr(node, 'rel')?.value === 'canonical' &&
249482
- findAttr(node, 'href')
249514
+ getStaticAttribute(node, 'rel')?.value === 'canonical' &&
249515
+ getStaticAttribute(node, 'href')
249483
249516
  ? 'link[rel=canonical]'
249484
249517
  : null;
249485
249518
  }
249486
- const rule$H = createRule({
249519
+ const rule$I = createRule({
249487
249520
  name: 'no-duplicate-in-head',
249488
249521
  rule: {
249489
249522
  create(context) {
@@ -249520,7 +249553,7 @@ const rule$H = createRule({
249520
249553
  context.report({
249521
249554
  data: { tag },
249522
249555
  loc: sourceSpanToLoc(duplicate.startSourceSpan),
249523
- messageId: MESSAGE_ID$b,
249556
+ messageId: MESSAGE_ID$c,
249524
249557
  });
249525
249558
  }
249526
249559
  }
@@ -249531,7 +249564,7 @@ const rule$H = createRule({
249531
249564
  docs: {
249532
249565
  description: 'Disallow duplicate title/base/meta/link tags inside head',
249533
249566
  },
249534
- messages: { [MESSAGE_ID$b]: 'Duplicate <{{tag}}> tag in <head>.' },
249567
+ messages: { [MESSAGE_ID$c]: 'Duplicate <{{tag}}> tag in <head>.' },
249535
249568
  schema: [],
249536
249569
  type: 'problem',
249537
249570
  },
@@ -249539,7 +249572,7 @@ const rule$H = createRule({
249539
249572
  });
249540
249573
 
249541
249574
  const COMPONENT_DECORATORS = new Set(['Component']);
249542
- const rule$G = createRule({
249575
+ const rule$H = createRule({
249543
249576
  create(context) {
249544
249577
  const { sourceCode } = context;
249545
249578
  return {
@@ -250187,7 +250220,7 @@ const ANGULAR_SIGNALS_UNTRACKED_GUIDE_URL = 'https://angular.dev/guide/signals#r
250187
250220
  const ANGULAR_SIGNALS_ASYNC_GUIDE_URL = 'https://angular.dev/guide/signals#reactive-context-and-async-operations';
250188
250221
  const createUntrackedRule = createRule;
250189
250222
 
250190
- const rule$F = createUntrackedRule({
250223
+ const rule$G = createUntrackedRule({
250191
250224
  create(context) {
250192
250225
  const { checker, esTreeNodeToTSNodeMap, program } = getTypeAwareRuleContext(context);
250193
250226
  const signalNodeMap = esTreeNodeToTSNodeMap;
@@ -250225,7 +250258,7 @@ const rule$F = createUntrackedRule({
250225
250258
  name: 'no-fully-untracked-effect',
250226
250259
  });
250227
250260
 
250228
- const MESSAGE_ID$a = 'no-href-with-router-link';
250261
+ const MESSAGE_ID$b = 'no-href-with-router-link';
250229
250262
  const ERROR_MESSAGE$1 = 'Do not use href and routerLink attributes together on the same element';
250230
250263
  const config$3 = {
250231
250264
  create(context) {
@@ -250235,9 +250268,9 @@ const config$3 = {
250235
250268
  if (node.name !== 'a') {
250236
250269
  return;
250237
250270
  }
250238
- const hrefAttr = node.attributes.find((attr) => attr.name === 'href');
250239
- const hasRouterLink = node.attributes.some((attr) => attr.name.toLowerCase() === 'routerlink') ||
250240
- node.inputs.some((input) => input.name.toLowerCase() === 'routerlink');
250271
+ const hrefAttr = getStaticAttribute(node, 'href');
250272
+ const hasRouterLink = Boolean(getStaticAttribute(node, 'routerlink')) ||
250273
+ hasInputBinding(node, 'routerlink');
250241
250274
  if (!hrefAttr || !hasRouterLink) {
250242
250275
  return;
250243
250276
  }
@@ -250247,7 +250280,7 @@ const config$3 = {
250247
250280
  hrefAttr.sourceSpan.end.offset,
250248
250281
  ]),
250249
250282
  loc: sourceSpanToLoc(hrefAttr.sourceSpan),
250250
- messageId: MESSAGE_ID$a,
250283
+ messageId: MESSAGE_ID$b,
250251
250284
  });
250252
250285
  },
250253
250286
  };
@@ -250255,12 +250288,12 @@ const config$3 = {
250255
250288
  meta: {
250256
250289
  docs: { description: ERROR_MESSAGE$1 },
250257
250290
  fixable: 'code',
250258
- messages: { [MESSAGE_ID$a]: ERROR_MESSAGE$1 },
250291
+ messages: { [MESSAGE_ID$b]: ERROR_MESSAGE$1 },
250259
250292
  schema: [],
250260
250293
  type: 'problem',
250261
250294
  },
250262
250295
  };
250263
- const rule$E = createRule({
250296
+ const rule$F = createRule({
250264
250297
  name: 'no-href-with-router-link',
250265
250298
  rule: config$3,
250266
250299
  });
@@ -250321,7 +250354,7 @@ function getScopeRoot(node) {
250321
250354
  return (findAncestor(node, (ancestor) => ancestor.type === dist$3.AST_NODE_TYPES.Program || isFunctionLike(ancestor)) ?? node);
250322
250355
  }
250323
250356
 
250324
- const rule$D = createRule({
250357
+ const rule$E = createRule({
250325
250358
  create(context) {
250326
250359
  const checkImplicitPublic = (node) => {
250327
250360
  const classRef = getEnclosingClass(node);
@@ -250383,7 +250416,7 @@ const rule$D = createRule({
250383
250416
  name: 'no-implicit-public',
250384
250417
  });
250385
250418
 
250386
- const rule$C = createRule({
250419
+ const rule$D = createRule({
250387
250420
  create(context) {
250388
250421
  const { sourceCode } = context;
250389
250422
  return {
@@ -250439,7 +250472,7 @@ function isInfiniteLoopLiteral(node) {
250439
250472
  function isInfiniteLoopTest(test) {
250440
250473
  return test == null || isInfiniteLoopLiteral(test);
250441
250474
  }
250442
- const rule$B = createRule({
250475
+ const rule$C = createRule({
250443
250476
  create(context) {
250444
250477
  return {
250445
250478
  DoWhileStatement(node) {
@@ -250484,7 +250517,7 @@ const rule$B = createRule({
250484
250517
  });
250485
250518
 
250486
250519
  const LEGACY_PEER_DEPS_PATTERN = /^legacy-peer-deps\s*=\s*true$/i;
250487
- const rule$A = createRule({
250520
+ const rule$B = createRule({
250488
250521
  create(context) {
250489
250522
  return {
250490
250523
  Program(node) {
@@ -250522,6 +250555,100 @@ const rule$A = createRule({
250522
250555
  name: 'no-legacy-peer-deps',
250523
250556
  });
250524
250557
 
250558
+ function isInteractiveElement(node) {
250559
+ const tagName = node.name.toLowerCase();
250560
+ if (hasElementAttribute(node, 'tabindex') || hasOutputBinding(node)) {
250561
+ return true;
250562
+ }
250563
+ switch (tagName) {
250564
+ case 'a':
250565
+ return hasElementAttribute(node, ['href', 'routerLink']);
250566
+ case 'area':
250567
+ return hasElementAttribute(node, 'href');
250568
+ case 'audio':
250569
+ return hasElementAttribute(node, 'controls');
250570
+ case 'button':
250571
+ return true;
250572
+ case 'details':
250573
+ return true;
250574
+ case 'embed':
250575
+ return true;
250576
+ case 'iframe':
250577
+ return true;
250578
+ case 'img':
250579
+ return hasElementAttribute(node, 'usemap');
250580
+ case 'input':
250581
+ return (getStaticAttributeValue(node, 'type')?.trim().toLowerCase() !== 'hidden');
250582
+ case 'label':
250583
+ return true;
250584
+ case 'select':
250585
+ return true;
250586
+ case 'summary':
250587
+ return true;
250588
+ case 'textarea':
250589
+ return true;
250590
+ case 'video':
250591
+ return hasElementAttribute(node, 'controls');
250592
+ default:
250593
+ return false;
250594
+ }
250595
+ }
250596
+
250597
+ const MESSAGE_ID$a = 'noNestedInteractive';
250598
+ function getAvailableLabelParent(stack, node, labelsWithControl) {
250599
+ const parent = stack[stack.length - 1];
250600
+ return stack.length === 1 &&
250601
+ parent?.name.toLowerCase() === 'label' &&
250602
+ node.name.toLowerCase() !== 'label' &&
250603
+ !labelsWithControl.has(parent)
250604
+ ? parent
250605
+ : null;
250606
+ }
250607
+ const rule$A = createRule({
250608
+ name: 'no-nested-interactive',
250609
+ rule: {
250610
+ create(context) {
250611
+ const interactiveStack = [];
250612
+ const labelsWithControl = new WeakSet();
250613
+ return {
250614
+ Element(rawNode) {
250615
+ const node = rawNode;
250616
+ if (!isInteractiveElement(node)) {
250617
+ return;
250618
+ }
250619
+ const parent = interactiveStack[interactiveStack.length - 1];
250620
+ const availableLabelParent = getAvailableLabelParent(interactiveStack, node, labelsWithControl);
250621
+ if (availableLabelParent) {
250622
+ labelsWithControl.add(availableLabelParent);
250623
+ }
250624
+ else if (parent) {
250625
+ context.report({
250626
+ data: { tag: parent.name },
250627
+ loc: sourceSpanToLoc(node.startSourceSpan),
250628
+ messageId: MESSAGE_ID$a,
250629
+ });
250630
+ }
250631
+ interactiveStack.push(node);
250632
+ },
250633
+ 'Element:exit'(rawNode) {
250634
+ const node = rawNode;
250635
+ if (interactiveStack[interactiveStack.length - 1] === node) {
250636
+ interactiveStack.pop();
250637
+ }
250638
+ },
250639
+ };
250640
+ },
250641
+ meta: {
250642
+ docs: { description: 'Disallow nested interactive elements' },
250643
+ messages: {
250644
+ [MESSAGE_ID$a]: 'Unexpected interactive element nested inside interactive <{{tag}}>.',
250645
+ },
250646
+ schema: [],
250647
+ type: 'problem',
250648
+ },
250649
+ },
250650
+ });
250651
+
250525
250652
  const OBSOLETE_HTML_ATTRS = {
250526
250653
  abbr: [
250527
250654
  {
@@ -254048,10 +254175,6 @@ const rule$e = createRule({
254048
254175
  });
254049
254176
 
254050
254177
  const MESSAGE_ID$5 = 'missingAlt';
254051
- function hasAlt(node) {
254052
- return (node.attributes.some((attr) => attr.name === 'alt') ||
254053
- node.inputs.some((input) => input.name === 'alt' || input.keySpan.details === 'attr.alt'));
254054
- }
254055
254178
  const rule$d = createRule({
254056
254179
  name: 'require-img-alt',
254057
254180
  rule: {
@@ -254059,7 +254182,7 @@ const rule$d = createRule({
254059
254182
  return {
254060
254183
  Element(rawNode) {
254061
254184
  const node = rawNode;
254062
- if (node.name !== 'img' || hasAlt(node)) {
254185
+ if (node.name !== 'img' || hasElementAttribute(node, 'alt')) {
254063
254186
  return;
254064
254187
  }
254065
254188
  context.report({
@@ -254092,16 +254215,14 @@ const rule$c = createRule({
254092
254215
  if (node.name !== 'html') {
254093
254216
  return;
254094
254217
  }
254095
- const langAttr = node.attributes.find((attr) => attr.name === 'lang');
254096
- const hasBoundLang = node.inputs.some((input) => input.name === 'lang' ||
254097
- input.keySpan.details === 'attr.lang');
254098
- if (!langAttr && !hasBoundLang) {
254218
+ if (!hasElementAttribute(node, 'lang')) {
254099
254219
  context.report({
254100
254220
  loc: sourceSpanToLoc(node.startSourceSpan),
254101
254221
  messageId: MESSAGE_IDS$1.MISSING,
254102
254222
  });
254103
254223
  return;
254104
254224
  }
254225
+ const langAttr = getStaticAttribute(node, 'lang');
254105
254226
  if (langAttr?.value.trim().length === 0) {
254106
254227
  context.report({
254107
254228
  loc: sourceSpanToLoc(langAttr.sourceSpan),
@@ -255707,28 +255828,29 @@ const plugin = {
255707
255828
  },
255708
255829
  rules: {
255709
255830
  'array-as-const': rule$5,
255710
- 'attrs-newline': rule$T,
255831
+ 'attrs-newline': rule$U,
255711
255832
  'class-property-naming': rule$4,
255712
- 'decorator-key-sort': rule$S,
255713
- 'element-newline': rule$R,
255833
+ 'decorator-key-sort': rule$T,
255834
+ 'element-newline': rule$S,
255714
255835
  'flat-exports': rule$3,
255715
- 'host-attributes-sort': rule$Q,
255716
- 'html-logical-properties': rule$P,
255717
- 'import-integrity': rule$O,
255718
- 'injection-token-description': rule$N,
255719
- 'no-commonjs-import-patterns': rule$M,
255720
- 'no-deep-imports': rule$L,
255721
- 'no-deep-imports-to-indexed-packages': rule$K,
255722
- 'no-duplicate-attrs': rule$J,
255723
- 'no-duplicate-id': rule$I,
255724
- 'no-duplicate-in-head': rule$H,
255725
- 'no-empty-style-metadata': rule$G,
255726
- 'no-fully-untracked-effect': rule$F,
255727
- 'no-href-with-router-link': rule$E,
255728
- 'no-implicit-public': rule$D,
255729
- 'no-import-assertions': rule$C,
255730
- 'no-infinite-loop': rule$B,
255731
- 'no-legacy-peer-deps': rule$A,
255836
+ 'host-attributes-sort': rule$R,
255837
+ 'html-logical-properties': rule$Q,
255838
+ 'import-integrity': rule$P,
255839
+ 'injection-token-description': rule$O,
255840
+ 'no-commonjs-import-patterns': rule$N,
255841
+ 'no-deep-imports': rule$M,
255842
+ 'no-deep-imports-to-indexed-packages': rule$L,
255843
+ 'no-duplicate-attrs': rule$K,
255844
+ 'no-duplicate-id': rule$J,
255845
+ 'no-duplicate-in-head': rule$I,
255846
+ 'no-empty-style-metadata': rule$H,
255847
+ 'no-fully-untracked-effect': rule$G,
255848
+ 'no-href-with-router-link': rule$F,
255849
+ 'no-implicit-public': rule$E,
255850
+ 'no-import-assertions': rule$D,
255851
+ 'no-infinite-loop': rule$C,
255852
+ 'no-legacy-peer-deps': rule$B,
255853
+ 'no-nested-interactive': rule$A,
255732
255854
  'no-obsolete-attrs': rule$z,
255733
255855
  'no-obsolete-tags': rule$y,
255734
255856
  'no-playwright-empty-fill': rule$x,
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.512.0",
3
+ "version": "0.514.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
+ "homepage": "https://github.com/taiga-family/toolkit#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/taiga-family/toolkit/issues"
8
+ },
5
9
  "repository": {
6
10
  "type": "git",
7
11
  "url": "https://github.com/taiga-family/toolkit.git"
@@ -0,0 +1,5 @@
1
+ import { type Rule } from 'eslint';
2
+ export declare const rule: Rule.RuleModule & {
3
+ name: string;
4
+ };
5
+ export default rule;
@@ -1,4 +1,11 @@
1
1
  import { type ParseSourceSpan, type TmplAstBoundAttribute, TmplAstBoundEvent, type TmplAstElement, type TmplAstReference, type TmplAstTextAttribute } from '@angular-eslint/bundled-angular-compiler';
2
2
  export type ElementAttributeLike = TmplAstBoundAttribute | TmplAstBoundEvent | TmplAstReference | TmplAstTextAttribute;
3
+ export type ElementAttributeName = string | readonly string[];
3
4
  export declare function getAttributeValueSpan(attr: ElementAttributeLike): ParseSourceSpan | undefined;
5
+ export declare function getStaticAttribute(element: TmplAstElement, name: string): TmplAstTextAttribute | undefined;
6
+ export declare function getStaticAttributeValue(element: TmplAstElement, name: string): string | undefined;
7
+ export declare function hasInputBinding(element: TmplAstElement, name: ElementAttributeName): boolean;
8
+ export declare function hasAttributeBinding(element: TmplAstElement, name: ElementAttributeName): boolean;
9
+ export declare function hasElementAttribute(element: TmplAstElement, name: ElementAttributeName): boolean;
10
+ export declare function hasOutputBinding(element: TmplAstElement, name?: ElementAttributeName): boolean;
4
11
  export declare function getElementAttributeLikes(element: TmplAstElement): ElementAttributeLike[];
@@ -0,0 +1,2 @@
1
+ import { type TmplAstElement } from '@angular-eslint/bundled-angular-compiler';
2
+ export declare function isInteractiveElement(node: TmplAstElement): boolean;