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