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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylelint-plugin-defensive-css",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "A Stylelint plugin to enforce defensive CSS best practices.",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -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
- /* GROUPING VENDOR PREFIXES */
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