-
Notifications
You must be signed in to change notification settings - Fork 45
Expand file tree
/
Copy pathtailwind.tsx
More file actions
267 lines (230 loc) · 10.6 KB
/
tailwind.tsx
File metadata and controls
267 lines (230 loc) · 10.6 KB
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
import React, { CSSProperties } from "react"
import domElements from "./domElements"
import { twMerge } from "tailwind-merge"
const isTwElement = Symbol("isTwElement?")
export type IsTwElement = { [isTwElement]: true }
export type FalseyValue = undefined | null | false
export type FlattenInterpolation<P> = ReadonlyArray<Interpolation<P>>
export type InterpolationValue = string | number | FalseyValue | TailwindComponentInterpolation
export type Interpolation<P> = InterpolationValue | InterpolationFunction<P> | FlattenInterpolation<P>
export type InterpolationFunction<P> = (props: P) => Interpolation<P>
type TailwindComponentInterpolation = PickU<TailwindComponentBase<any, any>, keyof TailwindComponentBase<any, any>>
type IntrinsicElementsKeys = keyof JSX.IntrinsicElements
type IsAny<T, True, False = never> = True | False extends (T extends never ? True : False) ? True : False
export const mergeArrays = (template: TemplateStringsArray, templateElements: (string | undefined | null)[]) => {
return template.reduce(
(acc, c, i) => acc.concat(c || [], templateElements[i] || []), // x || [] to remove false values e.g '', null, undefined. as Array.concat() ignores empty arrays i.e []
[] as string[]
)
}
export const cleanTemplate = (template: Array<Interpolation<any>>, inheritedClasses: string = "") => {
const newClasses: string[] = template
.join(" ")
.trim()
.replace(/\n/g, " ") // replace newline with space
.replace(/\s{2,}/g, " ") // replace line return by space
.split(" ")
.filter((c) => c !== ",") // remove comma introduced by template to string
const inheritedClassesArray: string[] = inheritedClasses ? inheritedClasses.split(" ") : []
return twMerge(
...newClasses
.concat(inheritedClassesArray) // add new classes to inherited classes
.filter((c: string) => c !== " ") // remove empty classes
)
}
export type PickU<T, K extends keyof T> = T extends any ? { [P in K]: T[P] } : never
// export type OmitU<T, K extends keyof T> = T extends any ? PickU<T, Exclude<keyof T, K>> : never
export type RemoveIndex<T> = {
[K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]
}
/**
* ForwardRef typings
*/
export type TailwindExoticComponent<P> = PickU<
React.ForwardRefExoticComponent<P>,
keyof React.ForwardRefExoticComponent<any>
>
type MergeProps<O extends object, P extends {} = {}> =
// Distribute unions early to avoid quadratic expansion
P extends any ? IsAny<P, RemoveIndex<P> & O, P & O> : never
// RemoveIndex<P> is used to make React.ComponentPropsWithRef typesafe on Tailwind components, delete if causing issues
type TailwindPropHelper<
P,
O extends object = {}
// PickU is needed here to make $as typing work
> = PickU<MergeProps<O, P>, keyof MergeProps<O, P>>
type TailwindComponentPropsWith$As<
P extends object,
O extends object,
$As extends string | React.ComponentType<any> = React.ComponentType<P>,
P2 = $As extends AnyTailwindComponent
? TailwindComponentAllInnerProps<$As>
: $As extends IntrinsicElementsKeys | React.ComponentType<any>
? React.ComponentPropsWithRef<$As>
: never
> = P & O & TailwindPropHelper<P2> & { $as?: $As }
/**
* An interface represent a component styled by tailwind-styled-components
*
* @export
* @interface TailwindComponent
* @template P The base react props
* @template O The props added with the template function.
*/
export type TailwindComponent<P extends object, O extends object = {}> = IsTwElement &
TailwindComponentBase<P, O> &
WithStyle<P, O>
/**
* An interface represent a component styled by tailwind-styled-components
*
* @export
* @interface TailwindComponentBase
* @extends {TailwindExoticComponent<TailwindPropHelper<P, O>>}
* @template P The base react props
* @template O The props added with the template function.
*/
export interface TailwindComponentBase<P extends object, O extends object = {}>
extends TailwindExoticComponent<TailwindPropHelper<P, O>> {
// add our own fake call signature to implement the polymorphic '$as' prop
(props: TailwindPropHelper<P, O> & { $as?: never | undefined }): React.ReactElement<TailwindPropHelper<P, O>>
<$As extends string | React.ComponentType<any> = React.ComponentType<P>>(
props: TailwindComponentPropsWith$As<P, O, $As>
): React.ReactElement<TailwindComponentPropsWith$As<P, O, $As>>
}
/**
* An interface represent withStyle functionality
*
* @export
* @interface WithStyle
* @template P
* @template O
*/
export interface WithStyle<P extends object, O extends object = {}> {
withStyle: <S extends object = {}>(
styles: CSSProperties | ((p: P & O & S) => CSSProperties)
) => TailwindComponent<P, O & S>
}
/**
* Generice TailwindComponent
*/
type AnyTailwindComponent = TailwindComponent<any, any>
/**
* A template function that accepts a template literal of tailwind classes and returns a tailwind-styled-component
*
* @export
* @interface TemplateFunction
* @template E
*/
export interface TemplateFunction<P extends object, O extends object = {}> {
(template: TemplateStringsArray): TailwindComponent<P, O>
(
template: TemplateStringsArray | InterpolationFunction<P & O>,
...rest: Array<Interpolation<P & O>>
): TailwindComponent<P, O>
<K extends object>(
template: TemplateStringsArray | InterpolationFunction<P & O & K>,
...rest: Array<Interpolation<P & O & K>>
): TailwindComponent<P, O & K>
}
/**
* A utility function that strips out transient props from a [key,value] array of props
*
* @param {[string, any]} [key]
* @return boolean
*/
const removeTransientProps = ([key]: [string, any]): boolean => key.charAt(0) !== "$"
export type TailwindComponentInnerProps<C extends AnyTailwindComponent> = C extends TailwindComponent<infer P, any>
? P
: never
export type TailwindComponentInnerOtherProps<C extends AnyTailwindComponent> = C extends TailwindComponent<any, infer O>
? O
: never
export type TailwindComponentAllInnerProps<C extends AnyTailwindComponent> = TailwindComponentInnerProps<C> &
TailwindComponentInnerOtherProps<C>
export type IntrinsicElementsTemplateFunctionsMap = {
[RTag in keyof JSX.IntrinsicElements]: TemplateFunction<JSX.IntrinsicElements[RTag]>
}
/**
*
*
* @export
* @interface TailwindInterface
* @extends {IntrinsicElementsTemplateFunctionsMap}
*/
export interface TailwindInterface extends IntrinsicElementsTemplateFunctionsMap {
<C extends TailwindComponent<any, any>>(component: C): TemplateFunction<
TailwindComponentInnerProps<C>,
TailwindComponentInnerOtherProps<C>
>
<C extends React.ComponentType<any>>(component: C): TemplateFunction<
// Prevent functional components without props infering props as `unknown`
C extends (P?: never) => any ? {} : React.ComponentPropsWithoutRef<C>
>
<C extends keyof JSX.IntrinsicElements>(component: C): TemplateFunction<JSX.IntrinsicElements[C]>
}
const isTw = (c: any): c is AnyTailwindComponent => c[isTwElement] === true
// type FDF = React.ElementType<JSX.IntrinsicElements['div']>
const templateFunctionFactory: TailwindInterface = (<C extends React.ElementType>(Element: C): any => {
return (template: TemplateStringsArray, ...templateElements: ((props: any) => string | undefined | null)[]) => {
const TwComponentConstructor = (styleArray: (CSSProperties | ((p: any) => CSSProperties))[] = []) => {
// const renderFunction =
const TwComponent: any = React.forwardRef((baseProps: any, ref: any): JSX.Element => {
const { $as = Element, style = {}, ...props } = baseProps
// set FinalElement based on if Element is a TailwindComponent, $as defaults to Element if undefined
const FinalElement = isTw(Element) ? Element : $as
const withStyles: CSSProperties = styleArray
? styleArray.reduce<CSSProperties>(
(acc, intStyle) =>
Object.assign(acc, typeof intStyle === "function" ? intStyle(baseProps) : intStyle),
{} as CSSProperties
)
: {}
// const style = TwComponent.style(props)
// filter out props that starts with "$" props except when styling a tailwind-styled-component
const filteredProps = isTw(FinalElement)
? props
: (Object.fromEntries(Object.entries(props).filter(removeTransientProps)) as any)
return (
<FinalElement
// forward props
{...filteredProps}
style={{ ...withStyles, ...style }}
// forward ref
ref={ref}
// set class names
className={cleanTemplate(
mergeArrays(
template,
templateElements.map((t) => t({ ...props, $as }))
),
props.className
)}
// forward $as prop when styling a tailwind-styled-component
{...(isTw(Element) ? { $as } : {})}
/>
)
}) as any
// symbol identifier for detecting tailwind-styled-components
TwComponent[isTwElement] = true
// This enables the react tree to show a name in devtools, much better debugging experience Note: Far from perfect, better implementations welcome
if (typeof Element !== "string") {
TwComponent.displayName = (Element as any).displayName || (Element as any).name || "tw.Component"
} else {
TwComponent.displayName = "tw." + Element
}
TwComponent.withStyle = (styles: ((p: any) => CSSProperties) | CSSProperties) =>
TwComponentConstructor(styleArray.concat(styles)) as any
return TwComponent
}
return TwComponentConstructor()
}
}) as any
const intrinsicElementsMap: IntrinsicElementsTemplateFunctionsMap = domElements.reduce(
<K extends IntrinsicElementsKeys>(acc: IntrinsicElementsTemplateFunctionsMap, DomElement: K) => ({
...acc,
[DomElement]: templateFunctionFactory(DomElement)
}),
{} as IntrinsicElementsTemplateFunctionsMap
)
const tw: TailwindInterface = Object.assign(templateFunctionFactory, intrinsicElementsMap)
export default tw