-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcreate-styled-context.tsx
147 lines (133 loc) · 3.97 KB
/
create-styled-context.tsx
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
/* eslint-disable react/prop-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/display-name */
import {
createContext,
createElement,
forwardRef,
useContext,
type ComponentProps,
type ElementType,
useMemo,
ForwardRefExoticComponent,
} from "react";
import { cn } from "./cn";
type GenericProps = Record<string, unknown>;
type StyleRecipe = {
(props?: GenericProps): Record<string, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
variantKeys: (string | number)[];
};
type StyleSlot<R extends StyleRecipe> = keyof ReturnType<R>;
type Classes<R extends StyleRecipe> = { [key in StyleSlot<R>]?: string };
type StyleSlotRecipe<R extends StyleRecipe> = Record<StyleSlot<R>, string>;
type StyleVariantProps<R extends StyleRecipe> = Parameters<R>[0] & {
className?: string;
class?: never;
};
type CombineProps<T, U> = Omit<T, keyof U> & U;
export type DataAttributes = Record<`data-${string}`, string | number>;
export type ComponentVariants<
T extends ElementType,
R extends StyleRecipe,
OtherProps,
> = ForwardRefExoticComponent<
CombineProps<ComponentProps<T>, StyleVariantProps<R>> & OtherProps
>;
export const splitProps = (
keys: (string | number)[],
props: Record<string, any>,
) => {
const keyProps: any = {};
const otherProps: any = {};
for (const [key, value] of Object.entries(props)) {
if (keys.includes(key)) {
keyProps[key] = value;
continue;
}
otherProps[key] = value;
}
return [keyProps, otherProps];
};
export const createStyleContext = <R extends StyleRecipe>(recipe: R) => {
const StyleContext = createContext<{
slotStyles: StyleSlotRecipe<R> | null;
classes: Classes<R>;
unstyled?: boolean;
}>({ slotStyles: null, classes: {} });
const withProvider = <T extends ElementType>(
Component: T,
slot: StyleSlot<R>,
defaultProps?: Partial<ComponentProps<T> & DataAttributes>,
): ForwardRefExoticComponent<
CombineProps<ComponentProps<T>, StyleVariantProps<R>> & {
classes?: Classes<R>;
unstyled?: boolean;
} & DataAttributes
> => {
const StyledComponent = forwardRef<
T,
ComponentProps<T> & {
classes?: Classes<R>;
unstyled?: boolean;
}
>(({ classes = {}, unstyled = false, ...props }, ref) => {
const [variantProps, otherProps] = splitProps(recipe.variantKeys, {
...defaultProps,
...props,
});
const slotStyles = useMemo(
() => recipe(variantProps) as any,
// eslint-disable-next-line react-hooks/exhaustive-deps
Object.values(variantProps),
);
return (
<StyleContext.Provider value={{ slotStyles, classes, unstyled }}>
<Component
ref={ref}
{...otherProps}
className={
unstyled
? props.className
: slotStyles[slot]({
className: cn(classes[slot], props.className),
})
}
/>
</StyleContext.Provider>
);
});
return StyledComponent as any;
};
const withContext = <T extends ElementType>(
Component: T,
slot: StyleSlot<R>,
defaultProps?: Partial<ComponentProps<T> & DataAttributes>,
): ForwardRefExoticComponent<
ComponentProps<T> & DataAttributes & { unstyled?: boolean }
> => {
const StyledComponent = forwardRef<
T,
ComponentProps<T> & { unstyled?: boolean }
>(({ unstyled: unstyledProp, ...props }, ref) => {
const { slotStyles, classes, unstyled } = useContext(StyleContext) as any;
const el = createElement(Component, {
...defaultProps,
...props,
className:
unstyled || unstyledProp
? props.className
: slotStyles?.[slot]({
className: cn(classes[slot], props.className),
}),
ref,
});
return el;
});
return StyledComponent as any;
};
return {
withProvider,
withContext,
};
};