Skip to content

Commit

Permalink
Add Support for a Scrollbar Gutter Rule (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
yuschick authored Oct 28, 2023
1 parent 2df9902 commit 2193b5a
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 8 deletions.
63 changes: 62 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ The plugin provides multiple rules that can be toggled on and off as needed.
3. [Custom Property Fallbacks](#custom-property-fallbacks)
4. [Flex Wrapping](#flex-wrapping)
5. [Scroll Chaining](#scroll-chaining)
6. [Vendor Prefix Grouping](#vendor-prefix-grouping)
6. [Scrollbar Gutter](#scrollbar-gutter)
7. [Vendor Prefix Grouping](#vendor-prefix-grouping)

---

Expand Down Expand Up @@ -328,6 +329,66 @@ div {
}
```

### Scrollbar Gutter

> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/scrollbar-gutter/)
Imagine a container with only a small amount of content with no need to scroll.
The content would be aligned evenly within the boundaries of its container. Now,
if that container has more content added, and a scrollbar appears, that
scrollbar will cause a layout shift, forcing the content to reflow and jump.
This behavior can be jarring.

To avoid layout shifting with variable content, enforce that a
`scrollbar-gutter` property is defined for any scrollable container.

```json
{
"rules": {
"plugin/use-defensive-css": [true, { "scrollbar-gutter": true }]
}
}
```

#### ✅ Passing Examples

```css
div {
overflow-x: auto;
scrollbar-gutter: auto;
}

div {
overflow: hidden scroll;
scrollbar-gutter: stable;
}

div {
overflow: hidden; /* No overscroll-behavior is needed in the case of hidden */
}

div {
overflow-block: auto;
scrollbar-gutter: stable both-edges;
}
```

#### ❌ Failing Examples

```css
div {
overflow-x: auto;
}

div {
overflow: hidden scroll;
}

div {
overflow-block: auto;
}
```

### Vendor Prefix Grouping

> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/grouping-selectors/)
Expand Down
3 changes: 3 additions & 0 deletions src/rules/use-defensive-css/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const ruleMessages = stylelint.utils.ruleMessages(ruleName, {
flexWrapping() {
return 'Flex rows must have a `flex-wrap` value defined.`';
},
scrollbarGutter() {
return `Containers with an auto or scroll 'overflow' must also have a 'scrollbar-gutter' property defined.`;
},
scrollChaining() {
return `Containers with an auto or scroll 'overflow' must also have an 'overscroll-behavior' property defined.`;
},
Expand Down
52 changes: 45 additions & 7 deletions src/rules/use-defensive-css/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const defaultFlexWrappingProps = {
isMissingFlexWrap: true,
nodeToReport: undefined,
};
const defaultScrollbarGutterProps = {
hasOverflow: false,
hasScrollbarGutter: false,
nodeToReport: undefined,
};
const defaultScrollChainingProps = {
hasOverflow: false,
hasOverscrollBehavior: false,
Expand All @@ -28,10 +33,19 @@ const defaultScrollChainingProps = {

let backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
let flexWrappingProps = { ...defaultFlexWrappingProps };
let scrollbarGutterProps = { ...defaultScrollbarGutterProps };
let scrollChainingProps = { ...defaultScrollChainingProps };
let isLastStyleDeclaration = false;
let isWrappedInHoverAtRule = false;

const overflowProperties = [
'overflow',
'overflow-x',
'overflow-y',
'overflow-inline',
'overflow-block',
];

function traverseParentRules(parent) {
if (parent.parent.type === 'root') {
return;
Expand Down Expand Up @@ -184,15 +198,39 @@ const ruleFunction = (_, options) => {
}
}

/* SCROLLBAR GUTTER */
if (options?.['scrollbar-gutter']) {
if (
overflowProperties.includes(decl.prop) &&
(decl.value.includes('auto') || decl.value.includes('scroll'))
) {
scrollbarGutterProps.hasOverflow = true;
scrollbarGutterProps.nodeToReport = decl;
}

if (decl.prop.includes('scrollbar-gutter')) {
scrollbarGutterProps.hasScrollbarGutter = true;
}

if (isLastStyleDeclaration) {
if (
scrollbarGutterProps.hasOverflow &&
!scrollbarGutterProps.hasScrollbarGutter
) {
stylelint.utils.report({
message: ruleMessages.scrollbarGutter(),
node: scrollbarGutterProps.nodeToReport,
result,
ruleName,
});
}

scrollbarGutterProps = { ...defaultScrollbarGutterProps };
}
}

/* SCROLL CHAINING */
if (options?.['scroll-chaining']) {
const overflowProperties = [
'overflow',
'overflow-x',
'overflow-y',
'overflow-inline',
'overflow-block',
];
if (
overflowProperties.includes(decl.prop) &&
(decl.value.includes('auto') || decl.value.includes('scroll'))
Expand Down
87 changes: 87 additions & 0 deletions src/rules/use-defensive-css/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,93 @@ testRule({
],
});

/* eslint-disable-next-line no-undef */
testRule({
ruleName,
config: [true, { 'scrollbar-gutter': true }],
plugins: ['./index.js'],
accept: [
{
code: `div { overflow: auto; scrollbar-gutter: auto; }`,
description: 'A container with shorthand overflow auto property.',
},
{
code: `div { overflow: hidden; }`,
description: 'A container with shorthand overflow hidden property.',
},
{
code: `div { overflow: scroll; scrollbar-gutter: stable; }`,
description: 'A container with shorthand overflow scroll property.',
},

{
code: `div { overflow: auto hidden; scrollbar-gutter: stable both-edges; }`,
description: 'A container with shorthand overflow auto hidden property.',
},
{
code: `div { overflow-x: hidden; }`,
description: 'A container with overflow-x hidden property.',
},
{
code: `div { overflow-x: auto; scrollbar-gutter: stable; }`,
description: 'A container with overflow-x auto property.',
},
{
code: `div { overflow-x: auto; overflow-y: scroll; scrollbar-gutter: stable; }`,
description:
'A container with overflow-x auto and overflow-y scroll property.',
},
{
code: `div { overflow-block: auto; scrollbar-gutter: stable; }`,
description: 'A container with overflow-block auto property.',
},
{
code: `div { overflow-inline: hidden; }`,
description: 'A container with overflow-inline hidden property.',
},
{
code: `div { overflow-anchor: auto; }`,
description:
'A container with overflow-anchor property which should be ignored.',
},
],

reject: [
{
code: `div { overflow: auto; }`,
description: 'A container with shorthand overflow auto property.',
message: messages.scrollbarGutter(),
},
{
code: `div { overflow: auto hidden; }`,
description: 'A container with shorthand overflow auto hidden property.',
message: messages.scrollbarGutter(),
},
{
code: `div { overflow-x: auto; }`,
description: 'A container with overflow-x auto property.',
message: messages.scrollbarGutter(),
},
{
code: `div { overflow-y: scroll; }`,
description: 'A container with overflow-y scroll property.',
message: messages.scrollbarGutter(),
},
{
code: `div { overflow-y: scroll; overflow-x: auto; }`,
description:
'A container with overflow-y scroll and overflow-x auto property.',
message: messages.scrollbarGutter(),
},
{
code: `div { overflow-block: scroll; overflow-inline: auto; }`,
description:
'A container with overflow-block scroll and overflow-inline auto property.',
message: messages.scrollbarGutter(),
},
],
});

/* eslint-disable-next-line no-undef */
testRule({
ruleName,
Expand Down

0 comments on commit 2193b5a

Please sign in to comment.