Last active
October 4, 2024 22:29
-
-
Save nikitatrifan/8aaa3054c0355ffe0c173d22991d5e4a to your computer and use it in GitHub Desktop.
createStyled for Restyle to add support for variants and compound variants
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { css, CSSObject } from "restyle"; | |
import { ComponentPropsWithRef, useMemo, ElementType } from "react"; | |
export const createStyled = < | |
As extends ElementType, | |
Styles extends CSSObject, | |
Variants extends Record<string, Record<string, CSSObject>>, | |
Compounds extends Array<VariantProps<Variants> & { css: CSSObject }>, | |
>(args: { | |
as: As; | |
styles: Styles; | |
variants: Variants; | |
defaults?: VariantProps<Variants>; | |
compounds?: Compounds; | |
}) => { | |
const stylesByVariant = new Map< | |
string, | |
Map<string, ReturnType<typeof css>> | |
>(); | |
for (const variantGroup in args.variants) { | |
const variants = args.variants[variantGroup]; | |
for (const variantName in variants) { | |
const variantStyles = variants[variantName]; | |
if (!stylesByVariant.has(variantGroup)) { | |
stylesByVariant.set(variantGroup, new Map()); | |
} | |
const variantGroupStyles = stylesByVariant.get(variantGroup)!; | |
variantGroupStyles.set(variantName, css(variantStyles)); | |
} | |
} | |
const compoundsWithStyles = args.compounds?.map( | |
({ css: cssObject, ...selector }) => { | |
return { ...selector, styles: css(cssObject) }; | |
}, | |
); | |
const [defaultStylesClassName, DefaultStyles] = css(args.styles); | |
const Component = args.as; | |
type Props = ComponentPropsWithRef<As> & VariantProps<Variants>; | |
return ({ children, ...props }: Props) => { | |
if (args.defaults) { | |
// patch props with defaults, if provided | |
for (const key in args.defaults) { | |
const propName = key as keyof VariantProps<Variants>; | |
const shouldPatch = !(propName in props); | |
if (shouldPatch) { | |
// @ts-expect-error as props are read only | |
props[propName] = args.defaults[propName]; | |
} | |
} | |
} | |
const matchingVariants = new Map<string, ReturnType<typeof css>>(); | |
for (const variantKey of stylesByVariant.keys()) { | |
if (variantKey in props) { | |
const variantGroup = stylesByVariant.get(variantKey)!; | |
const variantProps = variantGroup.get(props[variantKey])!; | |
matchingVariants.set(variantKey, variantProps); | |
delete props[variantKey]; | |
} | |
} | |
let variantClassNames = ""; | |
let variantStyles = []; | |
for (const [className, Styled] of matchingVariants.values()) { | |
variantClassNames += `${className} `; | |
variantStyles.push(<Styled key={className} />); | |
} | |
const compoundVariants = useMemo(() => { | |
let compoundVariantsClassNames = ""; | |
let compoundVariantStyles = []; | |
if (compoundsWithStyles) { | |
for (const compoundVariant of compoundsWithStyles) { | |
let matchesCompoundVariant = null; | |
for (const cvk in compoundVariant) { | |
const compoundVariantKey = cvk as keyof typeof compoundVariant; | |
if ( | |
matchesCompoundVariant === false || | |
["styles", "css"].includes(compoundVariantKey) | |
) { | |
continue; | |
} | |
if (matchingVariants.has(compoundVariantKey)) { | |
// a key found in compound variant also exists in matching variants | |
// let's compare if this is a key-value match as well | |
matchesCompoundVariant = | |
compoundVariant[compoundVariantKey] === | |
matchingVariants.get(compoundVariantKey); | |
} else { | |
matchesCompoundVariant = true; | |
} | |
} | |
if (matchesCompoundVariant) { | |
// match identified, apply its class name and render style rules | |
const [className, Styled] = compoundVariant.styles; | |
compoundVariantsClassNames += `${className} `; | |
compoundVariantStyles.push(<Styled key={className} />); | |
} | |
} | |
} | |
return { | |
styles: compoundVariantStyles, | |
className: compoundVariantsClassNames, | |
}; | |
}, [variantClassNames]); | |
return ( | |
<> | |
<Component | |
className={cs([ | |
props.className, | |
defaultStylesClassName, | |
variantClassNames, | |
compoundVariants.className, | |
])} | |
{...props} | |
> | |
{children} | |
</Component> | |
<DefaultStyles /> | |
{variantStyles} | |
{compoundVariants.styles} | |
</> | |
); | |
}; | |
}; | |
const cs = (classes: Array<string | undefined>) => { | |
let className = ""; | |
for (const cl of classes) { | |
if (typeof cl === "string") { | |
className += `${cl} `; | |
} | |
} | |
return className; | |
}; | |
type VariantGroups<V> = Extract<keyof V, string>; | |
type VariantNames<V, Group extends VariantGroups<V>> = Extract< | |
keyof V[Group], | |
string | boolean | number | |
>; | |
type VariantProps<Variants> = { | |
[Group in VariantGroups<Variants>]?: VariantNames<Variants, Group>; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Form() { | |
return ( | |
<Button variant="primary" disabled={!valid}> | |
Submit | |
</Button> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment