Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Do not allow CSS shorthands in makeStyles() calls #20573

Merged
merged 5 commits into from
Nov 23, 2021
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions rfcs/convergence/no-css-shorthands-in-make-styles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# RFC: Do not allow CSS shorthands in `makeStyles()` calls

---

@layershifter

## Summary

This RFC proposes to forbid usage of [CSS shorthands](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties) (`background`, `border`, `margin`, `padding`, etc.) in `makeStyles()` calls. Only CSS longhands (`backgroundColor`, `marginLeft`, `paddingLeft`, etc.) will be allowed for usage. This will solve the problem with expanding CSS shorthands, see [microsoft/fluentui#19402](https://github.com/microsoft/fluentui/issues/19402).

## Why expand shorthands?

```css
/* 💡 makeStyles() generates hashed classes, but it's not important in this example */
.a {
background-color: red;
}
.b {
background-color: green;
}
.c {
color: yellow;
}
```

```html
<!-- Case 1: ❌ Wrong usage -->
<div class="a b c">Hello world!</div>
<!-- Case 2: ✅ Correct usage -->
<div class="a c">Hello world!</div>
<div class="b c">Hello world!</div>
```

- In "Case 1" both classes are applied to an element: it's wrong as result is not deterministic and depends on classes order in CSS definitions (i.e. insertion order)
- In "Case 2" only single classname per CSS property is applied, result will be deterministic

[🗃 CodeSandbox demo](https://codesandbox.io/s/css-insertion-order-matters-mgt6y)

This problem is solved by [`mergeClasses()`](https://github.com/microsoft/fluentui/blob/3769833c54950aec1f54297e0730ff6b92e65147/packages/make-styles/src/mergeClasses.ts) function: it de-duplicates classes based on property name.

```jsx
// ⚠ Simplified example
function App() {
// 👇 skips "a", returns only "b c"
return <div className={mergeClasses('a', 'b', 'c')}>Hello world</div>;
}
```

To summarize: only non colliding properties should be applied to DOM elements with Atomic CSS. This works well for longhands, but there are cases when longhands and shorthands are combined:

```jsx
makeStyles({
root: { backgroundColor: 'red', background: 'green' },
});
```

👆 In this example the problem is the same: both classes will be applied, result depends on insertion order. In the same time it's annoying for customers use longhands, for example:

```tsx
makeStyles({
shorthands: {
padding: '10px 15px 5px',
border: '1px solid red',
},
longhands: {
paddingTop: '10px',
paddingRight: '15px',
paddingBottom: '5px',
paddingLeft: '15px',
borderTopWidth: '1px',
borderTopStyle: 'solid',
borderTopColor: 'red',
// ...
// 9 more properties for other side i.e. "borderLeft*", "borderBottom*", "borderRight*"
},
});
```

To allow use shorthands and longhands simultaneously `makeStyles()` currently uses [inline-style-expand-shorthand](https://github.com/robinweser/inline-style-expand-shorthand) (the same library is used in FluentUI Northstar, Fela\* & Styletron\*\* plugins).

- \* [Fela](https://fela.js.org/) is Atomic CSS-in-JS
- \*\* [Styletron](https://www.styletron.org/) has [`styletron-engine-atomic`](https://www.styletron.org/api-reference#styletron-engine-atomic) to work with Atomic CSS

**However** there are numerous issues with it (see [microsoft/fluentui#19402](https://github.com/microsoft/fluentui/issues/19402)), but the main problem is it's unclear what to do with CSS variables:

```js
// Input
const input = { padding: 'var(--foo)' };
// Output
const output = {
/* is it safe to expand it at all? */
paddingTop: 'var(--foo)',
paddingRight: 'var(--foo)',
paddingBottom: 'var(--foo)',
paddingLeft: 'var(--foo)',
};
```

- ✅ it works fine when `--foo` is atomic value, for example `4px`
- ❌ it does not work when `--foo` is compound value, for example `4px 8px`

[🔗 CodeSandbox demo](https://codesandbox.io/s/inline-style-expand-shorthand-css-variables-n9mh3)

## Requirements

### Stay with deterministic Atomic CSS

It's important for scaling our library to stay with deterministic Atomic classes. For example, we can extract common styles to a stylesheet that will be used across all products.

### Unsupported styles should fail at build, not runtime

There were multiple options covered in the original issue (see [microsoft/fluentui#19402](https://github.com/microsoft/fluentui/issues/19402)), but during offline discussion we agreed to go with build time checks instead runtime and rely on TypeScript typings.

The motivation is simple: no one sees console noise, in big applications it usually 100+ development warnings only on initial load.

### Keep CSS properties

- Keep interop with any CSS var on any CSS property
- Keep all style properties to spec with CSS properties

This means that custom syntax is a **discarded solution**, for example:

```ts
// ⚠ discarded solution, used only for an example
makeStyles({
rootA: {
// 1️⃣ no custom properties
paddingX: 'var(--foo)',
},
rootB: {
// 2️⃣ no custom values
padding: { left: 'var(--foo)', right: 'var(--foo)' },
margin: { all: 'var(--foo)' },
},
rootC: {
// 2️⃣ no custom values
padding: ['var(--foo)', 'var(--foo)'],
},
});
```

## Detailed Design or Proposal

### Ban CSS shorthands in typings

The first part of proposal is to ban CSS shorthands in typings: no shorthand properties = no need to expand them = no issues ✅

Proposed to forbid usage of all CSS shorthands ([list of CSS shorthands exported from MDN](https://csstree.github.io/docs/syntax/#report&noedit&title=CSS%20shorthand%20properties%20by%20MDN%20data&q=JGlzQXJyYXk6ID0%2BICQgIT0gKCQgKyAnJyk7Cm1kbi5wcm9wZXJ0aWVzLmVudHJpZXMoKS4oeyBpZHgsIG5hbWU6IGtleSwgLi4uKHZhbHVlIHwgeyBjb21wdXRlZCwgc3ludGF4IH0pIH0pLltjb21wdXRlZC4kaXNBcnJheSgpXQ%3D%3D&v=ewogICAgdmlldzogJ3RhYmxlJywKICAgIGNvbHM6IHsKICAgICAgICBpZHg6ICd0ZXh0OiMuaW5kZXggKyAxJywKICAgICAgICBjb21wdXRlZDogewogICAgICAgICAgICBoZWFkZXI6ICdwcm9wcycsCiAgICAgICAgICAgIGNvbnRlbnQ6ICd1bDpjb21wdXRlZCcKICAgICAgICB9CiAgICB9LAogICAgbGltaXQ6IGZhbHNlCn0%3D)) except vendor prefixed (i.e. `-ms`, `-webkit`).

```js
makeStyles({
// 🚨 TypeScript will throw
rootA: { padding: '4px' },
// ✅ TypeScript will pass
rootB: { paddingLeft: '4px', paddingRight: '4px', paddingBottom: '4px', paddingTop: '4px' },
});
```

### Create macro functions for expansion

As it will be annoying and redundant to use CSS longhands in every case we will have special functions for expansion.

```js
import { macros } from '@fluentui/react-make-styles';

makeStyles({
rootA: { paddingLeft: '4px', paddingRight: '4px', paddingBottom: '4px', paddingTop: '4px' },
rootB: {
...macros.padding('4px'),
Copy link
Member

@GeoffCoxMSFT GeoffCoxMSFT Nov 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For ex-C++ developers, the word macros has a lot of history to it :) How about calling it shorthand(), fui(), makeCss()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it also has overlap with "Babel macros". I will rename it to shorthands.*, that makes more sense to me 👍

...macros.padding('4px', '8px'),
...macros.padding('4px', '8px', '16px', 0),
},
});
```

`macros.padding('4px')` from the example above will expand it two four properties:

```js
shallowEqual(
{ paddingLeft: '4px', paddingRight: '4px', paddingBottom: '4px', paddingTop: '4px' },
macros.padding('4px'),
); // returns "true"
```

Each macro will implement arguments order & expand based on CSS spec.

For initial implementation following macros will be implemented:

- `border`
- `borderLeft`
- `borderBottom`
- `borderRight`
- `borderTop`
- `borderColor`
- `borderStyle`
- `borderRadius`
- `borderWidth`
- `margin`
- `padding`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding background.
Lower priority, it would be a bonus to have a few for common shorthand in flexbox and grid.

Copy link
Member Author

@layershifter layershifter Nov 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's one of the first things that I was thinking, but it's not obvious unfortunately as background value could also image:

// ✅ Easy-peasy
shorthands.background("green");
// ➡️ { backgroundColor: 'green' }

// 💥 Oops
shorthands.background('url("lizard.png")');
// ➡️ { backgroundColor: 'url("lizard.png")' }

https://developer.mozilla.org/en-US/docs/Web/CSS/background


Any ideas?


Additional functions can be implemented based on customer requests.

- These functions will not have significant impact on runtime version of `makeStyles()` as they are very lightweight.
- These functions will not **any** impact when Babel plugin/Webpack loader are used as they will be evaluated during build.
layershifter marked this conversation as resolved.
Show resolved Hide resolved

### Pros and Cons

- 👍 No CSS shorthands => no problems
- 👍 No new syntax
- 👎 Non obvious TypeScript errors in some cases
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider if a custom es-lint rule might help to convert shorthand to the macro function or provide better error information.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this, added 👍


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a con that we have to explain this to users of makeStyles in our documentation. It causes Fluent to seem a little less aligned with web standards since people would expect to use shorthand with raw CSS and with most frameworks.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, true, added 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid I don't have the full context so please take this with a grain of salt, but it doesn't make sense to me that complicating DX should be justified with adding documentation. As a user, I would expect a styling library to allow me to write CSS like I normally would.
Could we maybe contribute to inline-style-expand-shorthand to add the support we need instead or is this what you meant in the issue that it's not feasible in terms of complexity?

Copy link
Member Author

@layershifter layershifter Nov 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrefcdias I agree, but during offline discussion we agreed to make something always predictable instead of sometimes working. Currently it's sometimes working.


The problem is covered in the summary section of this RFC and in the original issue #19402. There are different cases: in some it's about complexity, but there is the main case when it's about possibility at all:

{ padding: 'var(--some-var)' }

👆 CSS variable can be anything (🗃️ CodeSandbox), it means that it's unsafe to expand it at all.

To summarize:

  • CSS variables are not expandable at all during build time (we don't have CSS variables in Node environment even if we would like to resolve and expand)
  • CSS can be resolved in runtime via calls of getPropertyValue() (🗃️ CodeSandbox), but will be too expensive as it should be done for every component and every usage of theme or CSS variables
    • Note 1: getPropertyValue() should be called after render as during we may not have DOM at all (👋 double rendering)
    • Note 2: we need to handle updates (How at all?)

The same problem is true for any CSS-in-JS engine that uses Atomic CSS (Compiled, Stylex, Fela, etc.).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarifying! Just wanted to make sure we covered all our options.

## Discarded Solutions

NA

## Open Issues

NA