@a11yfred/neighbor 1.0.4 → 2.0.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/lib/ulam-rules.js CHANGED
@@ -254,20 +254,154 @@ export function makeNoUsePageTitleInRemix() {
254
254
  }
255
255
  }
256
256
 
257
+ // ─── no-disabled-and-aria-disabled ───────────────────────────────────────────
258
+ //
259
+ // HTML5 disabled attribute and aria-disabled should not be used together on the
260
+ // same element or nested within each other (e.g., element with disabled containing
261
+ // a child with aria-disabled or vice versa). This creates conflicting semantics:
262
+ // - Native disabled (on form controls) is a concrete state
263
+ // - aria-disabled (on any element) is an accessibility annotation
264
+ // Using both leads to inconsistent screen reader announcements and styling conflicts.
265
+ //
266
+ // Solution: Use only aria-disabled on all custom controls; use only disabled on
267
+ // native form controls (button, input, select, textarea).
268
+
269
+ function hasDisabledInAncestors(node) {
270
+ let current = node
271
+ while (current) {
272
+ if (current.type === 'JSXOpeningElement' || current.type === 'JSXSelfClosingElement') {
273
+ const attrs = current.attributes || []
274
+ const hasDisabled = attrs.some(attr =>
275
+ (attr.type === 'JSXAttribute' && attr.name?.name === 'disabled') ||
276
+ (attr.type === 'JSXSpreadAttribute')
277
+ )
278
+ if (hasDisabled) return true
279
+ }
280
+ current = current.parent
281
+ }
282
+ return false
283
+ }
284
+
285
+ function hasAriaDisabledInAncestors(node) {
286
+ let current = node
287
+ while (current) {
288
+ if (current.type === 'JSXOpeningElement' || current.type === 'JSXSelfClosingElement') {
289
+ const attrs = current.attributes || []
290
+ const hasAriaDis = attrs.some(attr =>
291
+ attr.type === 'JSXAttribute' &&
292
+ attr.name?.name === 'aria-disabled'
293
+ )
294
+ if (hasAriaDis) return true
295
+ }
296
+ current = current.parent
297
+ }
298
+ return false
299
+ }
300
+
301
+ function hasDisabledInChildren(node) {
302
+ if (!node.children) return false
303
+ for (const child of node.children) {
304
+ if (child.type === 'JSXElement') {
305
+ const attrs = child.openingElement?.attributes || []
306
+ if (attrs.some(attr => attr.type === 'JSXAttribute' && attr.name?.name === 'disabled')) {
307
+ return true
308
+ }
309
+ if (hasDisabledInChildren(child)) return true
310
+ } else if (child.type === 'JSXFragment') {
311
+ if (hasDisabledInChildren(child)) return true
312
+ }
313
+ }
314
+ return false
315
+ }
316
+
317
+ function hasAriaDisabledInChildren(node) {
318
+ if (!node.children) return false
319
+ for (const child of node.children) {
320
+ if (child.type === 'JSXElement') {
321
+ const attrs = child.openingElement?.attributes || []
322
+ if (attrs.some(attr => attr.type === 'JSXAttribute' && attr.name?.name === 'aria-disabled')) {
323
+ return true
324
+ }
325
+ if (hasAriaDisabledInChildren(child)) return true
326
+ } else if (child.type === 'JSXFragment') {
327
+ if (hasAriaDisabledInChildren(child)) return true
328
+ }
329
+ }
330
+ return false
331
+ }
332
+
333
+ export function makeNoDisabledAndAriaDisabled() {
334
+ return {
335
+ meta: {
336
+ type: 'problem',
337
+ docs: {
338
+ description: 'Disallow disabled and aria-disabled used together on same element or nested',
339
+ url: 'https://www.w3.org/WAI/ARIA/apg/'
340
+ },
341
+ messages: {
342
+ sameElement:
343
+ 'Element has both `disabled` and `aria-disabled="true"`. Use only `aria-disabled` for custom controls ' +
344
+ 'or only `disabled` for native form controls (button, input, select, textarea), not both.',
345
+ nestedDisabledInAriaDis:
346
+ 'Element with `aria-disabled="true"` contains a descendant with native `disabled` attribute. ' +
347
+ 'Use either `aria-disabled` consistently across the tree or `disabled` on native controls only, ' +
348
+ 'not nested combinations.',
349
+ nestedAriaDisInDisabled:
350
+ 'Element with `disabled` contains a descendant with `aria-disabled="true"`. ' +
351
+ 'Use either `disabled` on native form controls or `aria-disabled` consistently, not both nested.',
352
+ },
353
+ schema: [],
354
+ },
355
+ create(context) {
356
+ return {
357
+ 'JSXOpeningElement, JSXSelfClosingElement'(node) {
358
+ const attrs = node.attributes || []
359
+ const hasDisabled = attrs.some(attr =>
360
+ attr.type === 'JSXAttribute' && attr.name?.name === 'disabled'
361
+ )
362
+ const hasAriaDis = attrs.some(attr =>
363
+ attr.type === 'JSXAttribute' && attr.name?.name === 'aria-disabled'
364
+ )
365
+
366
+ // Check: both on same element
367
+ if (hasDisabled && hasAriaDis) {
368
+ context.report({ node, messageId: 'sameElement' })
369
+ return
370
+ }
371
+
372
+ // Check: aria-disabled parent with disabled child
373
+ if (hasAriaDis && hasDisabledInChildren(node.parent)) {
374
+ context.report({ node, messageId: 'nestedDisabledInAriaDis' })
375
+ return
376
+ }
377
+
378
+ // Check: disabled parent with aria-disabled child
379
+ if (hasDisabled && hasAriaDisabledInChildren(node.parent)) {
380
+ context.report({ node, messageId: 'nestedAriaDisInDisabled' })
381
+ return
382
+ }
383
+ },
384
+ }
385
+ },
386
+ }
387
+ }
388
+
257
389
  // ─── All ulam rule factories ──────────────────────────────────────────────────
258
390
 
259
391
  export const ULAM_RULE_FACTORIES = {
260
- 'no-announce-in-render': makeNoAnnounceInRender,
261
- 'no-hash-router-in-remix': makeNoHashRouterInRemix,
262
- 'no-use-page-title-in-remix': makeNoUsePageTitleInRemix,
392
+ 'no-announce-in-render': makeNoAnnounceInRender,
393
+ 'no-hash-router-in-remix': makeNoHashRouterInRemix,
394
+ 'no-use-page-title-in-remix': makeNoUsePageTitleInRemix,
395
+ 'no-disabled-and-aria-disabled': makeNoDisabledAndAriaDisabled,
263
396
  }
264
397
 
265
- /** React plugin: all three ulam rules. */
398
+ /** React plugin: all ulam rules. */
266
399
  export function buildUlamRules() {
267
400
  return {
268
- 'no-announce-in-render': makeNoAnnounceInRender({ framework: 'react' }),
269
- 'no-hash-router-in-remix': makeNoHashRouterInRemix(),
270
- 'no-use-page-title-in-remix': makeNoUsePageTitleInRemix(),
401
+ 'no-announce-in-render': makeNoAnnounceInRender({ framework: 'react' }),
402
+ 'no-hash-router-in-remix': makeNoHashRouterInRemix(),
403
+ 'no-use-page-title-in-remix': makeNoUsePageTitleInRemix(),
404
+ 'no-disabled-and-aria-disabled': makeNoDisabledAndAriaDisabled(),
271
405
  }
272
406
  }
273
407
 
@@ -287,9 +421,10 @@ export function buildUlamRulesAngular() {
287
421
 
288
422
  export function buildUlamRecommendedRules(ns) {
289
423
  return {
290
- [`${ns}/no-announce-in-render`]: 'error',
291
- [`${ns}/no-hash-router-in-remix`]: 'warn',
292
- [`${ns}/no-use-page-title-in-remix`]: 'warn',
424
+ [`${ns}/no-announce-in-render`]: 'error',
425
+ [`${ns}/no-hash-router-in-remix`]: 'warn',
426
+ [`${ns}/no-use-page-title-in-remix`]: 'warn',
427
+ [`${ns}/no-disabled-and-aria-disabled`]: 'error',
293
428
  }
294
429
  }
295
430
 
@@ -57,7 +57,7 @@
57
57
  * SJSU Writing Ctr sjsu.edu/writingcenter/docs/handouts/Accessible Writing Strategies.pdf
58
58
  */
59
59
 
60
- import { CONTENT_RULE_FACTORIES, buildContentRecommendedRules } from './lib/content-rules.js'
60
+ import { CONTENT_RULE_FACTORIES, buildContentRecommendedRules } from '@a11yfred/neighbor/lib/content-rules.js'
61
61
 
62
62
  const NS = '@a11yfred/neighbor/content'
63
63
 
@@ -23,12 +23,13 @@
23
23
  * ]
24
24
  */
25
25
 
26
- import { h } from './lib/helpers-angular.js'
27
- import { buildRules, buildRecommendedRules, buildPortabilityRules } from './lib/rules.js'
28
- import { buildUlamRulesAngular, buildUlamRecommendedRulesFramework } from './lib/ulam-rules.js'
26
+ import { h } from '@a11yfred/neighbor/lib/helpers-angular.js'
27
+ import { buildRules, buildRecommendedRules, buildPortabilityRules } from '@a11yfred/neighbor/lib/rules.js'
28
+ import { buildUlamRulesAngular, buildUlamRecommendedRulesFramework } from '@a11yfred/neighbor/lib/ulam-rules.js'
29
+ import { buildAngularFrameworkRules, buildAngularHostRecommendedRules } from '@a11yfred/neighbor/lib/framework-rules.js'
29
30
 
30
31
  const NS = '@a11yfred/neighbor'
31
- const rules = { ...buildRules(h), ...buildUlamRulesAngular() }
32
+ const rules = { ...buildRules(h), ...buildUlamRulesAngular(), ...buildAngularFrameworkRules() }
32
33
  const plugin = { meta: { name: `${NS}/angular` }, rules }
33
34
 
34
35
  let angularA11y = null
@@ -41,14 +42,36 @@ const ANGULAR_A11Y_RULES = [
41
42
  'no-positive-tabindex', 'role-has-required-aria', 'table-scope', 'valid-aria',
42
43
  ]
43
44
 
45
+ const UNLIKELY_ANGULAR_RULES = new Set([
46
+ 'no-autofocus', 'no-distracting-elements'
47
+ ])
48
+
44
49
  function getAngularA11yRules(plugin) {
45
50
  const out = {}
46
51
  for (const rule of ANGULAR_A11Y_RULES) {
47
- if (plugin.rules?.[rule]) out[`@angular-eslint/template/${rule}`] = 'error'
52
+ if (plugin.rules?.[rule]) {
53
+ out[`@angular-eslint/template/${rule}`] = UNLIKELY_ANGULAR_RULES.has(rule) ? 'off' : 'error'
54
+ }
48
55
  }
49
56
  return out
50
57
  }
51
58
 
59
+ const angularRecommended = {
60
+ ...buildRecommendedRules(NS),
61
+ ...buildPortabilityRules(NS),
62
+ ...buildUlamRecommendedRulesFramework(NS),
63
+ ...buildAngularHostRecommendedRules(NS),
64
+ }
65
+
66
+ // Omit rules that are already covered by @angular-eslint/template (if installed):
67
+ if (angularA11y) {
68
+ delete angularRecommended[`${NS}/no-heading-no-content`] // covered by @angular-eslint/template/elements-content
69
+ delete angularRecommended[`${NS}/no-anchor-no-content`] // covered by @angular-eslint/template/elements-content
70
+ delete angularRecommended[`${NS}/no-img-redundant-alt`] // covered by @angular-eslint/template/alt-text
71
+ delete angularRecommended[`${NS}/no-scope-on-td`] // covered by @angular-eslint/template/table-scope
72
+ delete angularRecommended[`${NS}/no-invalid-aria-prop-value`] // covered by @angular-eslint/template/valid-aria
73
+ }
74
+
52
75
  export default {
53
76
  ...plugin,
54
77
  configs: {
@@ -59,9 +82,7 @@ export default {
59
82
  },
60
83
  rules: {
61
84
  ...(angularA11y ? getAngularA11yRules(angularA11y) : {}),
62
- ...buildRecommendedRules(NS),
63
- ...buildPortabilityRules(NS),
64
- ...buildUlamRecommendedRulesFramework(NS),
85
+ ...angularRecommended,
65
86
  },
66
87
  },
67
88
  },
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @a11yfred/neighbor - ESLint plugin (Lit)
3
+ *
4
+ * Flags Lit-specific accessibility issues like autofocus within html`...` templates.
5
+ *
6
+ * Usage in eslint.config.js:
7
+ * import neighborLit from '@a11yfred/neighbor/lit'
8
+ *
9
+ * export default [
10
+ * {
11
+ * files: ['**/*.ts', '**/*.js'],
12
+ * plugins: { '@a11yfred/neighbor': neighborLit },
13
+ * rules: neighborLit.configs.recommended.rules,
14
+ * },
15
+ * ]
16
+ */
17
+
18
+ import { buildLitRules, buildLitRecommendedRules } from '@a11yfred/neighbor/lib/framework-rules.js'
19
+ import { buildRecommendedRules, buildPortabilityRules } from '@a11yfred/neighbor/lib/rules.js'
20
+
21
+ const NS = '@a11yfred/neighbor'
22
+ const rules = buildLitRules()
23
+ const plugin = { meta: { name: `${NS}/lit` }, rules }
24
+
25
+ let litA11y = null
26
+ try { litA11y = (await import('eslint-plugin-lit-a11y')).default } catch {}
27
+
28
+ const LIT_A11Y_RULES = [
29
+ 'accessible-emoji', 'alt-text', 'anchor-is-valid', 'aria-activedescendant-has-tabindex',
30
+ 'aria-attr-valid-value', 'aria-attrs', 'aria-role', 'aria-unsupported-elements',
31
+ 'click-events-have-key-events', 'heading-has-content', 'iframe-title',
32
+ 'img-redundant-alt', 'mouse-events-have-key-events', 'no-access-key',
33
+ 'no-autofocus', 'no-distracting-elements', 'no-redundant-role',
34
+ 'role-has-required-aria-props', 'role-supports-aria-props', 'scope', 'tabindex-no-positive',
35
+ 'valid-lang'
36
+ ]
37
+
38
+ const UNLIKELY_LIT_RULES = new Set([
39
+ 'accessible-emoji', 'no-autofocus', 'no-distracting-elements', 'no-redundant-role'
40
+ ])
41
+
42
+ function getLitA11yRules(plugin) {
43
+ const out = {}
44
+ for (const rule of LIT_A11Y_RULES) {
45
+ if (plugin.rules?.[rule]) {
46
+ out[`lit-a11y/${rule}`] = UNLIKELY_LIT_RULES.has(rule) ? 'off' : 'error'
47
+ }
48
+ }
49
+ return out
50
+ }
51
+
52
+ const litRecommended = {
53
+ ...buildRecommendedRules(NS),
54
+ ...buildPortabilityRules(NS),
55
+ ...buildLitRecommendedRules(NS),
56
+ }
57
+
58
+ // Omit rules that are already covered by lit-a11y (if installed):
59
+ if (litA11y) {
60
+ delete litRecommended[`${NS}/no-heading-no-content`] // covered by lit-a11y/heading-has-content
61
+ delete litRecommended[`${NS}/no-iframe-no-title`] // covered by lit-a11y/iframe-title
62
+ delete litRecommended[`${NS}/no-img-redundant-alt`] // covered by lit-a11y/img-redundant-alt
63
+ delete litRecommended[`${NS}/no-access-key`] // covered by lit-a11y/no-access-key
64
+ delete litRecommended[`${NS}/no-aria-activedescendant-no-tabindex`] // covered by lit-a11y/aria-activedescendant-has-tabindex
65
+ delete litRecommended[`${NS}/no-anchor-no-content`] // covered by lit-a11y/anchor-is-valid
66
+ delete litRecommended[`${NS}/no-invalid-aria-prop-value`] // covered by lit-a11y/aria-attr-valid-value
67
+ delete litRecommended[`${NS}/no-role-supports-aria-props`] // covered by lit-a11y/role-supports-aria-props
68
+ delete litRecommended[`${NS}/no-scope-on-td`] // covered by lit-a11y/scope
69
+ }
70
+
71
+ export default {
72
+ ...plugin,
73
+ configs: {
74
+ recommended: {
75
+ plugins: {
76
+ [NS]: plugin,
77
+ ...(litA11y ? { 'lit-a11y': litA11y } : {}),
78
+ },
79
+ rules: {
80
+ ...(litA11y ? getLitA11yRules(litA11y) : {}),
81
+ ...litRecommended,
82
+ },
83
+ },
84
+ },
85
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @a11yfred/neighbor - ESLint plugin (Remix 3)
3
+ *
4
+ * Flags the same ARIA anti-patterns as neighbor-eslint.mjs, plus specific
5
+ * ulam rules tailored for Remix (e.g., no-use-page-title-in-remix).
6
+ *
7
+ * Usage in eslint.config.js:
8
+ * import neighbor from '@a11yfred/neighbor/remix3'
9
+ *
10
+ * export default [
11
+ * {
12
+ * files: ['**/*.tsx', '**/*.jsx'],
13
+ * plugins: { '@a11yfred/neighbor': neighbor },
14
+ * rules: neighbor.configs.recommended.rules,
15
+ * },
16
+ * ]
17
+ */
18
+
19
+ import { h } from '@a11yfred/neighbor/lib/helpers-jsx.js'
20
+ import { buildRules, buildRecommendedRules, buildReactFrameworkRules } from '@a11yfred/neighbor/lib/rules.js'
21
+ import { buildUlamRules, buildUlamRecommendedRules } from '@a11yfred/neighbor/lib/ulam-rules.js'
22
+ import { buildRemixRules, buildRemixRecommendedRules } from '@a11yfred/neighbor/lib/framework-rules.js'
23
+
24
+ const NS = '@a11yfred/neighbor'
25
+ const rules = { ...buildRules(h), ...buildUlamRules(), ...buildRemixRules() }
26
+
27
+ const plugin = { meta: { name: `${NS}/remix3` }, rules }
28
+
29
+ let jsxA11y = null
30
+ try { jsxA11y = (await import('eslint-plugin-jsx-a11y')).default } catch {}
31
+
32
+ export default {
33
+ ...plugin,
34
+ configs: {
35
+ recommended: {
36
+ plugins: {
37
+ [NS]: plugin,
38
+ ...(jsxA11y ? { 'jsx-a11y': jsxA11y } : {}),
39
+ },
40
+ rules: {
41
+ ...(jsxA11y ? jsxA11y.configs.recommended.rules : {}),
42
+ ...buildRecommendedRules(NS),
43
+ ...buildUlamRecommendedRules(NS),
44
+ ...buildReactFrameworkRules(NS),
45
+ ...buildRemixRecommendedRules(NS),
46
+ },
47
+ },
48
+ },
49
+ }
@@ -18,9 +18,9 @@
18
18
  * ]
19
19
  */
20
20
 
21
- import { h } from './lib/helpers-vue.js'
22
- import { buildRules, buildRecommendedRules, buildPortabilityRules } from './lib/rules.js'
23
- import { buildUlamRulesVue, buildUlamRecommendedRulesFramework } from './lib/ulam-rules.js'
21
+ import { h } from '@a11yfred/neighbor/lib/helpers-vue.js'
22
+ import { buildRules, buildRecommendedRules, buildPortabilityRules, buildVueFrameworkRules } from '@a11yfred/neighbor/lib/rules.js'
23
+ import { buildUlamRulesVue, buildUlamRecommendedRulesFramework } from '@a11yfred/neighbor/lib/ulam-rules.js'
24
24
 
25
25
  const NS = '@a11yfred/neighbor'
26
26
  const rules = { ...buildRules(h), ...buildUlamRulesVue() }
@@ -29,6 +29,24 @@ const plugin = { meta: { name: `${NS}/vue` }, rules }
29
29
  let vueA11y = null
30
30
  try { vueA11y = (await import('eslint-plugin-vuejs-accessibility')).default } catch {}
31
31
 
32
+ const vueRecommended = {
33
+ ...buildRecommendedRules(NS),
34
+ ...buildPortabilityRules(NS),
35
+ ...buildUlamRecommendedRulesFramework(NS),
36
+ ...buildVueFrameworkRules(NS),
37
+ }
38
+
39
+ // Omit rules that are already covered by vuejs-accessibility (if installed):
40
+ if (vueA11y) {
41
+ delete vueRecommended[`${NS}/no-heading-no-content`] // covered by vuejs-accessibility/heading-has-content
42
+ delete vueRecommended[`${NS}/no-iframe-no-title`] // covered by vuejs-accessibility/iframe-has-title
43
+ delete vueRecommended[`${NS}/no-access-key`] // covered by vuejs-accessibility/no-access-key
44
+ delete vueRecommended[`${NS}/no-img-redundant-alt`] // covered by vuejs-accessibility/alt-text
45
+ delete vueRecommended[`${NS}/no-anchor-no-content`] // covered by vuejs-accessibility/anchor-has-content
46
+ delete vueRecommended[`${NS}/no-invalid-aria-prop-value`] // covered by vuejs-accessibility/aria-props
47
+ delete vueRecommended[`${NS}/no-role-supports-aria-props`] // covered by vuejs-accessibility/aria-role
48
+ }
49
+
32
50
  export default {
33
51
  ...plugin,
34
52
  configs: {
@@ -39,9 +57,11 @@ export default {
39
57
  },
40
58
  rules: {
41
59
  ...(vueA11y ? vueA11y.configs['flat/recommended'].rules : {}),
42
- ...buildRecommendedRules(NS),
43
- ...buildPortabilityRules(NS),
44
- ...buildUlamRecommendedRulesFramework(NS),
60
+ 'vuejs-accessibility/accessible-emoji': 'off',
61
+ 'vuejs-accessibility/no-autofocus': 'off',
62
+ 'vuejs-accessibility/no-distracting-elements': 'off',
63
+ 'vuejs-accessibility/no-redundant-roles': 'off',
64
+ ...vueRecommended,
45
65
  },
46
66
  },
47
67
  },
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @a11yfred/neighbor - ESLint plugin (Vanilla Web Components / HTML)
3
+ *
4
+ * Flags ARIA anti-patterns in vanilla HTML or Web Component templates.
5
+ * Requires @html-eslint/parser as the project's ESLint parser for .html files.
6
+ *
7
+ * Usage in eslint.config.js:
8
+ * import htmlParser from '@html-eslint/parser'
9
+ * import neighbor from '@a11yfred/neighbor/webcomponents'
10
+ *
11
+ * export default [
12
+ * {
13
+ * files: ['**/*.html'],
14
+ * languageOptions: { parser: htmlParser },
15
+ * plugins: { '@a11yfred/neighbor': neighbor },
16
+ * rules: neighbor.configs.recommended.rules,
17
+ * },
18
+ * ]
19
+ */
20
+
21
+ import { h } from '@a11yfred/neighbor/lib/helpers-webcomponents.js'
22
+ import { buildRules, buildRecommendedRules } from '@a11yfred/neighbor/lib/rules.js'
23
+
24
+ const NS = '@a11yfred/neighbor'
25
+ const rules = buildRules(h)
26
+
27
+ const plugin = { meta: { name: `${NS}/webcomponents` }, rules }
28
+
29
+ export default {
30
+ ...plugin,
31
+ configs: {
32
+ recommended: {
33
+ plugins: {
34
+ [NS]: plugin,
35
+ },
36
+ rules: {
37
+ ...buildRecommendedRules(NS),
38
+ },
39
+ },
40
+ },
41
+ }
@@ -28,28 +28,35 @@
28
28
  * input missing label → jsx-a11y/label-has-associated-control
29
29
  */
30
30
 
31
- import jsxA11y from 'eslint-plugin-jsx-a11y'
32
- import { h } from './lib/helpers-jsx.js'
33
- import { buildRules, buildRecommendedRules } from './lib/rules.js'
34
- import { buildUlamRules, buildUlamRecommendedRules } from './lib/ulam-rules.js'
31
+ import { h } from '@a11yfred/neighbor/lib/helpers-jsx.js'
32
+ import { buildRules, buildRecommendedRules, buildReactFrameworkRules } from '@a11yfred/neighbor/lib/rules.js'
33
+ import { buildUlamRules, buildUlamRecommendedRules } from '@a11yfred/neighbor/lib/ulam-rules.js'
35
34
 
36
35
  const NS = '@a11yfred/neighbor'
37
36
  const rules = { ...buildRules(h), ...buildUlamRules() }
38
37
 
39
38
  const plugin = { meta: { name: NS }, rules }
40
39
 
40
+ let jsxA11y = null
41
+ try { jsxA11y = (await import('eslint-plugin-jsx-a11y')).default } catch {}
42
+
41
43
  export default {
42
44
  ...plugin,
43
45
  configs: {
44
46
  recommended: {
45
47
  plugins: {
46
48
  [NS]: plugin,
47
- 'jsx-a11y': jsxA11y,
49
+ ...(jsxA11y ? { 'jsx-a11y': jsxA11y } : {}),
48
50
  },
49
51
  rules: {
50
- ...jsxA11y.configs.recommended.rules,
52
+ ...(jsxA11y ? jsxA11y.configs.recommended.rules : {}),
53
+ 'jsx-a11y/accessible-emoji': 'off',
54
+ 'jsx-a11y/no-autofocus': 'off',
55
+ 'jsx-a11y/no-distracting-elements': 'off',
56
+ 'jsx-a11y/no-redundant-roles': 'off',
51
57
  ...buildRecommendedRules(NS),
52
58
  ...buildUlamRecommendedRules(NS),
59
+ ...buildReactFrameworkRules(NS),
53
60
  },
54
61
  },
55
62
  },