1import * as React from 'react'; 2import { StyleSheet, ViewStyle, ImageStyle, TextStyle } from 'react-native'; 3 4import { useTheme } from './useExpoTheme'; 5 6type StyleType = ViewStyle | TextStyle | ImageStyle; 7 8type Options = { 9 base?: StyleType; 10 variants?: VariantMap<StyleType>; 11}; 12 13type VariantMap<T> = { [key: string]: { [key: string]: T } }; 14 15type Nested<Type> = { 16 [Property in keyof Type]?: keyof Type[Property]; 17}; 18 19type SelectorMap<Variants> = Partial<{ 20 [K in keyof Variants]?: { 21 [T in keyof Variants[K]]?: StyleType; 22 }; 23}>; 24 25type Selectors<Variants> = { 26 light?: SelectorMap<Variants>; 27 dark?: SelectorMap<Variants>; 28}; 29 30type SelectorProps = { 31 light?: StyleType; 32 dark?: StyleType; 33}; 34 35export function create<T extends object, O extends Options>( 36 component: React.ComponentType<T>, 37 config: O & { selectors?: Selectors<O['variants']>; props?: T } 38) { 39 config.selectors = config.selectors ?? {}; 40 config.variants = config.variants ?? {}; 41 42 const Component = React.forwardRef< 43 T, 44 React.PropsWithChildren<T> & Nested<(typeof config)['variants']> & { selectors?: SelectorProps } 45 >((props, ref) => { 46 const theme = useTheme(); 47 48 const variantStyles = stylesForVariants(props, config.variants); 49 const selectorStyles = stylesForSelectors(props, config.selectors, { theme }); 50 const selectorPropsStyles = stylesForSelectorProps(props.selectors, { theme }); 51 52 const variantFreeProps: any = { ...props }; 53 54 // @ts-ignore 55 // there could be a conflict between the primitive prop and the variant name 56 // for example - variant name "width" and prop "width" 57 // in these cases, favor the variant because it is under the users control (e.g they can update the conflicting name) 58 59 Object.keys(config.variants).forEach((variant) => { 60 delete variantFreeProps[variant]; 61 }); 62 63 return React.createElement(component, { 64 ...config.props, 65 ...variantFreeProps, 66 style: StyleSheet.flatten([ 67 config.base, 68 variantStyles, 69 selectorStyles, 70 selectorPropsStyles, 71 // @ts-ignore 72 props.style || {}, 73 ]), 74 ref, 75 }); 76 }); 77 78 return Component; 79} 80 81function stylesForVariants(props: any, variants: any = {}) { 82 let styles = {}; 83 84 for (const key in props) { 85 if (variants[key]) { 86 const value = props[key]; 87 88 const styleValue = variants[key][value]; 89 if (styleValue) { 90 styles = StyleSheet.flatten(StyleSheet.compose(styles, styleValue)); 91 } 92 } 93 } 94 95 return styles; 96} 97 98function stylesForSelectors(props: any, selectors: any = {}, state: any = {}) { 99 const styles: any[] = []; 100 101 if (state.theme != null) { 102 if (selectors[state.theme] != null) { 103 const variants = selectors[state.theme]; 104 const variantStyles = stylesForVariants(props, variants); 105 106 if (variants.base != null) { 107 styles.push(variants.base); 108 } 109 110 styles.push(variantStyles); 111 } 112 } 113 114 return StyleSheet.flatten(styles); 115} 116 117function stylesForSelectorProps(selectors: any = {}, state: any = {}) { 118 const styles: any[] = []; 119 120 if (state.theme != null) { 121 if (selectors[state.theme] != null) { 122 const selectorStyles = selectors[state.theme]; 123 styles.push(selectorStyles); 124 } 125 } 126 127 return StyleSheet.flatten(styles); 128} 129