Skip to content

Commit

Permalink
feat(atomic): add property priorities
Browse files Browse the repository at this point in the history
  • Loading branch information
jpnelson committed Mar 3, 2022
1 parent bc89d08 commit dee59fb
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 8 deletions.
26 changes: 26 additions & 0 deletions docs/ATOMIC_CSS.md
Expand Up @@ -119,6 +119,32 @@ export const mediaQuery = css`

These can also be combined for further nesting.

### Property priorities

Using atomic CSS, longhand properties such as `padding-top` have a _higher_ priority than their shorthand equivalents like `padding-top`. For example:

```ts
import { css } from '@linaria/atomic';

const noPadding = css`
padding: 0;
`;

const paddingTop = css`
padding-top: 5px:
`;

// In react:
<div className={cx(noPadding, paddingTop)}>...</div>;
```

The result will be that the div has `padding-top: 5px;`, as that is higher priority than `padding: 0`.

The way linaria achieves this is through property priorities. See [this blog post](https://weser.io/blog/the-shorthand-longhand-problem-in-atomic-css) for more details on the concept, and the problems it solves. The method used in linaria is to increase the specificity of the rules: see `@linaria/atomic`'s `propertyPriority` function for a list of longhand and shorthand properties supported by this. The basic rules are:

- Longhand properties have higher priority than shorthand properties
- Declarations in @media rules (and any @-rule, such as @supports) have higher priority than those outside of them

## Use cases

### Reducing number of rules
Expand Down
15 changes: 7 additions & 8 deletions packages/atomic/src/atomize.ts
Expand Up @@ -2,6 +2,7 @@ import postcss, { Document, AtRule, Container, Rule } from 'postcss';
import { slugify } from '@linaria/utils';
import stylis from 'stylis';
import { all as knownProperties } from 'known-css-properties';
import { getPropertyPriority } from './propertyPriority';

const knownPropertiesMap = knownProperties.reduce(
(acc: { [property: string]: number }, property, i) => {
Expand Down Expand Up @@ -48,11 +49,13 @@ export default function atomize(cssText: string) {
let thisParent: Document | Container | undefined = decl.parent;
const parents: (Document | Container)[] = [];
const atomicProperty = [decl.prop];
let hasAtRule = false;

// Traverse the declarations parents, and collect them all.
while (thisParent && thisParent !== stylesheet) {
parents.unshift(thisParent);
if (thisParent.type === 'atrule') {
hasAtRule = true;
// @media queries, @supports etc.
atomicProperty.push(
(thisParent as AtRule).name,
Expand Down Expand Up @@ -87,7 +90,9 @@ export default function atomize(cssText: string) {
const valueSlug = slugify(decl.value);
const className = `atm_${propertySlug}_${valueSlug}`;

const processedCss = stylis(`.${className}`, css);
const propertyPriority =
getPropertyPriority(decl.prop) + (hasAtRule ? 1 : 0);
const processedCss = stylis(`.${className}`.repeat(propertyPriority), css);

atomicRules.push({
property: atomicProperty.join(' '),
Expand All @@ -96,11 +101,5 @@ export default function atomize(cssText: string) {
});
});

// The most common reason for sorting these rules is so that @media queries appear after rules that they might override. For example,
// .atm_foo { background: red; }
// @media (max-width: 500px) { .atm_bar { background: white; } }
// it's very likely that the media atom should come after the other background atom.
// This is necessary because media queries don't add specificity to the rules.
// In general also, this deterministic ordering is helpful.
return atomicRules.sort((a, b) => (a.cssText > b.cssText ? 1 : -1));
return atomicRules;
}
41 changes: 41 additions & 0 deletions packages/babel/__tests__/__snapshots__/babel.test.ts.snap
Expand Up @@ -38,6 +38,27 @@ Dependencies: NA
`;

exports[`compiles atomic css with at-rules and property priorities 1`] = `
"/* @flow */
import { css } from '@linaria/atomic';
import { styled } from '@linaria/react';
const x = \\"atm_ci1k5c_i2wt44 atm_le_1v6z61t atm_1h9nsec_idpfg4 atm_7xgmf7_14y27yu\\";
console.log(x);"
`;

exports[`compiles atomic css with at-rules and property priorities 2`] = `
CSS:
.atm_ci1k5c_i2wt44.atm_ci1k5c_i2wt44:enabled{padding-left:6px;}
.atm_le_1v6z61t.atm_le_1v6z61t{padding-bottom:7px;}
@media (max-width:500px){.atm_1h9nsec_idpfg4{padding:0;}}
@media (min-width:300px){.atm_7xgmf7_14y27yu.atm_7xgmf7_14y27yu:hover{padding-top:5px;}}
Dependencies: NA
`;

exports[`compiles atomic css with at-rules and pseudo classes 1`] = `
"/* @flow */
import { css } from '@linaria/atomic';
Expand Down Expand Up @@ -88,6 +109,26 @@ Dependencies: NA
`;

exports[`compiles atomic css with property priorities 1`] = `
"/* @flow */
import { css } from '@linaria/atomic';
import { styled } from '@linaria/react';
const y = \\"atm_gz_14y27yu\\";
const x = \\"atm_gi_idpfg4\\";
console.log(x, y);"
`;

exports[`compiles atomic css with property priorities 2`] = `
CSS:
.atm_gz_14y27yu.atm_gz_14y27yu{margin-left:5px;}
.atm_gi_idpfg4{margin:0;}
Dependencies: NA
`;

exports[`does not include styles if not referenced anywhere 1`] = `
"import { css } from '@linaria/core';
import { styled } from '@linaria/react';
Expand Down
56 changes: 56 additions & 0 deletions packages/babel/__tests__/babel.test.ts
Expand Up @@ -579,6 +579,31 @@ it('compiles atomic css', async () => {
expect(metadata).toMatchSnapshot();
});

it('compiles atomic css with property priorities', async () => {
const { code, metadata } = await transpile(
dedent`
/* @flow */
import { css } from '@linaria/atomic';
import { styled } from '@linaria/react';
const y = css\`
margin-left: 5px;
\`;
const x = css\`
margin: 0;
\`;
console.log(x, y);
`
);

expect(code).toMatchSnapshot();
expect(metadata).toMatchSnapshot();
});

it('compiles atomic css with at-rules and pseudo classes', async () => {
const { code, metadata } = await transpile(
dedent`
Expand Down Expand Up @@ -612,6 +637,37 @@ it('compiles atomic css with at-rules and pseudo classes', async () => {
expect(metadata).toMatchSnapshot();
});

it('compiles atomic css with at-rules and property priorities', async () => {
const { code, metadata } = await transpile(
dedent`
/* @flow */
import { css } from '@linaria/atomic';
import { styled } from '@linaria/react';
const x = css\`
@media (max-width: 500px) {
padding: 0;
}
@media (min-width: 300px) {
&:hover {
padding-top: 5px;
}
}
&:enabled {
padding-left: 6px;
}
padding-bottom: 7px;
\`;
console.log(x);
`
);

expect(code).toMatchSnapshot();
expect(metadata).toMatchSnapshot();
});

it('compiles atomic css with keyframes', async () => {
const { code, metadata } = await transpile(
dedent`
Expand Down

0 comments on commit dee59fb

Please sign in to comment.