Skip to content

Commit 4a2174c

Browse files
liamqmaJakeLane
andauthoredAug 29, 2023
CSS Map alternative compilation approach (#1496)
* Add CSS Map * Update packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts Co-authored-by: Jake Lane <jlane2@atlassian.com> * Update packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts Co-authored-by: Jake Lane <jlane2@atlassian.com> * Remove console.log * Update function name * Add CSS Map to Parcel example * Refactor error handling * Add doc to cssMap * Regenerate cssMap Flow types * Update test * Update error messages * Implement alternative compilation method * Update packages/babel-plugin/src/types.ts Co-authored-by: Jake Lane <jlane2@atlassian.com> * Add integration tests, storybooks, and vr tests for CSS Map * Add changeset --------- Co-authored-by: Jake Lane <jlane2@atlassian.com>
1 parent 39daf9e commit 4a2174c

24 files changed

+663
-2
lines changed
 

‎.changeset/friendly-sloths-jam.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@compiled/babel-plugin': minor
3+
'@compiled/webpack-app': minor
4+
'@compiled/parcel-app': minor
5+
'@compiled/react': minor
6+
---
7+
8+
Implement the `cssMap` API to enable library users to dynamically choose a varied set of CSS rules.
Loading
Loading
Loading
Loading

‎examples/parcel/src/app.jsx

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import '@compiled/react';
99

1010
import { primary } from './constants';
1111
import Annotated from './ui/annotated';
12+
import CSSMap from './ui/css-map';
1213
import {
1314
CustomFileExtensionStyled,
1415
customFileExtensionCss,
@@ -29,5 +30,6 @@ export const App = () => (
2930
<React.Suspense fallback={<div>Loading...</div>}>
3031
<AsyncComponent />
3132
</React.Suspense>
33+
<CSSMap variant="danger">CSS Map</CSSMap>
3234
</>
3335
);

‎examples/parcel/src/ui/css-map.jsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { css, cssMap } from '@compiled/react';
2+
3+
const styles = cssMap({
4+
danger: {
5+
color: 'red',
6+
},
7+
success: {
8+
color: 'green',
9+
},
10+
});
11+
12+
export default ({ variant, children }) => <div css={css(styles[variant])}>{children}</div>;

‎examples/webpack/src/app.jsx

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Suspense, lazy } from 'react';
44

55
import { primary } from './common/constants';
66
import Annotated from './ui/annotated';
7+
import CSSMap from './ui/css-map';
78
import {
89
CustomFileExtensionStyled,
910
customFileExtensionCss,
@@ -23,5 +24,6 @@ export const App = () => (
2324
<CustomFileExtensionStyled>Custom File Extension Styled</CustomFileExtensionStyled>
2425
<div css={customFileExtensionCss}>Custom File Extension CSS</div>
2526
<Annotated />
27+
<CSSMap variant="danger">CSS Map</CSSMap>
2628
</>
2729
);

‎examples/webpack/src/ui/css-map.jsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { css, cssMap } from '@compiled/react';
2+
3+
const styles = cssMap({
4+
danger: {
5+
color: 'red',
6+
},
7+
success: {
8+
color: 'green',
9+
},
10+
});
11+
12+
export default ({ variant, children }) => <div css={css(styles[variant])}>{children}</div>;

‎packages/babel-plugin/src/babel-plugin.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as t from '@babel/types';
88
import { unique, preserveLeadingComments } from '@compiled/utils';
99

1010
import { visitClassNamesPath } from './class-names';
11+
import { visitCssMapPath } from './css-map';
1112
import { visitCssPropPath } from './css-prop';
1213
import { visitStyledPath } from './styled';
1314
import type { State } from './types';
@@ -20,6 +21,7 @@ import {
2021
isCompiledKeyframesTaggedTemplateExpression,
2122
isCompiledStyledCallExpression,
2223
isCompiledStyledTaggedTemplateExpression,
24+
isCompiledCSSMapCallExpression,
2325
} from './utils/is-compiled';
2426
import { normalizePropsUsage } from './utils/normalize-props-usage';
2527

@@ -39,6 +41,7 @@ export default declare<State>((api) => {
3941
inherits: jsxSyntax,
4042
pre() {
4143
this.sheets = {};
44+
this.cssMap = {};
4245
let cache: Cache;
4346

4447
if (this.opts.cache === true) {
@@ -150,7 +153,7 @@ export default declare<State>((api) => {
150153
return;
151154
}
152155

153-
(['styled', 'ClassNames', 'css', 'keyframes'] as const).forEach((apiName) => {
156+
(['styled', 'ClassNames', 'css', 'keyframes', 'cssMap'] as const).forEach((apiName) => {
154157
if (
155158
state.compiledImports &&
156159
t.isIdentifier(specifier.node?.imported) &&
@@ -171,6 +174,11 @@ export default declare<State>((api) => {
171174
path: NodePath<t.TaggedTemplateExpression> | NodePath<t.CallExpression>,
172175
state: State
173176
) {
177+
if (isCompiledCSSMapCallExpression(path.node, state)) {
178+
visitCssMapPath(path, { context: 'root', state, parentPath: path });
179+
return;
180+
}
181+
174182
const hasStyles =
175183
isCompiledCSSTaggedTemplateExpression(path.node, state) ||
176184
isCompiledStyledTaggedTemplateExpression(path.node, state) ||
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { TransformOptions } from '../../test-utils';
2+
import { transform as transformCode } from '../../test-utils';
3+
import { ErrorMessages } from '../index';
4+
5+
describe('css map', () => {
6+
const transform = (code: string, opts: TransformOptions = {}) =>
7+
transformCode(code, { pretty: false, ...opts });
8+
9+
const styles = `{
10+
danger: {
11+
color: 'red',
12+
backgroundColor: 'red'
13+
},
14+
success: {
15+
color: 'green',
16+
backgroundColor: 'green'
17+
}
18+
}`;
19+
20+
it('should transform css map', () => {
21+
const actual = transform(`
22+
import { cssMap } from '@compiled/react';
23+
24+
const styles = cssMap(${styles});
25+
`);
26+
27+
expect(actual).toInclude(
28+
'const styles={danger:"_syaz5scu _bfhk5scu",success:"_syazbf54 _bfhkbf54"};'
29+
);
30+
});
31+
32+
it('should error out if variants are not defined at the top-most scope of the module.', () => {
33+
expect(() => {
34+
transform(`
35+
import { cssMap } from '@compiled/react';
36+
37+
const styles = {
38+
map1: cssMap(${styles}),
39+
}
40+
`);
41+
}).toThrow(ErrorMessages.DEFINE_MAP);
42+
43+
expect(() => {
44+
transform(`
45+
import { cssMap } from '@compiled/react';
46+
47+
const styles = () => cssMap(${styles})
48+
`);
49+
}).toThrow(ErrorMessages.DEFINE_MAP);
50+
});
51+
52+
it('should error out if cssMap receives more than one argument', () => {
53+
expect(() => {
54+
transform(`
55+
import { cssMap } from '@compiled/react';
56+
57+
const styles = cssMap(${styles}, ${styles})
58+
`);
59+
}).toThrow(ErrorMessages.NUMBER_OF_ARGUMENT);
60+
});
61+
62+
it('should error out if cssMap does not receive an object', () => {
63+
expect(() => {
64+
transform(`
65+
import { cssMap } from '@compiled/react';
66+
67+
const styles = cssMap('color: red')
68+
`);
69+
}).toThrow(ErrorMessages.ARGUMENT_TYPE);
70+
});
71+
72+
it('should error out if spread element is used', () => {
73+
expect(() => {
74+
transform(`
75+
import { css, cssMap } from '@compiled/react';
76+
77+
const styles = cssMap({
78+
...base
79+
});
80+
`);
81+
}).toThrow(ErrorMessages.NO_SPREAD_ELEMENT);
82+
});
83+
84+
it('should error out if object method is used', () => {
85+
expect(() => {
86+
transform(`
87+
import { css, cssMap } from '@compiled/react';
88+
89+
const styles = cssMap({
90+
danger() {}
91+
});
92+
`);
93+
}).toThrow(ErrorMessages.NO_OBJECT_METHOD);
94+
});
95+
96+
it('should error out if variant object is dynamic', () => {
97+
expect(() => {
98+
transform(`
99+
import { css, cssMap } from '@compiled/react';
100+
101+
const styles = cssMap({
102+
danger: otherStyles
103+
});
104+
`);
105+
}).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT);
106+
});
107+
108+
it('should error out if styles include runtime variables', () => {
109+
expect(() => {
110+
transform(`
111+
import { css, cssMap } from '@compiled/react';
112+
113+
const styles = cssMap({
114+
danger: {
115+
color: canNotBeStaticallyEvulated
116+
}
117+
});
118+
`);
119+
}).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT);
120+
});
121+
122+
it('should error out if styles include conditional CSS', () => {
123+
expect(() => {
124+
transform(`
125+
import { css, cssMap } from '@compiled/react';
126+
127+
const styles = cssMap({
128+
danger: {
129+
color: canNotBeStaticallyEvulated ? 'red' : 'blue'
130+
}
131+
});
132+
`);
133+
}).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT);
134+
});
135+
});
+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import type { NodePath } from '@babel/core';
2+
import * as t from '@babel/types';
3+
4+
import type { Metadata } from '../types';
5+
import { buildCodeFrameError } from '../utils/ast';
6+
import { buildCss } from '../utils/css-builders';
7+
import { transformCssItems } from '../utils/transform-css-items';
8+
9+
// The messages are exported for testing.
10+
export enum ErrorMessages {
11+
NO_TAGGED_TEMPLATE = 'cssMap function cannot be used as a tagged template expression.',
12+
NUMBER_OF_ARGUMENT = 'cssMap function can only receive one argument.',
13+
ARGUMENT_TYPE = 'cssMap function can only receive an object.',
14+
DEFINE_MAP = 'CSS Map must be declared at the top-most scope of the module.',
15+
NO_SPREAD_ELEMENT = 'Spread element is not supported in CSS Map.',
16+
NO_OBJECT_METHOD = 'Object method is not supported in CSS Map.',
17+
STATIC_VARIANT_OBJECT = 'The variant object must be statically defined.',
18+
}
19+
20+
const createErrorMessage = (message: string): string => {
21+
return `
22+
${message}
23+
To correctly implement a CSS Map, follow the syntax below:
24+
25+
\`\`\`
26+
import { css, cssMap } from '@compiled/react';
27+
const borderStyleMap = cssMap({
28+
none: { borderStyle: 'none' },
29+
solid: { borderStyle: 'solid' },
30+
});
31+
const Component = ({ borderStyle }) => <div css={css(borderStyleMap[borderStyle])} />
32+
\`\`\`
33+
`;
34+
};
35+
36+
/**
37+
* Takes `cssMap` function expression and then transforms it to a record of class names and sheets.
38+
*
39+
* For example:
40+
* ```
41+
* const styles = cssMap({
42+
* none: { color: 'red' },
43+
* solid: { color: 'green' },
44+
* });
45+
* ```
46+
* gets transformed to
47+
* ```
48+
* const styles = {
49+
* danger: "_syaz5scu",
50+
* success: "_syazbf54",
51+
* };
52+
* ```
53+
*
54+
* @param path {NodePath} The path to be evaluated.
55+
* @param meta {Metadata} Useful metadata that can be used during the transformation
56+
*/
57+
export const visitCssMapPath = (
58+
path: NodePath<t.CallExpression> | NodePath<t.TaggedTemplateExpression>,
59+
meta: Metadata
60+
): void => {
61+
// We don't support tagged template expressions.
62+
if (t.isTaggedTemplateExpression(path.node)) {
63+
throw buildCodeFrameError(
64+
createErrorMessage(ErrorMessages.DEFINE_MAP),
65+
path.node,
66+
meta.parentPath
67+
);
68+
}
69+
70+
// We need to ensure CSS Map is declared at the top-most scope of the module.
71+
if (!t.isVariableDeclarator(path.parent) || !t.isIdentifier(path.parent.id)) {
72+
throw buildCodeFrameError(
73+
createErrorMessage(ErrorMessages.DEFINE_MAP),
74+
path.node,
75+
meta.parentPath
76+
);
77+
}
78+
79+
// We need to ensure cssMap receives only one argument.
80+
if (path.node.arguments.length !== 1) {
81+
throw buildCodeFrameError(
82+
createErrorMessage(ErrorMessages.NUMBER_OF_ARGUMENT),
83+
path.node,
84+
meta.parentPath
85+
);
86+
}
87+
88+
// We need to ensure the argument is an objectExpression.
89+
if (!t.isObjectExpression(path.node.arguments[0])) {
90+
throw buildCodeFrameError(
91+
createErrorMessage(ErrorMessages.ARGUMENT_TYPE),
92+
path.node,
93+
meta.parentPath
94+
);
95+
}
96+
97+
const totalSheets: string[] = [];
98+
path.replaceWith(
99+
t.objectExpression(
100+
path.node.arguments[0].properties.map((property) => {
101+
if (t.isSpreadElement(property)) {
102+
throw buildCodeFrameError(
103+
createErrorMessage(ErrorMessages.NO_SPREAD_ELEMENT),
104+
property.argument,
105+
meta.parentPath
106+
);
107+
}
108+
109+
if (t.isObjectMethod(property)) {
110+
throw buildCodeFrameError(
111+
createErrorMessage(ErrorMessages.NO_OBJECT_METHOD),
112+
property.key,
113+
meta.parentPath
114+
);
115+
}
116+
117+
if (!t.isObjectExpression(property.value)) {
118+
throw buildCodeFrameError(
119+
createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT),
120+
property.value,
121+
meta.parentPath
122+
);
123+
}
124+
125+
const { css, variables } = buildCss(property.value, meta);
126+
127+
if (variables.length) {
128+
throw buildCodeFrameError(
129+
createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT),
130+
property.value,
131+
meta.parentPath
132+
);
133+
}
134+
135+
const { sheets, classNames } = transformCssItems(css, meta);
136+
totalSheets.push(...sheets);
137+
138+
if (classNames.length !== 1) {
139+
throw buildCodeFrameError(
140+
createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT),
141+
property,
142+
meta.parentPath
143+
);
144+
}
145+
146+
return t.objectProperty(property.key, classNames[0]);
147+
})
148+
)
149+
);
150+
151+
// We store sheets in the meta state so that we can use it later to generate Compiled component.
152+
meta.state.cssMap[path.parent.id.name] = totalSheets;
153+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { TransformOptions } from '../../test-utils';
2+
import { transform as transformCode } from '../../test-utils';
3+
4+
describe('css map behaviour', () => {
5+
beforeAll(() => {
6+
process.env.AUTOPREFIXER = 'off';
7+
});
8+
9+
afterAll(() => {
10+
delete process.env.AUTOPREFIXER;
11+
});
12+
13+
const transform = (code: string, opts: TransformOptions = {}) =>
14+
transformCode(code, { pretty: false, ...opts });
15+
16+
const styles = `
17+
import { css, cssMap } from '@compiled/react';
18+
19+
const styles = cssMap({
20+
danger: {
21+
color: 'red',
22+
backgroundColor: 'red'
23+
},
24+
success: {
25+
color: 'green',
26+
backgroundColor: 'green'
27+
}
28+
});
29+
`;
30+
31+
it('should evaluate css map with various syntactic patterns', () => {
32+
const actual = transform(
33+
`
34+
${styles}
35+
<div css={[
36+
foo && styles['danger'],
37+
props.foo && styles['danger'],
38+
styles.success,
39+
styles['danger'],
40+
styles[variant],
41+
styles[\`danger\`],
42+
styles[isDanger?'danger':'success'],
43+
styles['dang' + 'er'],
44+
styles[props.variant],
45+
{ color: 'blue' }
46+
]} />;
47+
`,
48+
{ pretty: true }
49+
);
50+
51+
expect(actual).toMatchInlineSnapshot(`
52+
"import * as React from "react";
53+
import { ax, ix, CC, CS } from "@compiled/react/runtime";
54+
const _5 = "._syaz13q2{color:blue}";
55+
const _4 = "._bfhkbf54{background-color:green}";
56+
const _3 = "._syazbf54{color:green}";
57+
const _2 = "._bfhk5scu{background-color:red}";
58+
const _ = "._syaz5scu{color:red}";
59+
const styles = {
60+
danger: "_syaz5scu _bfhk5scu",
61+
success: "_syazbf54 _bfhkbf54",
62+
};
63+
<CC>
64+
<CS>{[_, _2, _3, _4, _5]}</CS>
65+
{
66+
<div
67+
className={ax([
68+
foo && styles["danger"],
69+
props.foo && styles["danger"],
70+
styles.success,
71+
styles["danger"],
72+
styles[variant],
73+
styles[\`danger\`],
74+
styles[isDanger ? "danger" : "success"],
75+
styles["dang" + "er"],
76+
styles[props.variant],
77+
"_syaz13q2",
78+
])}
79+
/>
80+
}
81+
</CC>;
82+
"
83+
`);
84+
});
85+
});

‎packages/babel-plugin/src/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export interface State extends PluginPass {
9898
css?: string;
9999
keyframes?: string;
100100
styled?: string;
101+
cssMap?: string;
101102
};
102103

103104
importedCompiledImports?: {
@@ -141,6 +142,11 @@ export interface State extends PluginPass {
141142
* Files that have been included in this pass.
142143
*/
143144
includedFiles: string[];
145+
146+
/**
147+
* Holds a record of currently evaluated CSS Map and its sheets in the module.
148+
*/
149+
cssMap: Record<string, string[]>;
144150
}
145151

146152
interface CommonMetadata {

‎packages/babel-plugin/src/utils/css-builders.ts

+38
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,33 @@ import type {
2828
CssItem,
2929
LogicalCssItem,
3030
SheetCssItem,
31+
CssMapItem,
3132
PartialBindingWithMeta,
3233
} from './types';
3334

35+
/**
36+
* Retrieves the leftmost identity from a given expression.
37+
*
38+
* For example:
39+
* Given a member expression "colors.primary.500", the function will return "colors".
40+
*
41+
* @param expression The expression to be evaluated.
42+
* @returns {string} The leftmost identity in the expression.
43+
*/
44+
const findBindingIdentifier = (
45+
expression: t.Expression | t.V8IntrinsicIdentifier
46+
): t.Identifier | undefined => {
47+
if (t.isIdentifier(expression)) {
48+
return expression;
49+
} else if (t.isCallExpression(expression)) {
50+
return findBindingIdentifier(expression.callee);
51+
} else if (t.isMemberExpression(expression)) {
52+
return findBindingIdentifier(expression.object);
53+
}
54+
55+
return undefined;
56+
};
57+
3458
/**
3559
* Will normalize the value of a `content` CSS property to ensure it has quotations around it.
3660
* This is done to replicate both how Styled Components behaves,
@@ -804,6 +828,13 @@ export const buildCss = (node: t.Expression | t.Expression[], meta: Metadata): C
804828
}
805829

806830
if (t.isMemberExpression(node)) {
831+
const bindingIdentifier = findBindingIdentifier(node);
832+
if (bindingIdentifier && meta.state.cssMap[bindingIdentifier.name]) {
833+
return {
834+
css: [{ type: 'map', expression: node, name: bindingIdentifier.name, css: '' }],
835+
variables: [],
836+
};
837+
}
807838
const { value, meta: updatedMeta } = evaluateExpression(node, meta);
808839
return buildCss(value, updatedMeta);
809840
}
@@ -877,6 +908,13 @@ export const buildCss = (node: t.Expression | t.Expression[], meta: Metadata): C
877908
};
878909
}
879910

911+
if (item.type === 'map') {
912+
return {
913+
...item,
914+
expression: t.logicalExpression(node.operator, expression, item.expression),
915+
} as CssMapItem;
916+
}
917+
880918
const logicalItem: LogicalCssItem = {
881919
type: 'logical',
882920
css: getItemCss(item),

‎packages/babel-plugin/src/utils/is-compiled.ts

+15
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ export const isCompiledKeyframesCallExpression = (
4747
t.isIdentifier(node.callee) &&
4848
node.callee.name === state.compiledImports?.keyframes;
4949

50+
/**
51+
* Returns `true` if the node is using `cssMap` from `@compiled/react` as a call expression
52+
*
53+
* @param node {t.Node} The node that is being checked
54+
* @param state {State} Plugin state
55+
* @returns {boolean} Whether the node is a compiled cssMap
56+
*/
57+
export const isCompiledCSSMapCallExpression = (
58+
node: t.Node,
59+
state: State
60+
): node is t.CallExpression =>
61+
t.isCallExpression(node) &&
62+
t.isIdentifier(node.callee) &&
63+
node.callee.name === state.compiledImports?.cssMap;
64+
5065
/**
5166
* Returns `true` if the node is using `keyframes` from `@compiled/react` as a tagged template expression
5267
*

‎packages/babel-plugin/src/utils/transform-css-items.ts

+6
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ const transformCssItem = (
7474
),
7575
};
7676

77+
case 'map':
78+
return {
79+
sheets: meta.state.cssMap[item.name],
80+
classExpression: item.expression,
81+
};
82+
7783
default:
7884
const css = transformCss(getItemCss(item), meta.state.opts);
7985
const className = compressClassNamesForRuntime(

‎packages/babel-plugin/src/utils/types.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,20 @@ export interface SheetCssItem {
2727
css: string;
2828
}
2929

30-
export type CssItem = UnconditionalCssItem | ConditionalCssItem | LogicalCssItem | SheetCssItem;
30+
export interface CssMapItem {
31+
name: string;
32+
type: 'map';
33+
expression: t.Expression;
34+
// We can remove this once we local transform other CssItem types
35+
css: string;
36+
}
37+
38+
export type CssItem =
39+
| UnconditionalCssItem
40+
| ConditionalCssItem
41+
| LogicalCssItem
42+
| SheetCssItem
43+
| CssMapItem;
3144

3245
export type Variable = {
3346
name: string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/** @jsxImportSource @compiled/react */
2+
// eslint-disable-next-line import/no-extraneous-dependencies
3+
import { css, cssMap } from '@compiled/react';
4+
import { render } from '@testing-library/react';
5+
6+
describe('css map', () => {
7+
const styles = cssMap({
8+
danger: {
9+
color: 'red',
10+
},
11+
success: {
12+
color: 'green',
13+
},
14+
});
15+
16+
it('should generate css based on the selected variant', () => {
17+
const Foo = ({ variant }: { variant: keyof typeof styles }) => (
18+
<div css={styles[variant]}>hello world</div>
19+
);
20+
const { getByText, rerender } = render(<Foo variant="danger" />);
21+
22+
expect(getByText('hello world')).toHaveCompiledCss('color', 'red');
23+
24+
rerender(<Foo variant="success" />);
25+
expect(getByText('hello world')).toHaveCompiledCss('color', 'green');
26+
});
27+
28+
it('should statically access a variant', () => {
29+
const Foo = () => <div css={styles.danger}>hello world</div>;
30+
const { getByText } = render(<Foo />);
31+
32+
expect(getByText('hello world')).toHaveCompiledCss('color', 'red');
33+
});
34+
35+
it('should merge styles', () => {
36+
const hover = css({ ':hover': { color: 'red' } });
37+
const Foo = () => <div css={[hover, styles.success]}>hello world</div>;
38+
const { getByText } = render(<Foo />);
39+
40+
expect(getByText('hello world')).toHaveCompiledCss('color', 'green');
41+
expect(getByText('hello world')).toHaveCompiledCss('color', 'red', { target: ':hover' });
42+
});
43+
44+
it('should conditionally apply variant', () => {
45+
const Foo = ({ isDanger }: { isDanger: boolean }) => (
46+
<div css={isDanger && styles.danger}>hello world</div>
47+
);
48+
const { getByText } = render(<Foo isDanger={true} />);
49+
50+
expect(getByText('hello world')).toHaveCompiledCss('color', 'red');
51+
});
52+
});
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Flowtype definitions for index
3+
* Generated by Flowgen from a Typescript Definition
4+
* Flowgen v1.20.1
5+
* @flow
6+
*/
7+
import type { CSSProps, CssObject } from '../types';
8+
/**
9+
* ## cssMap
10+
*
11+
* Creates a collection of named CSS rules that are statically typed and useable with other Compiled APIs.
12+
* For further details [read the documentation](https://compiledcssinjs.com/docs/api-cssmap).
13+
* @example ```
14+
* const borderStyleMap = cssMap({
15+
* none: { borderStyle: 'none' },
16+
* solid: { borderStyle: 'solid' },
17+
* });
18+
* const Component = ({ borderStyle }) => <div css={css(borderStyleMap[borderStyle])} />
19+
*
20+
* <Component borderStyle="solid" />
21+
* ```
22+
*/
23+
declare export default function cssMap<T: string, TProps>(_styles: {
24+
[key: T]: CssObject<TProps> | CssObject<TProps>[],
25+
}): { [key: T]: CSSProps<TProps> };

‎packages/react/src/css-map/index.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { CSSProps, CssObject } from '../types';
2+
import { createSetupError } from '../utils/error';
3+
4+
/**
5+
* ## cssMap
6+
*
7+
* Creates a collection of named CSS rules that are statically typed and useable with other Compiled APIs.
8+
* For further details [read the documentation](https://compiledcssinjs.com/docs/api-cssmap).
9+
*
10+
* @example
11+
* ```
12+
* const borderStyleMap = cssMap({
13+
* none: { borderStyle: 'none' },
14+
* solid: { borderStyle: 'solid' },
15+
* });
16+
* const Component = ({ borderStyle }) => <div css={css(borderStyleMap[borderStyle])} />
17+
*
18+
* <Component borderStyle="solid" />
19+
* ```
20+
*/
21+
export default function cssMap<T extends string, TProps = unknown>(
22+
_styles: Record<T, CssObject<TProps> | CssObject<TProps>[]>
23+
): Record<T, CSSProps<TProps>> {
24+
throw createSetupError();
25+
}

‎packages/react/src/index.js.flow

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ declare export { keyframes } from './keyframes';
1010
declare export { styled } from './styled';
1111
declare export { ClassNames } from './class-names';
1212
declare export { default as css } from './css';
13+
declare export { default as cssMap } from './css-map';

‎packages/react/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { keyframes } from './keyframes';
99
export { styled } from './styled';
1010
export { ClassNames } from './class-names';
1111
export { default as css } from './css';
12+
export { default as cssMap } from './css-map';
1213

1314
// Pass through the (classic) jsx runtime.
1415
// Compiled currently doesn't define its own and uses this purely to enable a local jsx namespace.

‎stories/css-map.tsx

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { cssMap } from '@compiled/react';
2+
import { useState } from 'react';
3+
4+
export default {
5+
title: 'css map',
6+
};
7+
8+
const styles = cssMap({
9+
success: {
10+
color: 'green',
11+
':hover': {
12+
color: 'DarkGreen',
13+
},
14+
'@media (max-width: 800px)': {
15+
color: 'SpringGreen',
16+
},
17+
},
18+
danger: {
19+
color: 'red',
20+
':hover': {
21+
color: 'DarkRed',
22+
},
23+
'@media (max-width: 800px)': {
24+
color: 'Crimson',
25+
},
26+
},
27+
});
28+
29+
export const DynamicVariant = (): JSX.Element => {
30+
const [variant, setVariant] = useState<keyof typeof styles>('success');
31+
32+
return (
33+
<>
34+
<div
35+
css={{
36+
'> *': {
37+
margin: '5px',
38+
},
39+
}}>
40+
<button onClick={() => setVariant('success')}>success</button>
41+
<button onClick={() => setVariant('danger')}>danger</button>
42+
<div css={styles[variant]}>hello world</div>
43+
</div>
44+
</>
45+
);
46+
};
47+
48+
export const VariantAsProp = (): JSX.Element => {
49+
const Component = ({ variant }: { variant: keyof typeof styles }) => (
50+
<div css={styles[variant]}>hello world</div>
51+
);
52+
return <Component variant={'success'} />;
53+
};
54+
55+
export const MergeStyles = (): JSX.Element => {
56+
return <div css={[styles.danger, { backgroundColor: 'green' }]}>hello world</div>;
57+
};
58+
59+
export const ConditionalStyles = (): JSX.Element => {
60+
const isDanger = true;
61+
return <div css={styles[isDanger ? 'danger' : 'success']}>hello world</div>;
62+
};

0 commit comments

Comments
 (0)
Please sign in to comment.