@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/CHANGELOG.md +88 -71
- package/CONTRIBUTING.md +41 -41
- package/README.md +4 -466
- package/RULES-CONTENT.md +245 -86
- package/RULES-CSS.md +41 -19
- package/RULES-MARKUP.md +168 -93
- package/RULES.md +47 -28
- package/lib/content-rules.js +216 -0
- package/lib/framework-rules.js +282 -0
- package/lib/helpers-webcomponents.js +134 -0
- package/lib/rules.js +376 -11
- package/lib/ulam-rules.js +145 -10
- package/neighbor-content.mjs +1 -1
- package/neighbor-eslint-angular.mjs +29 -8
- package/neighbor-eslint-lit.mjs +85 -0
- package/neighbor-eslint-remix3.mjs +49 -0
- package/neighbor-eslint-vue.mjs +26 -6
- package/neighbor-eslint-webcomponents.mjs +41 -0
- package/neighbor-eslint.mjs +13 -6
- package/neighbor-stylelint.mjs +141 -3
- package/package.json +10 -10
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':
|
|
261
|
-
'no-hash-router-in-remix':
|
|
262
|
-
'no-use-page-title-in-remix':
|
|
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
|
|
398
|
+
/** React plugin: all ulam rules. */
|
|
266
399
|
export function buildUlamRules() {
|
|
267
400
|
return {
|
|
268
|
-
'no-announce-in-render':
|
|
269
|
-
'no-hash-router-in-remix':
|
|
270
|
-
'no-use-page-title-in-remix':
|
|
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`]:
|
|
291
|
-
[`${ns}/no-hash-router-in-remix`]:
|
|
292
|
-
[`${ns}/no-use-page-title-in-remix`]:
|
|
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
|
|
package/neighbor-content.mjs
CHANGED
|
@@ -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 '
|
|
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 '
|
|
27
|
-
import { buildRules, buildRecommendedRules, buildPortabilityRules } from '
|
|
28
|
-
import { buildUlamRulesAngular, buildUlamRecommendedRulesFramework } from '
|
|
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])
|
|
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
|
-
...
|
|
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
|
+
}
|
package/neighbor-eslint-vue.mjs
CHANGED
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* ]
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { h } from '
|
|
22
|
-
import { buildRules, buildRecommendedRules, buildPortabilityRules } from '
|
|
23
|
-
import { buildUlamRulesVue, buildUlamRecommendedRulesFramework } from '
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|
package/neighbor-eslint.mjs
CHANGED
|
@@ -28,28 +28,35 @@
|
|
|
28
28
|
* input missing label → jsx-a11y/label-has-associated-control
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
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
|
},
|