@a11yfred/neighbor 1.0.1 → 1.0.3
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 +100 -6
- package/neighbor-stylelint.mjs +36 -11
- package/package.json +14 -2
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ Some rules are specific to **@ulam** - an upcoming JavaScript framework by the
|
|
|
9
9
|
- [Install](#install)
|
|
10
10
|
- [Entry points](#entry-points)
|
|
11
11
|
- [Setup](#setup)
|
|
12
|
-
- [
|
|
12
|
+
- [Vanilla JS / plain HTML (no framework)](#vanilla-js--plain-html-no-framework)
|
|
13
|
+
- [React / JSX](#react-jsx)
|
|
13
14
|
- [Remix 2](#remix-2)
|
|
14
15
|
- [Remix 3](#remix-3)
|
|
15
16
|
- [Vue](#vue)
|
|
@@ -18,11 +19,11 @@ Some rules are specific to **@ulam** - an upcoming JavaScript framework by the
|
|
|
18
19
|
- [Content linting](#content-linting)
|
|
19
20
|
- [Peer dependencies](#peer-dependencies)
|
|
20
21
|
- [What neighbor adds](#what-neighbor-adds)
|
|
21
|
-
- [ESLint - React / JSX](#eslint
|
|
22
|
-
- [ESLint - Remix 2](#eslint
|
|
23
|
-
- [ESLint - Vue SFCs](#eslint
|
|
24
|
-
- [ESLint - Angular templates](#eslint
|
|
25
|
-
- [Stylelint - CSS](#stylelint
|
|
22
|
+
- [ESLint - React / JSX](#eslint-react-jsx)
|
|
23
|
+
- [ESLint - Remix 2](#eslint-remix-2)
|
|
24
|
+
- [ESLint - Vue SFCs](#eslint-vue-sfcs)
|
|
25
|
+
- [ESLint - Angular templates](#eslint-angular-templates)
|
|
26
|
+
- [Stylelint - CSS](#stylelint-css)
|
|
26
27
|
- [Content linter](#content-linter)
|
|
27
28
|
- [Rule severity](#rule-severity)
|
|
28
29
|
- [Contributing](CONTRIBUTING.md)
|
|
@@ -48,6 +49,99 @@ npm install --save-dev @a11yfred/neighbor
|
|
|
48
49
|
|
|
49
50
|
## Setup
|
|
50
51
|
|
|
52
|
+
### Vanilla JS / plain HTML (no framework)
|
|
53
|
+
|
|
54
|
+
If you write plain JavaScript with no JSX, React, Vue, or Angular, only the **Stylelint CSS rules** and the **content linter** apply. The ESLint markup rules require a component framework — they lint JSX or template syntax that plain JS does not have.
|
|
55
|
+
|
|
56
|
+
**What you get:**
|
|
57
|
+
|
|
58
|
+
| Plugin | What it checks |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| Stylelint (`@a11yfred/neighbor`) | CSS: bare `outline: none`, forced-colors opt-out, motion/transparency without `prefers-*` fallbacks |
|
|
61
|
+
| Content linter (`@a11yfred/neighbor/content`) | JS strings: ableist language, vague CTAs, unexplained abbreviations, idioms, all-caps prose |
|
|
62
|
+
|
|
63
|
+
**Stylelint setup** (CSS only, no framework needed):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install --save-dev stylelint stylelint-config-standard @a11yfred/neighbor
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
// .stylelintrc.json
|
|
71
|
+
{
|
|
72
|
+
"extends": ["stylelint-config-standard"],
|
|
73
|
+
"plugins": ["@a11yfred/neighbor"],
|
|
74
|
+
"rules": {
|
|
75
|
+
"neighbor/no-outline-none": true,
|
|
76
|
+
"neighbor/no-forced-colors-none": true,
|
|
77
|
+
"neighbor/user-preferences": true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Run it:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx stylelint "**/*.css"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Content linter setup** (plain JS string literals, no framework needed):
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install --save-dev eslint @a11yfred/neighbor
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
// eslint.config.js (ESLint flat config, ESLint >= 8)
|
|
96
|
+
import neighborContent from '@a11yfred/neighbor/content'
|
|
97
|
+
|
|
98
|
+
export default [
|
|
99
|
+
{
|
|
100
|
+
files: ['**/*.js'],
|
|
101
|
+
plugins: { ...neighborContent.configs.recommended.plugins },
|
|
102
|
+
rules: { ...neighborContent.configs.recommended.rules },
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Run it:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx eslint src/
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Both together:**
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
// eslint.config.js
|
|
117
|
+
import neighborContent from '@a11yfred/neighbor/content'
|
|
118
|
+
|
|
119
|
+
export default [
|
|
120
|
+
{
|
|
121
|
+
files: ['**/*.js'],
|
|
122
|
+
plugins: { ...neighborContent.configs.recommended.plugins },
|
|
123
|
+
rules: { ...neighborContent.configs.recommended.rules },
|
|
124
|
+
},
|
|
125
|
+
]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
// .stylelintrc.json
|
|
130
|
+
{
|
|
131
|
+
"extends": ["stylelint-config-standard"],
|
|
132
|
+
"plugins": ["@a11yfred/neighbor"],
|
|
133
|
+
"rules": {
|
|
134
|
+
"neighbor/no-outline-none": true,
|
|
135
|
+
"neighbor/no-forced-colors-none": true,
|
|
136
|
+
"neighbor/user-preferences": true
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The ESLint markup rules (`@a11yfred/neighbor/eslint` and variants) require JSX or a component template syntax. They will not produce useful output on plain HTML or vanilla JS files and should be skipped.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
51
145
|
### React / JSX
|
|
52
146
|
|
|
53
147
|
Neighbor works alongside `eslint-plugin-jsx-a11y`. Install both.
|
package/neighbor-stylelint.mjs
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* double-great/stylelint-a11y github.com/double-great/stylelint-a11y
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import stylelint from 'stylelint';
|
|
17
|
+
const { utils: { report } } = stylelint;
|
|
18
|
+
|
|
16
19
|
const defined = (x) => x !== undefined && x !== null;
|
|
17
20
|
|
|
18
21
|
/** True if the node or any ancestor is a prefers-* / forced-colors media block */
|
|
@@ -117,7 +120,7 @@ function rule(primaryOption) {
|
|
|
117
120
|
|
|
118
121
|
// opacity - warn on non-structural values (i.e. dims like 0.5, 0.75)
|
|
119
122
|
if (prop === 'opacity' && !isStructuralOpacity(value)) {
|
|
120
|
-
decl
|
|
123
|
+
report(decl, messages.opacity(value));
|
|
121
124
|
return;
|
|
122
125
|
}
|
|
123
126
|
|
|
@@ -125,7 +128,7 @@ function rule(primaryOption) {
|
|
|
125
128
|
if (prop === 'animation' || prop === 'transition' || prop === 'animation-name') {
|
|
126
129
|
// Skip "none" values - they're already the reduced state
|
|
127
130
|
if (/^none\b/i.test(value.trim())) return;
|
|
128
|
-
decl
|
|
131
|
+
report(decl, messages.animation(prop, value));
|
|
129
132
|
return;
|
|
130
133
|
}
|
|
131
134
|
|
|
@@ -136,7 +139,7 @@ function rule(primaryOption) {
|
|
|
136
139
|
'outline-color', 'box-shadow', 'text-shadow', 'fill', 'stroke',
|
|
137
140
|
]);
|
|
138
141
|
if (visualProps.has(prop) && hasAlphaChannel(value)) {
|
|
139
|
-
decl
|
|
142
|
+
report(decl, messages.alpha(value));
|
|
140
143
|
}
|
|
141
144
|
});
|
|
142
145
|
};
|
|
@@ -169,6 +172,30 @@ function isFocusSelector(selector) {
|
|
|
169
172
|
return /:focus(?:-visible|-within)?/i.test(selector);
|
|
170
173
|
}
|
|
171
174
|
|
|
175
|
+
/** Returns true if the node is inside a @media (pointer: fine/coarse) block — keyboard users are unaffected. */
|
|
176
|
+
function insidePointerMedia(node) {
|
|
177
|
+
let current = node.parent;
|
|
178
|
+
while (current) {
|
|
179
|
+
if (
|
|
180
|
+
current.type === 'atrule' &&
|
|
181
|
+
current.name === 'media' &&
|
|
182
|
+
/pointer\s*:\s*(fine|coarse)/.test(current.params)
|
|
183
|
+
) return true;
|
|
184
|
+
current = current.parent;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Returns true if the decl or any ancestor rule node targets a focus state. */
|
|
190
|
+
function insideFocusSelector(decl) {
|
|
191
|
+
let current = decl.parent;
|
|
192
|
+
while (current) {
|
|
193
|
+
if (current.type === 'rule' && isFocusSelector(current.selector ?? '')) return true;
|
|
194
|
+
current = current.parent;
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
172
199
|
/** @type {import('stylelint').Rule} */
|
|
173
200
|
function noOutlineNoneRule(_primaryOption) {
|
|
174
201
|
return (root, result) => {
|
|
@@ -176,14 +203,12 @@ function noOutlineNoneRule(_primaryOption) {
|
|
|
176
203
|
const value = decl.value.trim().toLowerCase();
|
|
177
204
|
if (value !== 'none' && value !== '0') return;
|
|
178
205
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
if (parent?.type === 'rule' && isFocusSelector(parent.selector ?? '')) return;
|
|
206
|
+
// Allow inside :focus / :focus-visible (author is intentionally restyling focus)
|
|
207
|
+
if (insideFocusSelector(decl)) return;
|
|
208
|
+
// Allow inside @media (pointer: fine/coarse) — keyboard users are unaffected
|
|
209
|
+
if (insidePointerMedia(decl)) return;
|
|
184
210
|
|
|
185
|
-
|
|
186
|
-
decl.warn(result, noOutlineNoneMessages.removed(decl.value), { rule: noOutlineNoneRuleName });
|
|
211
|
+
report({ message: noOutlineNoneMessages.removed(decl.value), node: decl, result, ruleName: noOutlineNoneRuleName });
|
|
187
212
|
});
|
|
188
213
|
};
|
|
189
214
|
}
|
|
@@ -243,7 +268,7 @@ function noForcedColorsNoneRule(_primaryOption) {
|
|
|
243
268
|
if (decl.value.trim().toLowerCase() !== 'none') return;
|
|
244
269
|
if (!insideForcedColorsMedia(decl)) return;
|
|
245
270
|
const selector = decl.parent?.selector ?? decl.parent?.name ?? '(unknown)';
|
|
246
|
-
|
|
271
|
+
report({ message: noForcedColorsNoneMessages.none(selector), node: decl, result, ruleName: noForcedColorsNoneRuleName });
|
|
247
272
|
});
|
|
248
273
|
};
|
|
249
274
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a11yfred/neighbor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Accessibility linting for a11yfred - ESLint (markup + content), Stylelint (CSS). Won't you be my neighbor?",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"files": [
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"*.mjs",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"README.md",
|
|
12
|
+
"CHANGELOG.md",
|
|
13
|
+
"CONTRIBUTING.md",
|
|
14
|
+
"RULES.md",
|
|
15
|
+
"RULES-MARKUP.md",
|
|
16
|
+
"RULES-CSS.md",
|
|
17
|
+
"RULES-CONTENT.md"
|
|
18
|
+
],
|
|
8
19
|
"main": "./neighbor-stylelint.mjs",
|
|
9
20
|
"repository": {
|
|
10
21
|
"type": "git",
|
|
@@ -67,6 +78,7 @@
|
|
|
67
78
|
}
|
|
68
79
|
},
|
|
69
80
|
"devDependencies": {
|
|
81
|
+
"@a11yfred/neighbor": "^1.0.1",
|
|
70
82
|
"eslint": "^9.39.4",
|
|
71
83
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
72
84
|
"stylelint": "^17.11.0"
|