stylelint-plugin-defensive-css 0.9.2 → 0.10.1

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
@@ -35,7 +35,8 @@ The plugin provides multiple rules that can be toggled on and off as needed.
35
35
  3. [Custom Property Fallbacks](#custom-property-fallbacks)
36
36
  4. [Flex Wrapping](#flex-wrapping)
37
37
  5. [Scroll Chaining](#scroll-chaining)
38
- 6. [Vendor Prefix Grouping](#vendor-prefix-grouping)
38
+ 6. [Scrollbar Gutter](#scrollbar-gutter)
39
+ 7. [Vendor Prefix Grouping](#vendor-prefix-grouping)
39
40
 
40
41
  ---
41
42
 
@@ -328,6 +329,66 @@ div {
328
329
  }
329
330
  ```
330
331
 
332
+ ### Scrollbar Gutter
333
+
334
+ > [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/scrollbar-gutter/)
335
+
336
+ Imagine a container with only a small amount of content with no need to scroll.
337
+ The content would be aligned evenly within the boundaries of its container. Now,
338
+ if that container has more content added, and a scrollbar appears, that
339
+ scrollbar will cause a layout shift, forcing the content to reflow and jump.
340
+ This behavior can be jarring.
341
+
342
+ To avoid layout shifting with variable content, enforce that a
343
+ `scrollbar-gutter` property is defined for any scrollable container.
344
+
345
+ ```json
346
+ {
347
+ "rules": {
348
+ "plugin/use-defensive-css": [true, { "scrollbar-gutter": true }]
349
+ }
350
+ }
351
+ ```
352
+
353
+ #### ✅ Passing Examples
354
+
355
+ ```css
356
+ div {
357
+ overflow-x: auto;
358
+ scrollbar-gutter: auto;
359
+ }
360
+
361
+ div {
362
+ overflow: hidden scroll;
363
+ scrollbar-gutter: stable;
364
+ }
365
+
366
+ div {
367
+ overflow: hidden; /* No scrollbar-gutter is needed in the case of hidden */
368
+ }
369
+
370
+ div {
371
+ overflow-block: auto;
372
+ scrollbar-gutter: stable both-edges;
373
+ }
374
+ ```
375
+
376
+ #### ❌ Failing Examples
377
+
378
+ ```css
379
+ div {
380
+ overflow-x: auto;
381
+ }
382
+
383
+ div {
384
+ overflow: hidden scroll;
385
+ }
386
+
387
+ div {
388
+ overflow-block: auto;
389
+ }
390
+ ```
391
+
331
392
  ### Vendor Prefix Grouping
332
393
 
333
394
  > [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.9.2",
3
+ "version": "0.10.1",
4
4
  "description": "A Stylelint plugin to enforce defensive CSS best practices.",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -17,6 +17,9 @@ const ruleMessages = stylelint.utils.ruleMessages(ruleName, {
17
17
  flexWrapping() {
18
18
  return 'Flex rows must have a `flex-wrap` value defined.`';
19
19
  },
20
+ scrollbarGutter() {
21
+ return `Containers with an auto or scroll 'overflow' must also have a 'scrollbar-gutter' property defined.`;
22
+ },
20
23
  scrollChaining() {
21
24
  return `Containers with an auto or scroll 'overflow' must also have an 'overscroll-behavior' property defined.`;
22
25
  },
@@ -20,6 +20,11 @@ const defaultFlexWrappingProps = {
20
20
  isMissingFlexWrap: true,
21
21
  nodeToReport: undefined,
22
22
  };
23
+ const defaultScrollbarGutterProps = {
24
+ hasOverflow: false,
25
+ hasScrollbarGutter: false,
26
+ nodeToReport: undefined,
27
+ };
23
28
  const defaultScrollChainingProps = {
24
29
  hasOverflow: false,
25
30
  hasOverscrollBehavior: false,
@@ -28,10 +33,19 @@ const defaultScrollChainingProps = {
28
33
 
29
34
  let backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
30
35
  let flexWrappingProps = { ...defaultFlexWrappingProps };
36
+ let scrollbarGutterProps = { ...defaultScrollbarGutterProps };
31
37
  let scrollChainingProps = { ...defaultScrollChainingProps };
32
38
  let isLastStyleDeclaration = false;
33
39
  let isWrappedInHoverAtRule = false;
34
40
 
41
+ const overflowProperties = [
42
+ 'overflow',
43
+ 'overflow-x',
44
+ 'overflow-y',
45
+ 'overflow-inline',
46
+ 'overflow-block',
47
+ ];
48
+
35
49
  function traverseParentRules(parent) {
36
50
  if (parent.parent.type === 'root') {
37
51
  return;
@@ -184,15 +198,39 @@ const ruleFunction = (_, options) => {
184
198
  }
185
199
  }
186
200
 
201
+ /* SCROLLBAR GUTTER */
202
+ if (options?.['scrollbar-gutter']) {
203
+ if (
204
+ overflowProperties.includes(decl.prop) &&
205
+ (decl.value.includes('auto') || decl.value.includes('scroll'))
206
+ ) {
207
+ scrollbarGutterProps.hasOverflow = true;
208
+ scrollbarGutterProps.nodeToReport = decl;
209
+ }
210
+
211
+ if (decl.prop.includes('scrollbar-gutter')) {
212
+ scrollbarGutterProps.hasScrollbarGutter = true;
213
+ }
214
+
215
+ if (isLastStyleDeclaration) {
216
+ if (
217
+ scrollbarGutterProps.hasOverflow &&
218
+ !scrollbarGutterProps.hasScrollbarGutter
219
+ ) {
220
+ stylelint.utils.report({
221
+ message: ruleMessages.scrollbarGutter(),
222
+ node: scrollbarGutterProps.nodeToReport,
223
+ result,
224
+ ruleName,
225
+ });
226
+ }
227
+
228
+ scrollbarGutterProps = { ...defaultScrollbarGutterProps };
229
+ }
230
+ }
231
+
187
232
  /* SCROLL CHAINING */
188
233
  if (options?.['scroll-chaining']) {
189
- const overflowProperties = [
190
- 'overflow',
191
- 'overflow-x',
192
- 'overflow-y',
193
- 'overflow-inline',
194
- 'overflow-block',
195
- ];
196
234
  if (
197
235
  overflowProperties.includes(decl.prop) &&
198
236
  (decl.value.includes('auto') || decl.value.includes('scroll'))