import type {
AnchorHTMLAttributes,
ButtonHTMLAttributes,
FormHTMLAttributes,
HTMLAttributes,
InputHTMLAttributes,
LabelHTMLAttributes,
SelectHTMLAttributes,
TextareaHTMLAttributes,
} from 'react'
import { createComponent } from './createComponent'
import { mergeVariants } from './helpers/mergeVariants'
import type { GetRef } from './interfaces/GetRef'
import { getReactNativeConfig } from './setupReactNative'
import type {
GetBaseStyles,
GetNonStyledProps,
GetStaticConfig,
GetStyledVariants,
GetVariantValues,
InferStyleProps,
InferStyledProps,
StackStyle,
StackStyleBase,
StaticConfig,
StaticConfigPublic,
StylableComponent,
StyledContext,
TamaDefer,
TamaguiComponent,
TamaguiComponentPropsBase,
TextStyle,
TextStylePropsBase,
ThemeValueByCategory,
ThemeValueGet,
VariantDefinitions,
VariantSpreadFunction,
} from './types'
import type { Text } from './views/Text'
type AreVariantsUndefined =
// because we pass in the Generic variants which for some reason has this :)
Required extends { _isEmpty: 1 } ? true : false
type GetVariantAcceptedValues = V extends object
? {
[Key in keyof V]?: V[Key] extends VariantSpreadFunction
? Val
: GetVariantValues
}
: undefined
// ---- HTML element support for styledHtml('tagName') ----
// text-like elements use TextStylePropsBase
type TextLikeElements =
| 'a'
| 'abbr'
| 'b'
| 'bdi'
| 'bdo'
| 'cite'
| 'code'
| 'data'
| 'del'
| 'dfn'
| 'em'
| 'i'
| 'ins'
| 'kbd'
| 'label'
| 'mark'
| 'q'
| 's'
| 'samp'
| 'small'
| 'span'
| 'strong'
| 'sub'
| 'sup'
| 'time'
| 'u'
| 'var'
// props that conflict with tamagui style props
type ConflictingHTMLProps =
| 'color'
| 'display'
| 'height'
| 'width'
| 'size'
| 'left'
| 'right'
| 'top'
| 'bottom'
| 'translate'
| 'content'
// map HTML tag to its specific attributes
type HTMLElementSpecificProps = T extends 'a'
? Omit, ConflictingHTMLProps>
: T extends 'button'
? Omit, ConflictingHTMLProps>
: T extends 'input'
? Omit, ConflictingHTMLProps>
: T extends 'select'
? Omit, ConflictingHTMLProps>
: T extends 'textarea'
? Omit, ConflictingHTMLProps>
: T extends 'form'
? Omit, ConflictingHTMLProps>
: T extends 'label'
? Omit, ConflictingHTMLProps>
: Omit, ConflictingHTMLProps>
// base style props based on element type
// use StackStyle/TextStyle to get token support (WithThemeShorthandsPseudosMedia)
type HTMLElementStyleBase =
T extends TextLikeElements ? TextStyle : StackStyle
// runtime check for text-like elements
const textLikeElements = new Set([
'a',
'abbr',
'b',
'bdi',
'bdo',
'cite',
'code',
'data',
'del',
'dfn',
'em',
'i',
'ins',
'kbd',
'label',
'mark',
'q',
's',
'samp',
'small',
'span',
'strong',
'sub',
'sup',
'time',
'u',
'var',
])
/**
* styledHtml() for HTML element tags like 'a', 'button', 'div', etc.
* Automatically provides element-specific props (href for anchors, type for buttons, etc.)
*
* @example
* const StyledAnchor = styledHtml('a', {
* color: '$blue10',
* textDecorationLine: 'underline',
* })
* // StyledAnchor now accepts `href` prop with proper typing
* Link
*/
export function styledHtml<
Tag extends keyof HTMLElementTagNameMap,
Variants extends VariantDefinitions | undefined = undefined,
>(
tag: Tag,
options?: Partial> & {
name?: string
variants?: Variants
defaultVariants?: GetVariantAcceptedValues>
context?: StyledContext
}
) {
type StyleBase = HTMLElementStyleBase
type HTMLProps = HTMLElementSpecificProps
type VariantProps = Variants extends undefined
? {}
: AreVariantsUndefined> extends true
? {}
: GetVariantAcceptedValues>
const isText = textLikeElements.has(tag)
const { variants, name, defaultVariants, context, ...defaultProps } = options || {}
const conf: Partial = {
Component: tag as any,
variants: variants as any,
defaultProps: defaultProps as any,
defaultVariants,
componentName: name,
isReactNative: false,
isText,
acceptsClassName: true,
context,
}
if (defaultProps['children'] || context) {
conf.neverFlatten = true
}
const component = createComponent(conf)
return component as any as TamaguiComponent<
TamaDefer,
HTMLElementTagNameMap[Tag],
TamaguiComponentPropsBase & HTMLProps,
StyleBase,
VariantProps,
{}
>
}
/**
* styled() for creating Tamagui components from other components.
*/
function styled<
ParentComponent extends StylableComponent,
StyledConfig extends StaticConfigPublic,
Variants extends VariantDefinitions,
>(
ComponentIn: ParentComponent,
// this should be Partial> but causes excessively deep type issues
options?: Partial> & {
name?: string
variants?: Variants | undefined
defaultVariants?: GetVariantAcceptedValues
context?: StyledContext
render?: string | React.ReactElement
},
config?: StyledConfig
) {
// do type stuff at top for easier readability
// get parent props without pseudos and medias so we can rebuild both with new variants
type ParentNonStyledProps = GetNonStyledProps
type ParentStylesBase = GetBaseStyles
type ParentVariants = GetStyledVariants
type OurVariantProps =
AreVariantsUndefined extends true ? {} : GetVariantAcceptedValues
type MergedVariants =
AreVariantsUndefined extends true
? ParentVariants
: AreVariantsUndefined extends true
? Omit
: {
// exclude _isEmpty as it no longer is empty
[Key in Exclude]?:
| (Key extends keyof ParentVariants ? ParentVariants[Key] : undefined)
| (Key extends keyof OurVariantProps ? OurVariantProps[Key] : undefined)
}
type Accepted = StyledConfig['accept']
type CustomTokenProps =
Accepted extends Record
? {
[Key in keyof Accepted]?:
| (Key extends keyof ParentStylesBase ? ParentStylesBase[Key] : never)
| (Accepted[Key] extends 'style'
? Partial>
: Accepted[Key] extends 'textStyle'
? Partial>
: ThemeValueByCategory)
}
: {}
/**
* de-opting a bit of type niceness because were hitting depth issues too soon
* before we had:
*
* type OurPropsBase = OurStylesBase & PseudoProps>
* and then below in type Props you would remove the PseudoProps line
* that would give you nicely merged pseudo sub-styles but its just too much for TS
* so now pseudos wont be nicely typed inside media queries, but at least we can nest
*/
type StyledComponent = TamaguiComponent<
TamaDefer,
GetRef,
ParentNonStyledProps,
Accepted extends Record
? ParentStylesBase & CustomTokenProps
: ParentStylesBase,
MergedVariants,
GetStaticConfig
>
// validate not using a variant over an existing valid style
if (process.env.NODE_ENV !== 'production') {
if (!ComponentIn) {
throw new Error(`No component given to styled()`)
}
}
const parentStaticConfig = ComponentIn['staticConfig'] as StaticConfig | undefined
const isPlainStyledComponent =
!!parentStaticConfig &&
!(parentStaticConfig.isReactNative || parentStaticConfig.isHOC)
const isNonStyledHOC = parentStaticConfig?.isHOC && !parentStaticConfig?.isStyledHOC
let Component: any =
isNonStyledHOC || isPlainStyledComponent
? ComponentIn
: parentStaticConfig?.Component || ComponentIn
const reactNativeConfig = !parentStaticConfig
? getReactNativeConfig(Component)
: undefined
const isReactNative = Boolean(
reactNativeConfig || config?.isReactNative || parentStaticConfig?.isReactNative
)
const staticConfigProps = (() => {
let { variants, name, defaultVariants, context, ...defaultProps } = options || {}
let parentDefaultVariants
let parentDefaultProps
if (parentStaticConfig) {
const avoid = parentStaticConfig.isHOC && !parentStaticConfig.isStyledHOC
if (!avoid) {
const pdp = parentStaticConfig.defaultProps
// apply parent props only if not already defined, they are lesser specificity
for (const key in pdp) {
const val = pdp[key]
if (parentStaticConfig.defaultVariants) {
if (key in parentStaticConfig.defaultVariants) {
// ensure we don't add it if its also in our default variants so we keep the order!
if (!defaultVariants || !(key in defaultVariants)) {
parentDefaultVariants ||= {}
parentDefaultVariants[key] = val
}
}
}
if (!(key in defaultProps) && (!defaultVariants || !(key in defaultVariants))) {
parentDefaultProps ||= {}
parentDefaultProps[key] = pdp[key]
}
}
if (parentStaticConfig.variants) {
// @ts-expect-error
variants = mergeVariants(parentStaticConfig.variants, variants)
}
}
}
// applies everything in the right order! order is important
if (parentDefaultProps || defaultVariants || parentDefaultVariants) {
defaultProps = {
...parentDefaultProps,
...parentDefaultVariants,
...defaultProps,
...defaultVariants,
}
}
if (parentStaticConfig?.isHOC) {
// if HOC we map name => componentName as we have a difference in how we name prop vs styled() there
if (name) {
// @ts-ignore
defaultProps.componentName = name
}
}
const isText = Boolean(config?.isText || parentStaticConfig?.isText)
const acceptsClassName =
config?.acceptsClassName ??
(isPlainStyledComponent ||
isReactNative ||
(parentStaticConfig?.isHOC && parentStaticConfig?.acceptsClassName))
const conf: Partial = {
...parentStaticConfig,
...config,
...(!isPlainStyledComponent && {
Component,
}),
// @ts-expect-error
variants,
defaultProps,
defaultVariants,
componentName: name || parentStaticConfig?.componentName,
isReactNative,
isText,
acceptsClassName,
context,
...reactNativeConfig,
isStyledHOC: Boolean(parentStaticConfig?.isHOC),
parentStaticConfig,
}
// bail on non className views as well
if (defaultProps['children'] || !acceptsClassName || context) {
conf.neverFlatten = true
}
return conf
})()
const component = createComponent(staticConfigProps || {})
for (const key in ComponentIn) {
// dont inherit propTypes
if (key === 'propTypes') continue
if (key in component) continue
// @ts-expect-error assigning static properties over
component[key] = ComponentIn[key]
}
return component as any as StyledComponent
}
// sanity check types:
// type YP = GetProps
// type x = YP['onChangeText']
// type x2 = YP['size']
// const X =
// import { Stack } from './views/Stack'
// const X = styled(Stack, {
// variants: {
// size: {
// '...size': (val) => {
// return {
// pointerEvents: 'auto'
// }
// }
// },
// disabled: {
// true: {
// alignContent: 'center',
// opacity: 0.5,
// pointerEvents: 'none',
// },
// },
// } as const
// })
// const TestStyleable = X.styleable<{ abc: 123 }>((props) => {
// return null
// })
// // type variants = GetStyledVariants
// const y =
// sanity check more complex types:
// import { Paragraph } from '../../text/src/Paragraph'
// import { Text } from './views/Text'
// import { getFontSized } from '../../get-font-sized/src'
// import { SizableText } from '../../text/src/SizableText'
// const Text1 = styled(Text, {
// name: 'SizableText',
// fontFamily: '$body',
// variants: {
// size: getFontSized,
// } as const,
// defaultVariants: {
// size: '$true',
// },
// })
// const Test2 = styled(Text1, {
// render: 'p',
// userSelect: 'auto',
// color: '$color',
// })
// const Test3 = styled(Test2, {
// render: 'p',
// userSelect: 'auto',
// color: '$color',
// variants: {
// ork: {
// true: {}
// }
// }
// })
// const Test = styled(Paragraph, {
// render: 'p',
// userSelect: 'auto',
// color: '$color',
// variants: {
// someting: {
// true: {},
// },
// } as const,
// })
// type X = typeof Paragraph
// type Props1 = GetProps
// type z = typeof Text1
// type ParentV = GetVariantProps
// type Props = GetProps
// const y = sadad
// const z = sadad
//
// merges variant types properly:
// const OneVariant = styled(Stack, {
// variants: {
// variant: {
// test: { backgroundColor: 'gray' },
// },
// } as const,
// })
// const Second = styled(Stack, {
// variants: {
// variant: {
// simple: { backgroundColor: 'gray' },
// colorful: { backgroundColor: 'violet' },
// },
// } as const,
// })
// const TwoVariant = styled(OneVariant, {
// variants: {
// variant: {
// simple: { backgroundColor: 'gray' },
// colorful: { backgroundColor: 'violet' },
// },
// } as const,
// })
// type X = typeof OneVariant extends TamaguiComponent ? V : any
// type V = typeof Second extends TamaguiComponent ? V : any
// type V2 = VariantDefinitions
// type R = typeof TwoVariant extends TamaguiComponent ? V : any
// type Keys = keyof X | keyof V
// type Z = {
// [Key in Keys]: V[Key] | X[Key]
// }
// const a: Z = {
// variant: 'colorful',
// }
// const b: Z = {
// variant: 'simple',
// }
// const c: Z = {
// variant: 'invalid',
// }
// const y =
// ---- styled.a, styled.div, styled.button, etc. API ----
type StyledHtmlFactory = <
Variants extends VariantDefinitions | undefined = undefined,
>(
options?: Partial> & {
name?: string
variants?: Variants
defaultVariants?: GetVariantAcceptedValues>
context?: StyledContext
}
) => TamaguiComponent<
TamaDefer,
HTMLElementTagNameMap[Tag],
TamaguiComponentPropsBase & HTMLElementSpecificProps,
HTMLElementStyleBase,
Variants extends undefined
? {}
: AreVariantsUndefined> extends true
? {}
: GetVariantAcceptedValues>,
{}
>
type StyledHtmlFactories = {
[K in keyof HTMLElementTagNameMap]: StyledHtmlFactory
}
// use a proxy to make styled.a(), styled.div() etc work
const styledExport = new Proxy(styled as typeof styled & StyledHtmlFactories, {
get(target, prop: string) {
if (prop in target) {
return (target as any)[prop]
}
// return factory for HTML elements
return (options: any) => styledHtml(prop as keyof HTMLElementTagNameMap, options)
},
})
export { styledExport as styled }