From dee59fb2b8159d47f386e2cdd28cd07eaeff6102 Mon Sep 17 00:00:00 2001 From: Josh Nelson Date: Thu, 3 Mar 2022 14:26:00 -0800 Subject: [PATCH] feat(atomic): add property priorities --- docs/ATOMIC_CSS.md | 26 +++++++++ packages/atomic/src/atomize.ts | 15 +++-- .../__snapshots__/babel.test.ts.snap | 41 ++++++++++++++ packages/babel/__tests__/babel.test.ts | 56 +++++++++++++++++++ 4 files changed, 130 insertions(+), 8 deletions(-) diff --git a/docs/ATOMIC_CSS.md b/docs/ATOMIC_CSS.md index 412f9061d..eceab1662 100644 --- a/docs/ATOMIC_CSS.md +++ b/docs/ATOMIC_CSS.md @@ -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: +
...
; +``` + +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 diff --git a/packages/atomic/src/atomize.ts b/packages/atomic/src/atomize.ts index 2d6f09146..89f3a7cb8 100644 --- a/packages/atomic/src/atomize.ts +++ b/packages/atomic/src/atomize.ts @@ -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) => { @@ -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, @@ -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(' '), @@ -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; } diff --git a/packages/babel/__tests__/__snapshots__/babel.test.ts.snap b/packages/babel/__tests__/__snapshots__/babel.test.ts.snap index d9684b16a..4d671605f 100644 --- a/packages/babel/__tests__/__snapshots__/babel.test.ts.snap +++ b/packages/babel/__tests__/__snapshots__/babel.test.ts.snap @@ -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'; @@ -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'; diff --git a/packages/babel/__tests__/babel.test.ts b/packages/babel/__tests__/babel.test.ts index 3d607a4b4..e340bd18f 100644 --- a/packages/babel/__tests__/babel.test.ts +++ b/packages/babel/__tests__/babel.test.ts @@ -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` @@ -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`