stylelint-plugin-defensive-css 0.3.0 → 0.5.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
|
@@ -39,6 +39,75 @@ configuration.
|
|
|
39
39
|
|
|
40
40
|
The plugin provides multiple rules that can be toggled on and off as needed.
|
|
41
41
|
|
|
42
|
+
[Accidental Hover](#accidental-hover) -
|
|
43
|
+
[Background-Repeat](#background-repeat) -
|
|
44
|
+
[Custom Property Fallbacks](#custom-property-fallbacks) -
|
|
45
|
+
[Flex Wrapping](#flex-wrapping) - [Scroll Chaining](#scroll-chaining) -
|
|
46
|
+
[Vendor Prefix Grouping](#vendor-prefix-grouping)
|
|
47
|
+
|
|
48
|
+
### Accidental Hover
|
|
49
|
+
|
|
50
|
+
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/hover-media/)
|
|
51
|
+
|
|
52
|
+
We use hover effects to provide an indication to the user that an element is
|
|
53
|
+
clickable or active. That is fine for devices that have a mouse or a trackpad.
|
|
54
|
+
However, for mobile browsing hover effects can get confusing.
|
|
55
|
+
|
|
56
|
+
Enable this rule in order to prevent unintentional hover effects on mobile
|
|
57
|
+
devices.
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"rules": {
|
|
62
|
+
"plugin/use-defensive-css": [true, { "accidental-hover": true }]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### ✅ Passing Examples
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
@media (hover: hover) {
|
|
71
|
+
.btn:hover {
|
|
72
|
+
color: black;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Will traverse nested media queries */
|
|
77
|
+
@media (hover: hover) {
|
|
78
|
+
@media (min-width: 1px) {
|
|
79
|
+
.btn:hover {
|
|
80
|
+
color: black;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Will traverse nested media queries */
|
|
86
|
+
@media (min-width: 1px) {
|
|
87
|
+
@media (hover: hover) {
|
|
88
|
+
@media (min-width: 100px) {
|
|
89
|
+
.btn:hover {
|
|
90
|
+
color: black;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### ❌ Failing Examples
|
|
98
|
+
|
|
99
|
+
```css
|
|
100
|
+
.fail-btn:hover {
|
|
101
|
+
color: black;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@media (min-width: 1px) {
|
|
105
|
+
.fail-btn:hover {
|
|
106
|
+
color: black;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
42
111
|
### Background Repeat
|
|
43
112
|
|
|
44
113
|
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/bg-repeat/)
|
|
@@ -191,6 +260,64 @@ div {
|
|
|
191
260
|
}
|
|
192
261
|
```
|
|
193
262
|
|
|
263
|
+
### Scroll Chaining
|
|
264
|
+
|
|
265
|
+
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/scroll-chain/)
|
|
266
|
+
|
|
267
|
+
Have you ever opened a modal and started scrolling, and then when you reach the
|
|
268
|
+
end and keep scrolling, the content underneath the modal (the body element) will
|
|
269
|
+
scroll? This is called scroll chaining.
|
|
270
|
+
|
|
271
|
+
Enable this rule in order to require all scrollable overflow properties to have
|
|
272
|
+
an overscroll-behavior value.
|
|
273
|
+
|
|
274
|
+
```json
|
|
275
|
+
{
|
|
276
|
+
"rules": {
|
|
277
|
+
"plugin/use-defensive-css": [true, { "scroll-chaining": true }]
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### ✅ Passing Examples
|
|
283
|
+
|
|
284
|
+
```css
|
|
285
|
+
div {
|
|
286
|
+
overflow-x: auto;
|
|
287
|
+
overscroll-behavior-x: contain;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
div {
|
|
291
|
+
overflow: hidden scroll;
|
|
292
|
+
overscroll-behavior: contain;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
div {
|
|
296
|
+
overflow: hidden; /* No overscroll-behavior is needed in the case of hidden */
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
div {
|
|
300
|
+
overflow-block: auto;
|
|
301
|
+
overscroll-behavior: none;
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
#### ❌ Failing Examples
|
|
306
|
+
|
|
307
|
+
```css
|
|
308
|
+
div {
|
|
309
|
+
overflow-x: auto;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
div {
|
|
313
|
+
overflow: hidden scroll;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
div {
|
|
317
|
+
overflow-block: auto;
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
194
321
|
### Vendor Prefix Grouping
|
|
195
322
|
|
|
196
323
|
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/grouping-selectors/)
|
package/package.json
CHANGED
|
@@ -5,6 +5,9 @@ const stylelint = require('stylelint');
|
|
|
5
5
|
const ruleName = 'plugin/use-defensive-css';
|
|
6
6
|
|
|
7
7
|
const ruleMessages = stylelint.utils.ruleMessages(ruleName, {
|
|
8
|
+
accidentalHover() {
|
|
9
|
+
return 'To prevent accidental hover states on mobile devices, wrap :hover selectors inside a @media (hover: hover) { ...your styles } query.';
|
|
10
|
+
},
|
|
8
11
|
backgroundRepeat() {
|
|
9
12
|
return 'Ensure a background-repeat property is defined when using a background image.';
|
|
10
13
|
},
|
|
@@ -14,6 +17,9 @@ const ruleMessages = stylelint.utils.ruleMessages(ruleName, {
|
|
|
14
17
|
flexWrapping() {
|
|
15
18
|
return 'Flex rows must have a `flex-wrap: wrap;` or `flex-wrap: wrap-reverse` declaration.';
|
|
16
19
|
},
|
|
20
|
+
scrollChaining() {
|
|
21
|
+
return `Containers with an auto or scroll 'overflow' must also have an 'overscroll-behavior' property defined.`;
|
|
22
|
+
},
|
|
17
23
|
vendorPrefixWGrouping() {
|
|
18
24
|
return `Separate different vendor prefixes into their own rules.`;
|
|
19
25
|
},
|
|
@@ -20,10 +20,32 @@ const defaultFlexWrappingProps = {
|
|
|
20
20
|
isMissingFlexWrap: true,
|
|
21
21
|
nodeToReport: undefined,
|
|
22
22
|
};
|
|
23
|
+
const defaultScrollChainingProps = {
|
|
24
|
+
hasOverflow: false,
|
|
25
|
+
hasOverscrollBehavior: false,
|
|
26
|
+
nodeToReport: undefined,
|
|
27
|
+
};
|
|
23
28
|
|
|
24
29
|
let backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
|
|
25
30
|
let flexWrappingProps = { ...defaultFlexWrappingProps };
|
|
31
|
+
let scrollChainingProps = { ...defaultScrollChainingProps };
|
|
26
32
|
let isLastStyleDeclaration = false;
|
|
33
|
+
let isWrappedInHoverAtRule = false;
|
|
34
|
+
|
|
35
|
+
function traverseParentRules(parent) {
|
|
36
|
+
console.log({ parent });
|
|
37
|
+
if (parent.parent.type === 'root') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (parent.parent.type === 'atrule') {
|
|
42
|
+
if (parent.parent.params.includes('hover: hover')) {
|
|
43
|
+
isWrappedInHoverAtRule = true;
|
|
44
|
+
} else {
|
|
45
|
+
traverseParentRules(parent.parent);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
27
49
|
|
|
28
50
|
const ruleFunction = (_, options) => {
|
|
29
51
|
return (root, result) => {
|
|
@@ -38,6 +60,27 @@ const ruleFunction = (_, options) => {
|
|
|
38
60
|
JSON.stringify(decl) ===
|
|
39
61
|
JSON.stringify(decl.parent.nodes[decl.parent.nodes.length - 1]);
|
|
40
62
|
|
|
63
|
+
/* ACCIDENTAL HOVER */
|
|
64
|
+
if (options?.['accidental-hover']) {
|
|
65
|
+
const parent = decl.parent;
|
|
66
|
+
const selector = parent.selector;
|
|
67
|
+
const isHoverSelector = selector.includes(':hover');
|
|
68
|
+
isWrappedInHoverAtRule = false;
|
|
69
|
+
|
|
70
|
+
if (isHoverSelector) {
|
|
71
|
+
traverseParentRules(parent);
|
|
72
|
+
|
|
73
|
+
if (!isWrappedInHoverAtRule) {
|
|
74
|
+
stylelint.utils.report({
|
|
75
|
+
message: ruleMessages.accidentalHover(),
|
|
76
|
+
node: decl.parent,
|
|
77
|
+
result,
|
|
78
|
+
ruleName,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
41
84
|
/* BACKGROUND REPEAT */
|
|
42
85
|
if (options?.['background-repeat']) {
|
|
43
86
|
if (decl.prop === 'background' && decl.value.includes('url(')) {
|
|
@@ -134,7 +177,45 @@ const ruleFunction = (_, options) => {
|
|
|
134
177
|
}
|
|
135
178
|
}
|
|
136
179
|
|
|
137
|
-
/*
|
|
180
|
+
/* SCROLL CHAINING */
|
|
181
|
+
if (options?.['scroll-chaining']) {
|
|
182
|
+
const overflowProperties = [
|
|
183
|
+
'overflow',
|
|
184
|
+
'overflow-x',
|
|
185
|
+
'overflow-y',
|
|
186
|
+
'overflow-inline',
|
|
187
|
+
'overflow-block',
|
|
188
|
+
];
|
|
189
|
+
if (
|
|
190
|
+
overflowProperties.includes(decl.prop) &&
|
|
191
|
+
(decl.value.includes('auto') || decl.value.includes('scroll'))
|
|
192
|
+
) {
|
|
193
|
+
scrollChainingProps.hasOverflow = true;
|
|
194
|
+
scrollChainingProps.nodeToReport = decl;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (decl.prop.includes('overscroll-behavior')) {
|
|
198
|
+
scrollChainingProps.hasOverscrollBehavior = true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (isLastStyleDeclaration) {
|
|
202
|
+
if (
|
|
203
|
+
scrollChainingProps.hasOverflow &&
|
|
204
|
+
!scrollChainingProps.hasOverscrollBehavior
|
|
205
|
+
) {
|
|
206
|
+
stylelint.utils.report({
|
|
207
|
+
message: ruleMessages.scrollChaining(),
|
|
208
|
+
node: scrollChainingProps.nodeToReport,
|
|
209
|
+
result,
|
|
210
|
+
ruleName,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
scrollChainingProps = { ...defaultScrollChainingProps };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* VENDOR PREFIX GROUPING */
|
|
138
219
|
if (options?.['vendor-prefix-grouping']) {
|
|
139
220
|
const hasMultiplePrefixes = findVendorPrefixes(decl.parent.selector);
|
|
140
221
|
|