stylelint-plugin-defensive-css 0.4.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/)
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.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
  },
@@ -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(')) {