/
styled.ts
272 lines (232 loc) · 8.38 KB
/
styled.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* This file contains an runtime version of `styled` component. Responsibilities of the component are:
* - returns ReactElement based on HTML tag used with `styled` or custom React Component
* - injects classNames for the returned component
* - injects CSS variables used to define dynamic styles based on props
*/
import validAttr from '@emotion/is-prop-valid';
import React from 'react';
import { cx } from '@linaria/core';
import type { CSSProperties } from '@linaria/core';
import type { StyledMeta } from '@linaria/tags';
export type NoInfer<A> = [A][A extends any ? 0 : never];
type Component<TProps> =
| ((props: TProps) => unknown)
| { new (props: TProps): unknown };
type Has<T, TObj> = [T] extends [TObj] ? T : T & TObj;
type Options = {
atomic?: boolean;
class: string;
name: string;
propsAsIs: boolean;
vars?: {
[key: string]: [
string | number | ((props: unknown) => string | number),
string | void
];
};
};
const isCapital = (ch: string): boolean => ch.toUpperCase() === ch;
const filterKey =
<TExclude extends keyof any>(keys: TExclude[]) =>
<TAll extends keyof any>(key: TAll): key is Exclude<TAll, TExclude> =>
keys.indexOf(key as any) === -1;
export const omit = <T extends Record<string, unknown>, TKeys extends keyof T>(
obj: T,
keys: TKeys[]
): Omit<T, TKeys> => {
const res = {} as Omit<T, TKeys>;
Object.keys(obj)
.filter(filterKey(keys))
.forEach((key) => {
res[key] = obj[key];
});
return res;
};
function filterProps<T extends Record<string, unknown>, TKeys extends keyof T>(
asIs: boolean,
props: T,
omitKeys: TKeys[]
): Partial<Omit<T, TKeys>> {
const filteredProps = omit(props, omitKeys) as Partial<T>;
if (!asIs) {
/**
* A failsafe check for esModule import issues
* if validAttr !== 'function' then it is an object of { default: Fn }
*/
const interopValidAttr =
typeof validAttr === 'function' ? { default: validAttr } : validAttr;
Object.keys(filteredProps).forEach((key) => {
if (!interopValidAttr.default(key)) {
// Don't pass through invalid attributes to HTML elements
delete filteredProps[key];
}
});
}
return filteredProps;
}
const warnIfInvalid = (value: unknown, componentName: string) => {
if (process.env.NODE_ENV !== 'production') {
if (
typeof value === 'string' ||
// eslint-disable-next-line no-self-compare,no-restricted-globals
(typeof value === 'number' && isFinite(value))
) {
return;
}
const stringified =
typeof value === 'object' ? JSON.stringify(value) : String(value);
// eslint-disable-next-line no-console
console.warn(
`An interpolation evaluated to '${stringified}' in the component '${componentName}', which is probably a mistake. You should explicitly cast or transform the value to a string.`
);
}
};
interface IProps {
className?: string;
style?: Record<string, string>;
[props: string]: unknown;
}
// Property-based interpolation is allowed only if `style` property exists
function styled<
TProps extends Has<TMustHave, { style?: React.CSSProperties }>,
TMustHave extends { style?: React.CSSProperties },
TConstructor extends Component<TProps>
>(
componentWithStyle: TConstructor & Component<TProps>
): ComponentStyledTagWithInterpolation<TProps, TConstructor>;
// If styled wraps custom component, that component should have className property
function styled<
TProps extends Has<TMustHave, { className?: string }>,
TMustHave extends { className?: string },
TConstructor extends Component<TProps>
>(
componentWithoutStyle: TConstructor & Component<TProps>
): ComponentStyledTagWithoutInterpolation<TConstructor>;
function styled<TName extends keyof JSX.IntrinsicElements>(
tag: TName
): HtmlStyledTag<TName>;
function styled(
component: 'The target component should have a className prop'
): never;
function styled(tag: any): any {
return (options: Options) => {
if (process.env.NODE_ENV !== 'production') {
if (Array.isArray(options)) {
// We received a strings array since it's used as a tag
throw new Error(
'Using the "styled" tag in runtime is not supported. Make sure you have set up the Babel plugin correctly. See https://github.com/callstack/linaria#setup'
);
}
}
const render = (props: any, ref: any) => {
const { as: component = tag, class: className } = props;
const shouldKeepProps =
options.propsAsIs === undefined
? !(
typeof component === 'string' &&
component.indexOf('-') === -1 &&
!isCapital(component[0])
)
: options.propsAsIs;
const filteredProps: IProps = filterProps(shouldKeepProps, props, [
'as',
'class',
]);
filteredProps.ref = ref;
filteredProps.className = options.atomic
? cx(options.class, filteredProps.className || className)
: cx(filteredProps.className || className, options.class);
const { vars } = options;
if (vars) {
const style: Record<string, string> = {};
// eslint-disable-next-line guard-for-in,no-restricted-syntax
for (const name in vars) {
const variable = vars[name];
const result = variable[0];
const unit = variable[1] || '';
const value = typeof result === 'function' ? result(props) : result;
warnIfInvalid(value, options.name);
style[`--${name}`] = `${value}${unit}`;
}
const ownStyle = filteredProps.style || {};
const keys = Object.keys(ownStyle);
if (keys.length > 0) {
keys.forEach((key) => {
style[key] = ownStyle[key];
});
}
filteredProps.style = style;
}
if ((tag as any).__linaria && tag !== component) {
// If the underlying tag is a styled component, forward the `as` prop
// Otherwise the styles from the underlying component will be ignored
filteredProps.as = component;
return React.createElement(tag, filteredProps);
}
return React.createElement(component, filteredProps);
};
const Result = React.forwardRef
? React.forwardRef(render)
: // React.forwardRef won't available on older React versions and in Preact
// Fallback to a innerRef prop in that case
(props: any) => {
const rest = omit(props, ['innerRef']);
return render(rest, props.innerRef);
};
(Result as any).displayName = options.name;
// These properties will be read by the babel plugin for interpolation
(Result as any).__linaria = {
className: options.class,
extends: tag,
};
return Result;
};
}
type StyledComponent<T> = StyledMeta &
([T] extends [React.FunctionComponent<any>]
? T
: React.FunctionComponent<T & { as?: React.ElementType }>);
type StaticPlaceholder = string | number | CSSProperties | StyledMeta;
type HtmlStyledTag<TName extends keyof JSX.IntrinsicElements> = <
TAdditionalProps = Record<string, unknown>
>(
strings: TemplateStringsArray,
...exprs: Array<
| StaticPlaceholder
| ((
// Without Omit here TS tries to infer TAdditionalProps
// from a component passed for interpolation
props: JSX.IntrinsicElements[TName] & Omit<TAdditionalProps, never>
) => string | number)
>
) => StyledComponent<JSX.IntrinsicElements[TName] & TAdditionalProps>;
type ComponentStyledTagWithoutInterpolation<TOrigCmp> = (
strings: TemplateStringsArray,
...exprs: Array<
| StaticPlaceholder
| ((props: 'The target component should have a style prop') => never)
>
) => StyledMeta & TOrigCmp;
// eslint-disable-next-line @typescript-eslint/ban-types
type ComponentStyledTagWithInterpolation<TTrgProps, TOrigCmp> = <OwnProps = {}>(
strings: TemplateStringsArray,
...exprs: Array<
| StaticPlaceholder
| ((props: NoInfer<OwnProps & TTrgProps>) => string | number)
>
) => keyof OwnProps extends never
? StyledMeta & TOrigCmp
: StyledComponent<OwnProps & TTrgProps>;
export type StyledJSXIntrinsics = {
readonly [P in keyof JSX.IntrinsicElements]: HtmlStyledTag<P>;
};
export type Styled = typeof styled & StyledJSXIntrinsics;
export default (process.env.NODE_ENV !== 'production'
? new Proxy(styled, {
get(o, prop: keyof JSX.IntrinsicElements) {
return o(prop);
},
})
: styled) as Styled;