Skip to content

Commit 2092839

Browse files
authoredOct 21, 2021
Ensure inline strings, template literals, and css work in styled (#857)
Fixed - Static styles getting omitted when they precede an expression in string literal - Error happening when keyframes expression appeared after another expression in string literal - Ordering of styles inside string literals Added - Support string 'key:value' pattern for conditional expressions - Support inline css mixins for conditional expressions - Support generic types in CSS mixins that use props
1 parent c641543 commit 2092839

File tree

12 files changed

+310
-71
lines changed

12 files changed

+310
-71
lines changed
 

‎.changeset/rotten-baboons-wash.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@compiled/babel-plugin': minor
3+
'@compiled/react': minor
4+
---
5+
6+
Allow inline strings and inline css mixins in conditional expressions. Fix ordering of styles in template literals.

‎examples/stories/conditional-rules-styled.tsx

+53-2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,31 @@ const TextWithMixins = styled.span<TextProps>`
4747
padding: 10px;
4848
`;
4949

50+
const InlineMixin = styled.div<TextProps>`
51+
${(props) =>
52+
props.isPrimary
53+
? css`
54+
color: green;
55+
`
56+
: css({
57+
color: 'red',
58+
})}
59+
`;
60+
61+
const ComplexMixin = styled.div<TextProps>`
62+
${(props) =>
63+
props.isPrimary
64+
? css<TextProps>`
65+
color: green;
66+
font-weight: ${({ isBolded }) => (isBolded ? 'bold' : undefined)};
67+
`
68+
: 'color: red'};
69+
`;
70+
71+
const KeyValueString = styled.div<TextProps>`
72+
${(props) => (props.isPrimary ? 'color: green' : `color: red`)};
73+
`;
74+
5075
export const PrimaryTextWithTemplateLiteral = (): JSX.Element => {
5176
return <TextWithTemplateLiteral isPrimary>Hello primary</TextWithTemplateLiteral>;
5277
};
@@ -118,9 +143,35 @@ export const NotPrimaryTextWithTernaryAndBooleanObjectStyle = (): JSX.Element =>
118143
};
119144

120145
export const PrimaryTextWithMixins = (): JSX.Element => {
121-
return <TextWithMixins isPrimary> Hello primary</TextWithMixins>;
146+
return <TextWithMixins isPrimary>Hello primary</TextWithMixins>;
122147
};
123148

124149
export const SecondaryTextWithMixins = (): JSX.Element => {
125-
return <TextWithMixins> Hello secondary</TextWithMixins>;
150+
return <TextWithMixins>Hello secondary</TextWithMixins>;
126151
};
152+
153+
export const TextWithInlineMixin = (): JSX.Element => (
154+
<div>
155+
<InlineMixin isPrimary>Using css``</InlineMixin>
156+
<InlineMixin isPrimary={false}>Using css()</InlineMixin>
157+
</div>
158+
);
159+
160+
export const TextWithComplexMixin = (): JSX.Element => (
161+
<div>
162+
<ComplexMixin isPrimary isBolded>
163+
Primary text using mixin with bold condition
164+
</ComplexMixin>
165+
<ComplexMixin isPrimary isBolded={false}>
166+
Primary text using mixin without bold condition
167+
</ComplexMixin>
168+
<ComplexMixin isPrimary={false}>Secondary text</ComplexMixin>
169+
</div>
170+
);
171+
172+
export const TextWithKeyValueString = (): JSX.Element => (
173+
<div>
174+
<KeyValueString isPrimary>color: green</KeyValueString>
175+
<KeyValueString isPrimary={false}>color: red</KeyValueString>
176+
</div>
177+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { transform } from '../../__tests__/test-utils';
2+
3+
describe('Keyframes', () => {
4+
it('places classes in given order when static styles precede keyframes expression', () => {
5+
const actual = transform(`
6+
import { styled, keyframes } from '@compiled/react';
7+
8+
const animation = keyframes\`
9+
from { top: 0; }
10+
to { top: 100px; }
11+
\`;
12+
13+
const ListItem = styled.div\`
14+
font-size: 20px;
15+
border-radius: 3px;
16+
animation: \${animation};
17+
\`;
18+
`);
19+
20+
expect(actual).toIncludeMultiple([
21+
'._1wybgktf{font-size:20px}',
22+
'._2rko1l7b{border-radius:3px}',
23+
'._y44v1bcx{-webkit-animation:kfwl3rt;animation:kfwl3rt}',
24+
'{ax(["_1wybgktf _2rko1l7b _y44v1bcx", props.className])}',
25+
]);
26+
});
27+
28+
it('places classes in given order when keyframes expression precedes static styles', () => {
29+
const actual = transform(`
30+
import { styled, keyframes } from '@compiled/react';
31+
32+
const animation = keyframes({
33+
from: { top: 0 },
34+
to: { top: '100px' },
35+
});
36+
37+
const ListItem = styled.div\`
38+
animation: \${animation};
39+
font-size: 20px;
40+
border-radius: 3px;
41+
\`;
42+
`);
43+
44+
expect(actual).toIncludeMultiple([
45+
'._y44v178k{-webkit-animation:kvif0b9;animation:kvif0b9}',
46+
'._1wybgktf{font-size:20px}',
47+
'._2rko1l7b{border-radius:3px}',
48+
'{ax(["_y44v178k _1wybgktf _2rko1l7b", props.className])}',
49+
]);
50+
});
51+
52+
it('evaluates any expressions that precede a keyframes expression', () => {
53+
const actual = transform(`
54+
import { styled, keyframes } from '@compiled/react';
55+
56+
const color = 'red';
57+
58+
const animation = keyframes\`
59+
from { top: 0; }
60+
to { top: 100px; }
61+
\`;
62+
63+
const ListItem = styled.div\`
64+
color: \${color};
65+
animation: \${animation};
66+
\`;
67+
`);
68+
69+
expect(actual).toIncludeMultiple([
70+
'._syaz5scu{color:red}',
71+
'._y44v1bcx{-webkit-animation:kfwl3rt;animation:kfwl3rt}',
72+
'{ax(["_syaz5scu _y44v1bcx", props.className])}',
73+
]);
74+
});
75+
76+
it('evaluates keyframes expression when it precedes another expression', () => {
77+
const actual = transform(`
78+
import { styled, keyframes } from '@compiled/react';
79+
80+
const color = 'red';
81+
82+
const animation = keyframes\`
83+
from { top: 0; }
84+
to { top: 100px; }
85+
\`;
86+
87+
const ListItem = styled.div\`
88+
animation: \${animation};
89+
color: \${color};
90+
\`;
91+
`);
92+
93+
expect(actual).toIncludeMultiple([
94+
'._y44v1bcx{-webkit-animation:kfwl3rt;animation:kfwl3rt}',
95+
'._syaz5scu{color:red}',
96+
'{ax(["_y44v1bcx _syaz5scu", props.className])}',
97+
]);
98+
});
99+
});

‎packages/babel-plugin/src/styled/__tests__/behaviour.test.tsx

+32
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,38 @@ describe('styled component behaviour', () => {
463463
);
464464
});
465465

466+
it('should apply conditional CSS when using "key: value" in string form', () => {
467+
const actual = transform(`
468+
import { styled } from '@compiled/react';
469+
470+
const Component = styled.div\`
471+
\${props => props.isPrimary ? 'color: green' : \`color: red\`};
472+
\`;
473+
`);
474+
475+
expect(actual).toIncludeMultiple([
476+
'._syazbf54{color:green}',
477+
'._syaz5scu{color:red}',
478+
'className={ax(["_syaz5scu",props.isPrimary&&"_syazbf54",props.className])}',
479+
]);
480+
});
481+
482+
it('should apply conditional CSS when using inline mixins', () => {
483+
const actual = transform(`
484+
import { styled, css } from '@compiled/react';
485+
486+
const Component = styled.div\`
487+
\${props => props.isPrimary ? css\`color: green\` : css({ color: 'red' })};
488+
\`;
489+
`);
490+
491+
expect(actual).toIncludeMultiple([
492+
'._syazbf54{color:green}',
493+
'._syaz5scu{color:red}',
494+
'className={ax(["_syaz5scu",props.isPrimary&&"_syazbf54",props.className])}',
495+
]);
496+
});
497+
466498
it('should apply unconditional before and after a conditional css rule with template literal', () => {
467499
const actual = transform(`
468500
import { styled } from '@compiled/react';

‎packages/babel-plugin/src/styled/__tests__/string-literal.test.tsx

+44
Original file line numberDiff line numberDiff line change
@@ -730,4 +730,48 @@ describe('styled component string literal', () => {
730730

731731
expect(actual).toInclude('{as:C="span",style,isLoading,loading,...props}');
732732
});
733+
734+
it('should place classes in given order when static styles precede expression', () => {
735+
const actual = transform(`
736+
import { styled, keyframes } from '@compiled/react';
737+
import colors from 'colors';
738+
739+
const color = { color: colors.color };
740+
741+
const ListItem = styled.div\`
742+
font-size: 20px;
743+
border-radius: 3px;
744+
\${color};
745+
\`;
746+
`);
747+
748+
expect(actual).toIncludeMultiple([
749+
'._1wybgktf{font-size:20px}',
750+
'._2rko1l7b{border-radius:3px}',
751+
'._syaz1qjj{color:var(--_pvyxdf)}',
752+
'{ax(["_1wybgktf _2rko1l7b _syaz1qjj",props.className])}',
753+
]);
754+
});
755+
756+
it('should place classes in given order when expression precedes static styles', () => {
757+
const actual = transform(`
758+
import { styled, keyframes } from '@compiled/react';
759+
import colors from 'colors';
760+
761+
const color = { color: colors.color };
762+
763+
const ListItem = styled.div\`
764+
\${color};
765+
font-size: 20px;
766+
border-radius: 3px;
767+
\`;
768+
`);
769+
770+
expect(actual).toIncludeMultiple([
771+
'._syaz1qjj{color:var(--_pvyxdf)}',
772+
'._1wybgktf{font-size:20px}',
773+
'._2rko1l7b{border-radius:3px}',
774+
'{ax(["_syaz1qjj _1wybgktf _2rko1l7b",props.className])}',
775+
]);
776+
});
733777
});

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

+49-45
Original file line numberDiff line numberDiff line change
@@ -237,42 +237,44 @@ const callbackIfFileIncluded = (meta: Metadata, next: Metadata) => {
237237
* @param meta {Metadata} Useful metadata that can be used during the transformation
238238
*/
239239
const extractConditionalExpression = (node: t.ConditionalExpression, meta: Metadata): CSSOutput => {
240-
const variables: CSSOutput['variables'] = [];
241-
const css: CSSOutput['css'] = [];
242-
let logicalItem: LogicalCssItem[] = [];
243-
244-
if (t.isObjectExpression(node.consequent)) {
245-
const consequent = buildCss(node.consequent, meta);
246-
logicalItem = getLogicalItemFromConditionalExpression(consequent.css, node, 'consequent');
240+
const conditionalPaths: ['consequent', 'alternate'] = ['consequent', 'alternate'];
241+
const css = [];
242+
const variables = [];
247243

248-
css.push(...logicalItem);
249-
variables.push(...consequent.variables);
250-
} else if (t.isIdentifier(node.consequent)) {
251-
const { value: interpolation, meta: updatedMeta } = evaluateExpression(node.consequent, meta);
244+
for (const path of conditionalPaths) {
245+
const pathNode = node[path];
246+
let buildOutput;
247+
let newCssItems;
252248

253-
if (isCompiledCSSTemplateLiteral(interpolation, updatedMeta)) {
254-
const consequent = buildCss(interpolation, updatedMeta);
255-
logicalItem = getLogicalItemFromConditionalExpression(consequent.css, node, 'consequent');
256-
257-
css.push(...logicalItem);
258-
variables.push(...consequent.variables);
249+
if (
250+
t.isObjectExpression(pathNode) ||
251+
// Check if string resembles CSS `property: value`
252+
(t.isStringLiteral(pathNode) && pathNode.value.includes(':')) ||
253+
t.isTemplateLiteral(pathNode) ||
254+
isCompiledCSSTemplateLiteral(pathNode, meta) ||
255+
isCompiledCSSCallExpression(pathNode, meta)
256+
) {
257+
buildOutput = buildCss(pathNode, meta);
258+
newCssItems =
259+
// Only mark truthy(consequent) condition as conditional CSS.
260+
// Falsey(alternate) will always be added to serve as default values
261+
path === 'consequent'
262+
? getLogicalItemFromConditionalExpression(buildOutput.css, node, path)
263+
: buildOutput.css;
264+
} else if (t.isIdentifier(pathNode)) {
265+
const { value: interpolation, meta: updatedMeta } = evaluateExpression(pathNode, meta);
266+
267+
if (
268+
isCompiledCSSTemplateLiteral(interpolation, updatedMeta) ||
269+
isCompiledCSSCallExpression(interpolation, updatedMeta)
270+
) {
271+
buildOutput = buildCss(interpolation, updatedMeta);
272+
newCssItems = getLogicalItemFromConditionalExpression(buildOutput.css, node, path);
273+
}
259274
}
260-
}
261-
262-
if (t.isObjectExpression(node.alternate)) {
263-
const alternate = extractObjectExpression(node.alternate, meta);
264275

265-
css.push(...alternate.css);
266-
variables.push(...alternate.variables);
267-
} else if (t.isIdentifier(node.alternate)) {
268-
const { value: interpolation, meta: updatedMeta } = evaluateExpression(node.alternate, meta);
269-
if (isCompiledCSSTemplateLiteral(interpolation, updatedMeta)) {
270-
const alternate = buildCss(interpolation, updatedMeta);
271-
logicalItem = getLogicalItemFromConditionalExpression(alternate.css, node, 'alternate');
272-
273-
css.push(...logicalItem);
274-
variables.push(...alternate.variables);
275-
}
276+
css.push(...(newCssItems || []));
277+
variables.push(...(buildOutput?.variables ?? []));
276278
}
277279

278280
return { css: mergeSubsequentUnconditionalCssItems(css), variables };
@@ -485,36 +487,38 @@ const extractTemplateLiteral = (node: t.TemplateLiteral, meta: Metadata): CSSOut
485487
if (
486488
t.isObjectExpression(interpolation) ||
487489
isCompiledCSSTemplateLiteral(interpolation, meta) ||
490+
isCompiledCSSCallExpression(interpolation, meta) ||
488491
(t.isArrowFunctionExpression(nodeExpression) &&
489492
t.isConditionalExpression(nodeExpression.body))
490493
) {
491494
// We found something that looks like CSS.
492495
const result = buildCss(interpolation, updatedMeta);
493-
css.push(...result.css);
494-
variables.push(...result.variables);
495-
496-
if (!t.isArrowFunctionExpression(nodeExpression) && quasi.hasOwnProperty('value')) {
497-
// To ensure that CSS is generated for declaration before a mixin
498-
return acc + quasi.value.raw;
499-
}
500496

501-
if (result.css.length > 0) {
502-
return acc;
497+
if (result.css.length) {
498+
// Add previous accumulative CSS first before CSS from expressions
499+
css.push({ type: 'unconditional', css: acc + quasi.value.raw }, ...result.css);
500+
variables.push(...result.variables);
501+
// Reset acc as we just added them
502+
return '';
503503
}
504504
}
505505

506506
if (
507507
isCompiledKeyframesCallExpression(interpolation, updatedMeta) ||
508508
isCompiledKeyframesTaggedTemplateExpression(interpolation, updatedMeta)
509509
) {
510-
const result = extractKeyframes(interpolation, {
510+
const {
511+
css: [keyframesSheet, unconditionalKeyframesItem],
512+
variables: keyframeVariables,
513+
} = extractKeyframes(interpolation, {
511514
...updatedMeta,
512515
prefix: quasi.value.raw,
513516
suffix: '',
514517
});
515-
css.push(...result.css);
516-
variables.push(...result.variables);
517-
return acc;
518+
519+
css.push(keyframesSheet);
520+
variables.push(...keyframeVariables);
521+
return acc + unconditionalKeyframesItem.css;
518522
}
519523

520524
const { expression, variableName } = getVariableDeclaratorValueForOwnPath(nodeExpression, meta);

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Flowgen v1.14.1
55
* @flow
66
*/
7-
import type { CSSProps } from '../types';
7+
import type { BasicTemplateInterpolations, CSSProps, FunctionInterpolation } from '../types';
88
/**
99
* Create styles that can be re-used between components with a template literal.
1010
*
@@ -17,9 +17,9 @@ import type { CSSProps } from '../types';
1717
* @param css
1818
* @param values
1919
*/
20-
declare export default function css(
20+
declare export default function css<T>(
2121
_css: $ReadOnlyArray<string>,
22-
..._values: (string | number)[]
22+
..._values: (BasicTemplateInterpolations | FunctionInterpolation<T>)[]
2323
): CSSProps;
2424
/**
2525
* Create styles that can be re-used between components with an object

‎packages/react/src/css/index.tsx

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createSetupError } from '../utils/error';
2-
import type { CSSProps } from '../types';
2+
import type { BasicTemplateInterpolations, CSSProps, FunctionInterpolation } from '../types';
33

44
/**
55
* Create styles that can be re-used between components with a template literal.
@@ -14,7 +14,10 @@ import type { CSSProps } from '../types';
1414
* @param css
1515
* @param values
1616
*/
17-
export default function css(_css: TemplateStringsArray, ..._values: (string | number)[]): CSSProps;
17+
export default function css<T = void>(
18+
_css: TemplateStringsArray,
19+
..._values: (BasicTemplateInterpolations | FunctionInterpolation<T>)[]
20+
): CSSProps;
1821

1922
/**
2023
* Create styles that can be re-used between components with an object
@@ -30,9 +33,9 @@ export default function css(_css: TemplateStringsArray, ..._values: (string | nu
3033
*/
3134
export default function css(_css: CSSProps): CSSProps;
3235

33-
export default function css(
36+
export default function css<T = void>(
3437
_css: TemplateStringsArray | CSSProps,
35-
..._values: (string | number)[]
38+
..._values: (BasicTemplateInterpolations | FunctionInterpolation<T>)[]
3639
): CSSProps {
3740
throw createSetupError();
3841
}

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

+3-6
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@
55
* @flow
66
*/
77
import type { ComponentType } from 'react';
8-
import type { BasicTemplateInterpolations, CssFunction, CSSProps } from '../types';
9-
export interface FunctionIterpolation<TProps> {
10-
(props: TProps): CSSProps | string | number | boolean | void;
11-
}
8+
import type { BasicTemplateInterpolations, CssFunction, FunctionInterpolation } from '../types';
129
/**
1310
* Typing for the CSS object.
1411
*/
15-
export type CssObject<TProps> = CssFunction<FunctionIterpolation<TProps>>;
12+
export type CssObject<TProps> = CssFunction<FunctionInterpolation<TProps>>;
1613
/**
1714
* Extra props added to the output Styled Component.
1815
*/
@@ -21,7 +18,7 @@ export type StyledProps = {
2118
};
2219
export type Interpolations<TProps: mixed> = (
2320
| BasicTemplateInterpolations
24-
| FunctionIterpolation<TProps>
21+
| FunctionInterpolation<TProps>
2522
| CssObject<TProps>
2623
| CssObject<TProps>[]
2724
)[];

‎packages/react/src/styled/index.tsx

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
import type { ComponentType } from 'react';
22
import { createSetupError } from '../utils/error';
3-
import type { BasicTemplateInterpolations, CssFunction, CSSProps } from '../types';
4-
5-
export interface FunctionIterpolation<TProps> {
6-
(props: TProps): CSSProps | string | number | boolean | undefined;
7-
}
3+
import type { BasicTemplateInterpolations, CssFunction, FunctionInterpolation } from '../types';
84

95
/**
106
* Typing for the CSS object.
117
*/
12-
export type CssObject<TProps> = CssFunction<FunctionIterpolation<TProps>>;
8+
export type CssObject<TProps> = CssFunction<FunctionInterpolation<TProps>>;
139

1410
/**
1511
* Extra props added to the output Styled Component.
@@ -20,7 +16,7 @@ export interface StyledProps {
2016

2117
export type Interpolations<TProps extends unknown> = (
2218
| BasicTemplateInterpolations
23-
| FunctionIterpolation<TProps>
19+
| FunctionInterpolation<TProps>
2420
| CssObject<TProps>
2521
| CssObject<TProps>[]
2622
)[];

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import * as CSS from 'csstype';
99
* Typing for the interpolations.
1010
*/
1111
export type BasicTemplateInterpolations = string | number;
12+
export interface FunctionInterpolation<TProps> {
13+
(props: TProps): CSSProps | BasicTemplateInterpolations | boolean | void;
14+
}
1215
/**
1316
* These are all the CSS props that will exist.
1417
*/
15-
export type CSSProps = CSS.Properties<string | number>;
18+
export type CSSProps = CSS.Properties<BasicTemplateInterpolations>;
1619
export type AnyKeyCssProps<TValue> = {
17-
[key: string]: AnyKeyCssProps<TValue> | CSSProps | string | number | TValue,
20+
[key: string]: AnyKeyCssProps<TValue> | CSSProps | BasicTemplateInterpolations | TValue,
1821
...
1922
};
2023
export type CssFunction<TValue = void> =

‎packages/react/src/types.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import type * as CSS from 'csstype';
55
*/
66
export type BasicTemplateInterpolations = string | number;
77

8+
export interface FunctionInterpolation<TProps> {
9+
(props: TProps): CSSProps | BasicTemplateInterpolations | boolean | undefined;
10+
}
11+
812
/**
913
* These are all the CSS props that will exist.
1014
*/
11-
export type CSSProps = CSS.Properties<string | number>;
15+
export type CSSProps = CSS.Properties<BasicTemplateInterpolations>;
1216

1317
export type AnyKeyCssProps<TValue> = {
14-
[key: string]: AnyKeyCssProps<TValue> | CSSProps | string | number | TValue;
18+
[key: string]: AnyKeyCssProps<TValue> | CSSProps | BasicTemplateInterpolations | TValue;
1519
};
1620

1721
export type CssFunction<TValue = void> =

0 commit comments

Comments
 (0)
Please sign in to comment.