stylelint-plugin-defensive-css 0.4.0 → 0.6.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
@@ -23,21 +23,84 @@ npm i stylelint-plugin-defensive-css --save-dev
23
23
  yarn add stylelint-plugin-defensive-css --dev
24
24
  ```
25
25
 
26
- With the plugin installed, the rule(s) can be added to the project's Stylelint
27
- configuration.
26
+ With the plugin installed, the individual rule(s) can be added to the project's
27
+ Stylelint configuration.
28
+
29
+ ## Rules / Options
30
+
31
+ The plugin provides multiple rules that can be toggled on and off as needed.
32
+
33
+ 1. [Accidental Hover](#accidental-hover)
34
+ 2. [Background-Repeat](#background-repeat)
35
+ 3. [Custom Property Fallbacks](#custom-property-fallbacks)
36
+ 4. [Flex Wrapping](#flex-wrapping)
37
+ 5. [Scroll Chaining](#scroll-chaining)
38
+ 6. [Vendor Prefix Grouping](#vendor-prefix-grouping)
39
+
40
+ ---
41
+
42
+ ### Accidental Hover
43
+
44
+ > [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/hover-media/)
45
+
46
+ We use hover effects to provide an indication to the user that an element is
47
+ clickable or active. That is fine for devices that have a mouse or a trackpad.
48
+ However, for mobile browsing hover effects can get confusing.
49
+
50
+ Enable this rule in order to prevent unintentional hover effects on mobile
51
+ devices.
28
52
 
29
53
  ```json
30
54
  {
31
- "plugins": ["stylelint-plugin-defensive-css"],
32
55
  "rules": {
33
- "plugin/use-defensive-css": [true, { "severity": "warning" }]
56
+ "plugin/use-defensive-css": [true, { "accidental-hover": true }]
34
57
  }
35
58
  }
36
59
  ```
37
60
 
38
- ## Rules / Options
61
+ #### Passing Examples
39
62
 
40
- The plugin provides multiple rules that can be toggled on and off as needed.
63
+ ```css
64
+ @media (hover: hover) {
65
+ .btn:hover {
66
+ color: black;
67
+ }
68
+ }
69
+
70
+ /* Will traverse nested media queries */
71
+ @media (hover: hover) {
72
+ @media (min-width: 1px) {
73
+ .btn:hover {
74
+ color: black;
75
+ }
76
+ }
77
+ }
78
+
79
+ /* Will traverse nested media queries */
80
+ @media (min-width: 1px) {
81
+ @media (hover: hover) {
82
+ @media (min-width: 100px) {
83
+ .btn:hover {
84
+ color: black;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ #### ❌ Failing Examples
92
+
93
+ ```css
94
+ .fail-btn:hover {
95
+ color: black;
96
+ }
97
+
98
+ @media (min-width: 1px) {
99
+ .fail-btn:hover {
100
+ color: black;
101
+ }
102
+ }
103
+ ```
41
104
 
42
105
  ### Background Repeat
43
106
 
@@ -152,8 +215,8 @@ div {
152
215
  CSS flexbox is one of the most useful CSS layout features nowadays. It’s
153
216
  tempting to add `display: flex` to a wrapper and have the child items ordered
154
217
  next to each other. The thing is when there is not enough space, those child
155
- items won’t wrap into a new line by default. We need to change that behavior
156
- with `flex-wrap: wrap`.
218
+ items won’t wrap into a new line by default. We need to either change that
219
+ behavior with `flex-wrap: wrap` or explicitly define `nowrap` on the container.
157
220
 
158
221
  Enable this rule in order to require all flex rows to have a flex-wrap value.
159
222
 
@@ -172,6 +235,10 @@ div {
172
235
  display: flex;
173
236
  flex-wrap: wrap;
174
237
  }
238
+ div {
239
+ display: flex;
240
+ flex-wrap: nowrap;
241
+ }
175
242
  div {
176
243
  display: flex;
177
244
  flex-direction: row-reverse;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylelint-plugin-defensive-css",
3
- "version": "0.4.0",
3
+ "version": "0.6.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
  },
@@ -12,7 +15,7 @@ const ruleMessages = stylelint.utils.ruleMessages(ruleName, {
12
15
  return 'Ensure that any custom properties have a fallback value.';
13
16
  },
14
17
  flexWrapping() {
15
- return 'Flex rows must have a `flex-wrap: wrap;` or `flex-wrap: wrap-reverse` declaration.';
18
+ return 'Flex rows must have a `flex-wrap` value defined.`';
16
19
  },
17
20
  scrollChaining() {
18
21
  return `Containers with an auto or scroll 'overflow' must also have an 'overscroll-behavior' property defined.`;
@@ -30,6 +30,22 @@ let backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
30
30
  let flexWrappingProps = { ...defaultFlexWrappingProps };
31
31
  let scrollChainingProps = { ...defaultScrollChainingProps };
32
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
+ }
33
49
 
34
50
  const ruleFunction = (_, options) => {
35
51
  return (root, result) => {
@@ -44,6 +60,27 @@ const ruleFunction = (_, options) => {
44
60
  JSON.stringify(decl) ===
45
61
  JSON.stringify(decl.parent.nodes[decl.parent.nodes.length - 1]);
46
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
+
47
84
  /* BACKGROUND REPEAT */
48
85
  if (options?.['background-repeat']) {
49
86
  if (decl.prop === 'background' && decl.value.includes('url(')) {
@@ -122,7 +159,7 @@ const ruleFunction = (_, options) => {
122
159
  flexWrappingProps.isFlexRow = false;
123
160
  }
124
161
 
125
- if (decl.prop === 'flex-wrap' && decl.value.startsWith('wrap')) {
162
+ if (decl.prop === 'flex-wrap') {
126
163
  flexWrappingProps.isMissingFlexWrap = false;
127
164
  }
128
165