Skip to content

Commit

Permalink
Support style compositions in selectors (#259)
Browse files Browse the repository at this point in the history
  • Loading branch information
markdalgleish committed Jul 30, 2021
1 parent c3d9d78 commit b8a6441
Show file tree
Hide file tree
Showing 30 changed files with 1,103 additions and 612 deletions.
29 changes: 29 additions & 0 deletions .changeset/curly-peaches-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@vanilla-extract/css': minor
'@vanilla-extract/esbuild-plugin': minor
'@vanilla-extract/integration': minor
'@vanilla-extract/snowpack-plugin': minor
'@vanilla-extract/vite-plugin': minor
'@vanilla-extract/webpack-plugin': minor
---

Allow the result of `composeStyles` to be used in selectors

When style compositions are used in selectors, they are now assigned an additional class so they can be uniquely identified. When selectors are processed internally, the composed classes are removed, only leaving behind the unique identifier classes. This allows you to treat them as if they were a single class within vanilla-extract selectors.

```ts
import {
style,
globalStyle,
composeStyles
} from '@vanilla-extract/css';

const background = style({ background: 'mintcream' });
const padding = style({ padding: 12 });

export const container = composeStyles(background, padding);

globalStyle(`${container} *`, {
boxSizing: 'border-box'
});
```
21 changes: 21 additions & 0 deletions .changeset/rich-crews-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@vanilla-extract/sprinkles': minor
---

Allow the result of calling `atoms` to be used in selectors

Sprinkles now uses vanilla-extract’s updated [`composeStyles`](https://github.com/seek-oss/vanilla-extract#composestyles) function internally, which means that atomic styles can be treated as if they were a single class within vanilla-extract selectors.

```ts
// styles.css.ts
import { globalStyle } from '@vanilla-extract/css';
import { atoms } from './sprinkles.css.ts';

export const container = atoms({
padding: 'small',
});

globalStyle(`${container} *`, {
boxSizing: 'border-box'
});
```
68 changes: 45 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Want to work at a higher level while maximising style re-use? Check out 🍨 [S
- [style](#style)
- [styleVariants](#styleVariants)
- [globalStyle](#globalstyle)
- [composeStyles](#composestyles)
- [createTheme](#createtheme)
- [createGlobalTheme](#createglobaltheme)
- [createThemeContract](#createthemecontract)
Expand All @@ -99,7 +100,6 @@ Want to work at a higher level while maximising style re-use? Check out 🍨 [S
- [globalFontFace](#globalfontface)
- [keyframes](#keyframes)
- [globalKeyframes](#globalkeyframes)
- [composeStyles](#composestyles)
- [Dynamic API](#dynamic-api)
- [createInlineTheme](#createinlinetheme)
- [setElementTheme](#setelementtheme)
Expand Down Expand Up @@ -437,6 +437,50 @@ globalStyle(`${parentClass} > a`, {
});
```

### composeStyles

Combines multiple styles into a single class string, while also deduplicating and removing unnecessary spaces.

```ts
import { style, composeStyles } from '@vanilla-extract/css';

const button = style({
padding: 12,
borderRadius: 8
});

export const primaryButton = composeStyles(
button,
style({ background: 'coral' })
);

export const secondaryButton = composeStyles(
button,
style({ background: 'peachpuff' })
);
```

> 💡 Styles can also be provided in shallow and deeply nested arrays, similar to [classnames.](https://github.com/JedWatson/classnames)
When style compositions are used in selectors, they are assigned an additional class so they can be uniquely identified. When selectors are processed internally, the composed classes are removed, only leaving behind the unique identifier classes. This allows you to treat them as if they were a single class within vanilla-extract selectors.

```ts
import {
style,
globalStyle,
composeStyles
} from '@vanilla-extract/css';

const background = style({ background: 'mintcream' });
const padding = style({ padding: 12 });

export const container = composeStyles(background, padding);

globalStyle(`${container} *`, {
boxSizing: 'border-box'
});
```

### createTheme

Creates a locally scoped theme class and a theme contract which can be consumed within your styles.
Expand Down Expand Up @@ -709,28 +753,6 @@ export const animated = style({
});
```

### composeStyles

Combines multiple styles into a single class string, while also deduplicating and removing unnecessary spaces.

```ts
import { style, composeStyles } from '@vanilla-extract/css';

const base = style({
padding: 12
});

export const blue = composeStyles(base, style({
background: 'blue'
}));

export const green = composeStyles(base, style({
background: 'green'
}));
```

> 💡 Styles can also be provided in shallow and deeply nested arrays. Think of it as a static version of [classnames.](https://github.com/JedWatson/classnames)
## Dynamic API

We also provide a lightweight standalone package to support dynamic runtime theming.
Expand Down
4 changes: 4 additions & 0 deletions fixtures/sprinkles/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
atoms,
mapResponsiveValue,
normalizeResponsiveValue,
preComposedAtoms,
preComposedAtomsUsedInSelector,
} from './styles.css';
import testNodes from '../test-nodes.json';

Expand All @@ -19,6 +21,8 @@ function render() {
})}">
Sprinkles
</div>
<div class="${preComposedAtoms}">Precomposed atoms</div>
<div class="${preComposedAtomsUsedInSelector}">Precomposed Atoms Used In Selector</div>
`;
}

Expand Down
15 changes: 15 additions & 0 deletions fixtures/sprinkles/src/styles.css.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { globalStyle } from '@vanilla-extract/css';
import {
createAtomicStyles,
createAtomsFn,
Expand Down Expand Up @@ -36,3 +37,17 @@ export const atoms = createAtomsFn(responsiveStyles);
export const mapResponsiveValue = createMapValueFn(responsiveStyles);
export const normalizeResponsiveValue =
createNormalizeValueFn(responsiveStyles);

export const preComposedAtoms = atoms({
display: 'block',
paddingTop: 'small',
});

export const preComposedAtomsUsedInSelector = atoms({
display: 'flex',
paddingTop: 'medium',
});

globalStyle(`body > ${preComposedAtomsUsedInSelector}`, {
background: 'red',
});
42 changes: 9 additions & 33 deletions fixtures/themed/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,51 +26,27 @@ function render() {
<div id="${testNodes.root}" class="${shadow}">
Root theme
<div id="${testNodes.rootContainer}" class="${container}">
<button id="${testNodes.rootButton}" class="${button.join(
' ',
)}">Main theme button</button>
<button id="${testNodes.rootButton}" class="${button}">Main theme button</button>
<div class="${altTheme}">
Alt theme
<div id="${testNodes.altContainer}" class="${container}">
<button id="${testNodes.altButton}" class="${button.join(
' ',
)}">Alt theme button</button>
<button id="${testNodes.altButton}" class="${button}">Alt theme button</button>
<div class="${theme}">
Back to root theme
<div id="${testNodes.nestedRootContainer}" class="${container}">
<button id="${testNodes.nestedRootButton}" class="${button.join(
' ',
)}">Main theme button</button>
<button id="${testNodes.nestedRootButton}" class="${button}">Main theme button</button>
<div style="${inlineTheme}">
Inline theme
<div id="${
testNodes.inlineThemeContainer
}" class="${container}">
<button id="${
testNodes.inlineThemeButton
}" class="${button.join(' ')} ${
opacity['1/2']
}">Inline theme button</button>
<div id="${testNodes.inlineThemeContainer}" class="${container}">
<button id="${testNodes.inlineThemeButton}" class="${button} ${opacity['1/2']}">Inline theme button</button>
<div>
Dynamic vars
<div id="${
testNodes.dynamicVarsContainer
}" class="${container}">
<button id="${
testNodes.dynamicVarsButton
}" class="${button.join(
' ',
)}">Dynamic vars button</button>
<div id="${testNodes.dynamicVarsContainer}" class="${container}">
<button id="${testNodes.dynamicVarsButton}" class="${button}">Dynamic vars button</button>
<div class="${responsiveTheme}">
Responsive theme
<div id="${
testNodes.responsiveThemeContainer
}" class="${container}">
<button id="${
testNodes.responsiveThemeButton
}" class="${button.join(
' ',
)}">Responsive theme button</button>
<div id="${testNodes.responsiveThemeContainer}" class="${container}">
<button id="${testNodes.responsiveThemeButton}" class="${button}">Responsive theme button</button>
</div>
</div>
</div>
Expand Down
17 changes: 14 additions & 3 deletions fixtures/themed/src/styles.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
globalFontFace,
keyframes,
globalKeyframes,
composeStyles,
globalStyle,
} from '@vanilla-extract/css';
import { shadow } from './shared.css';
import { vars, theme, altTheme } from './themes.css';
Expand Down Expand Up @@ -50,9 +52,13 @@ export const container = style({
},
});

export const button = [
const iDunno = composeStyles(
style({ zIndex: 1 }),
style({ position: 'relative' }),
);

export const button = composeStyles(
style({
animation: `3s infinite alternate ${slide} ease-in-out`,
fontFamily: impact,
backgroundColor: fallbackVar(
vars.colors.backgroundColor,
Expand All @@ -72,7 +78,12 @@ export const button = [
},
}),
shadow,
];
iDunno,
);

globalStyle(`body ${iDunno}`, {
animation: `3s infinite alternate ${slide} ease-in-out`,
});

const blankVar1 = createVar();
const blankVar2 = createVar();
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"dev": "preconstruct dev",
"build": "preconstruct build",
"watch": "preconstruct watch",
"start-fixture": "ts-node --log-error ./test-helpers/src/startFixtureCLI",
"start": "yarn start-fixture themed",
"start-site": "manypkg run site start",
Expand Down
14 changes: 14 additions & 0 deletions packages/css/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const mockAdapter: Adapter = {
appendCss: () => {},
registerClassName: () => {},
onEndFileScope: () => {},
registerComposition: () => {},
markCompositionUsed: () => {},
};

let adapter: Adapter = mockAdapter;
Expand All @@ -29,6 +31,18 @@ export const registerClassName: Adapter['registerClassName'] = (...props) => {
return adapter.registerClassName(...props);
};

export const registerComposition: Adapter['registerComposition'] = (
...props
) => {
return adapter.registerComposition(...props);
};

export const markCompositionUsed: Adapter['markCompositionUsed'] = (
...props
) => {
return adapter.markCompositionUsed(...props);
};

export const onEndFileScope: Adapter['onEndFileScope'] = (...props) => {
return adapter.onEndFileScope(...props);
};
6 changes: 3 additions & 3 deletions packages/css/src/composeStyles.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { composeStyles } from './composeStyles';
import { dudupeAndJoinClassList } from './composeStyles';

describe('composeStyles', () => {
describe('dudupeAndJoinClassList', () => {
it.each([
{ args: ['1'], output: '1' },
{ args: ['1 1'], output: '1' },
Expand All @@ -20,6 +20,6 @@ describe('composeStyles', () => {
{ args: ['1 2 3', '2 3 4', '1 5'], output: '1 2 3 4 5' },
{ args: [' 1 2 3 2 ', ' 2 3 4 2 ', ' 1 5 1 '], output: '1 2 3 4 5' },
])('composeStyles', ({ args, output }) => {
expect(composeStyles(...args)).toBe(output);
expect(dudupeAndJoinClassList(args)).toBe(output);
});
});
19 changes: 18 additions & 1 deletion packages/css/src/composeStyles.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { generateIdentifier } from './identifier';
import { registerComposition, registerClassName } from './adapter';

type ClassNames = string | Array<ClassNames>;

function composeStylesIntoSet(
Expand All @@ -21,10 +24,24 @@ function composeStylesIntoSet(
}
}

export function composeStyles(...classNames: Array<ClassNames>) {
function createComposition(classList: string) {
const identifier = generateIdentifier(undefined);
const compositionClassList = `${identifier} ${classList}`;

registerClassName(identifier);
registerComposition({ identifier, classList: compositionClassList });

return compositionClassList;
}

export function dudupeAndJoinClassList(classNames: Array<ClassNames>) {
const set: Set<string> = new Set();

composeStylesIntoSet(set, ...classNames);

return Array.from(set).join(' ');
}

export function composeStyles(...classNames: Array<ClassNames>) {
return createComposition(dudupeAndJoinClassList(classNames));
}
1 change: 1 addition & 0 deletions packages/css/src/identifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function generateIdentifier(debugId: string | undefined) {
// Convert ref count to base 36 for optimal hash lengths
const refCount = getAndIncrementRefCounter().toString(36);
const { filePath, packageName } = getFileScope();

const fileScopeHash = hash(
packageName ? `${packageName}${filePath}` : filePath,
);
Expand Down

0 comments on commit b8a6441

Please sign in to comment.