diff --git a/.eslintignore b/.eslintignore index 641d5e9be..b1d27cfce 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ flow-typed/ packages/**/dist/ examples/**/static/ *.ts +*.tsx diff --git a/packages/jss/src/index.d.ts b/packages/jss/src/index.d.ts index f867e9642..806a99d76 100644 --- a/packages/jss/src/index.d.ts +++ b/packages/jss/src/index.d.ts @@ -7,35 +7,36 @@ import {Properties as CSSProperties} from 'csstype' // TODO: refactor to only include Observable types if plugin is installed. import {Observable} from 'indefinite-observable' -// TODO: Type data better, currently typed as any for allowing to override it -type Func = ((data: any) => R) +type Func = T extends undefined ? ((data: P) => R) : ((data: P & {theme: T}) => R) type NormalCssProperties = CSSProperties -type NormalCssValues = K extends keyof NormalCssProperties - ? NormalCssProperties[K] - : JssValue +type NormalCssValues = K extends keyof NormalCssProperties ? NormalCssProperties[K] : JssValue -export type JssStyle = +export type JssStyle = | { [K in keyof NormalCssProperties]: | NormalCssValues - | JssStyle - | Func | JssStyle | undefined> + | JssStyle + | Func | JssStyle | undefined> | Observable | JssStyle | undefined> } | { [K: string]: | JssValue - | JssStyle - | Func + | JssStyle + | Func | undefined> | Observable } -export type Styles = Record< +export type Styles< + Name extends string | number | symbol = string, + Props = unknown, + Theme = undefined +> = Record< Name, - | JssStyle + | JssStyle | string - | Func + | Func | string | null | undefined> | Observable > export type Classes = Record @@ -215,7 +216,10 @@ export interface StyleSheet>, options?: Partial): Rule[] + addRules( + styles: Partial>, + options?: Partial + ): Rule[] /** * Get a rule by name. */ @@ -250,7 +254,7 @@ export interface JssOptions { export interface Jss { createStyleSheet( - styles: Partial>, + styles: Partial>, options?: StyleSheetFactoryOptions ): StyleSheet removeStyleSheet(sheet: StyleSheet): this diff --git a/packages/jss/tests/types/Styles.ts b/packages/jss/tests/types/Styles.ts index faaa088ce..0b6ccbc30 100644 --- a/packages/jss/tests/types/Styles.ts +++ b/packages/jss/tests/types/Styles.ts @@ -5,13 +5,18 @@ interface Props { flag?: boolean } +interface Theme { + color: string +} + declare const color$: Observable<'cyan'> declare const style$: Observable<{ backgroundColor: 'fuchsia' transform: 'translate(0px, 205px)' }> -const styles: Styles = { +// General Types Check +const styles: Styles = { basic: { textAlign: 'center', display: 'flex', @@ -22,7 +27,7 @@ const styles: Styles = { textAlign: 'center', display: 'flex', width: '100%', - justifyContent: (props: Props) => (props.flag ? 'center' : undefined) + justifyContent: props => (props.flag ? 'center' : undefined) }, inner: { textAlign: 'center', @@ -33,7 +38,7 @@ const styles: Styles = { fontSize: 12 } }, - func: (props: Props) => ({ + func: props => ({ display: 'flex', flexDirection: 'column', justifyContent: 'center', @@ -43,8 +48,8 @@ const styles: Styles = { position: 'relative', pointerEvents: props.flag ? 'none' : null }), - funcNull: (props: Props) => null, - funcWithTerm: (props: Props) => ({ + funcNull: props => null, + funcWithTerm: props => ({ width: props.flag ? 377 : 272, height: props.flag ? 330 : 400, boxShadow: '0px 2px 20px rgba(0, 0, 0, 0.08)', @@ -67,3 +72,30 @@ const styles: Styles = { to: {opacity: 1} } } + +// Test supplied Props and Theme +// Verify that nested parameter declarations are banned +const stylesPropsAndTheme: Styles = { + rootParamDeclaration: ({flag, theme}) => ({ + fontWeight: 'bold', + // @ts-expect-error + nothingAllowed: ({flag, theme}) => '' + }), + anotherClass: { + color: 'red', + innerParamDeclaration1: ({flag, theme}) => '', + innerParamDeclaration2: ({flag, theme}) => ({ + backgroundColor: 'blue', + // @ts-expect-error + nothingAllowed: ({flag, theme}) => '' + }) + } +} + +// Test the className types +const stylesClassNames: Styles = { + // @ts-expect-error + stringClassName: '', + [1]: '', + [2]: '' +} diff --git a/packages/react-jss/src/index.d.ts b/packages/react-jss/src/index.d.ts index 8728cc5ce..00842fc88 100644 --- a/packages/react-jss/src/index.d.ts +++ b/packages/react-jss/src/index.d.ts @@ -37,17 +37,21 @@ declare const JssContext: Context<{ disableStylesGeneration: boolean }> -type ClassesForStyles Styles)> = Classes< - S extends (theme: any) => Styles ? keyof ReturnType : keyof S -> +type ClassesForStyles< + S extends Styles | ((theme: any) => Styles) +> = Classes Styles ? keyof ReturnType : keyof S> -interface WithStylesProps Styles)> { +interface WithStylesProps< + S extends Styles | ((theme: any) => Styles) +> { classes: ClassesForStyles } /** * @deprecated Please use `WithStylesProps` instead */ -type WithStyles Styles)> = WithStylesProps +type WithStyles< + S extends Styles | ((theme: any) => Styles) +> = WithStylesProps declare global { namespace Jss { @@ -72,18 +76,17 @@ interface CreateUseStylesOptions extends BaseOptions( - styles: Styles | ((theme: Theme) => Styles), +declare function createUseStyles( + styles: Styles | ((theme: Theme) => Styles), options?: CreateUseStylesOptions -): (data?: unknown) => Classes +): (data?: Props & {theme?: Theme}) => Classes type GetProps = C extends ComponentType ? P : never -declare function withStyles< - ClassNames extends string | number | symbol, - S extends Styles | ((theme: any) => Styles) ->( - styles: S, +declare function withStyles( + styles: + | Styles + | ((theme: Theme) => Styles), options?: WithStylesOptions ): ( comp: C @@ -91,7 +94,7 @@ declare function withStyles< JSX.LibraryManagedAttributes< C, Omit, 'classes'> & { - classes?: Partial> + classes?: Partial> innerRef?: RefObject | ((instance: any) => void) } > diff --git a/packages/react-jss/tests/types/createUseStyles.ts b/packages/react-jss/tests/types/createUseStyles.ts new file mode 100644 index 000000000..7795c4f42 --- /dev/null +++ b/packages/react-jss/tests/types/createUseStyles.ts @@ -0,0 +1,238 @@ +import {createUseStyles} from '../../src' + +type DefaultTheme = Jss.Theme + +interface MyProps { + property: string +} + +interface MyTheme { + color: 'red' +} + +const expectedCustomProps = {property: ''} +const expectedDefaultTheme: DefaultTheme = {defaultFontSize: 0, themeColour: ''} +const expectedCustomTheme: MyTheme = {color: 'red'} + +/* -------------------- THEME ARGUMENT -------------------- */ +// Regular, static styles work fine +const themeArg1 = createUseStyles(theme => ({ + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } +})) +const themeArg1ClassesPass = themeArg1() + +// Theme type assumed to be the default +// Nested theme declaration banned +// @ts-expect-error +const themeArg2 = createUseStyles(theme => ({ + themeNotAllowed: ({theme: innerTheme}) => ({ + fontWeight: 'bold' + }) +})) +// @ts-expect-error +const themeArg2ClassesFail = themeArg2({theme: {}}) +// @ts-expect-error +const themeArg2ClassesFail2 = themeArg2({theme: expectedCustomTheme}) +const themeArg2ClassesPass = themeArg2({theme: expectedDefaultTheme}) + +// Props declaration is allowed +const themeArg3 = createUseStyles(theme => ({ + onlyPropsAllowed: ({...props}) => ({ + fontWeight: 'bold' + }) +})) +// @ts-expect-error +const themeArg3ClassesFail = themeArg3({property: 0}) +// @ts-expect-error +const themeArg3ClassesFail2 = themeArg3({...expectedCustomProps, theme: expectedCustomTheme}) +const themeArg3ClassesPass = themeArg3(expectedCustomProps) +const themeArg3ClassesPass2 = themeArg3({...expectedCustomProps, theme: expectedDefaultTheme}) + +// Nested props declaration banned +const themeArg4 = createUseStyles(theme => ({ + onlyPropsAllowed: ({...props}) => ({ + fontWeight: 'bold', + // @ts-expect-error + propsNotAllowed: ({...innerProps}) => '' + }) +})) + +// Supplied theme type is acknowledged +const themeArg5 = createUseStyles(theme => ({})) +// @ts-expect-error +const themeArg5ClassesFail = themeArg5({theme: {}}) +// @ts-expect-error +const themeArg5ClassesFail2 = themeArg5({theme: expectedDefaultTheme}) +const themeArg5ClassesPass = themeArg5({theme: expectedCustomTheme}) + +// Custom theme can be determined from argument +const themeArg6 = createUseStyles((theme: MyTheme) => ({ + someClassName: { + fontWeight: 'bold' + } +})) +// @ts-expect-error +const themeArg6ClassesFail = themeArg6({theme: {}}) +// @ts-expect-error +const themeArg6ClassesFail2 = themeArg6({theme: expectedDefaultTheme}) +const themeArg6ClassesPass = themeArg6({theme: expectedCustomTheme}) + +// Props can be determined implicitly +const themeArg7 = createUseStyles(theme => ({ + checkbox: ({property}: MyProps) => ({ + borderColor: property + }) +})) + +// @ts-expect-error invalid props +const themeArg7ClassesFail = themeArg7({colour: 'green'}) +// @ts-expect-error extraneous props +const themeArg7ClassesFail2 = themeArg7({...expectedCustomProps, someUnTypedProp: 1}) +const themeArg7ClassesPass = themeArg7(expectedCustomProps) + +// Classes check +const themeArgClasses7String: string = themeArg7ClassesPass.checkbox +// @ts-expect-error invalid className +themeArg7ClassesPass.doesntExist + +/* -------------------- NO THEME ARGUMENT -------------------- */ +// Regular, static styles work fine +const noThemeArg1 = createUseStyles({ + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } +}) +const noThemeArg1ClassesPass = noThemeArg1() + +// Theme declaration is allowed, but not nested theme declaration +// Theme type assumed to be the default +const noThemeArg2 = createUseStyles({ + themeOnly: ({theme}) => ({ + fontWeight: 'bold', + // @ts-expect-error + themeNotAllowed: ({theme: innerTheme}) => '', + '& > *': { + color: 'red', + // @ts-expect-error + themeNotAllowed: ({theme: innerMostTheme}) => '' + } + }) +}) +// @ts-expect-error +const noThemeArg2ClassesFail = noThemeArg2({theme: {}}) +// @ts-expect-error +const noThemeArg2ClassesFail2 = noThemeArg2({theme: expectedCustomTheme}) +const noThemeArg2ClassesPass = noThemeArg2({theme: expectedDefaultTheme}) + +// Props declaration is allowed, but not nested props declaration +const noThemeArg3 = createUseStyles({ + propsAndTheme: ({property, theme}) => ({ + fontWeight: 'bold', + // @ts-expect-error + nothingAllowed: ({property: innerProperty}) => '', + '& > *': { + color: 'red', + // @ts-expect-error + nothingAllowed: ({property: innerMostProperty}) => '' + } + }) +}) +// @ts-expect-error +const noThemeArg3ClassesFail = noThemeArg3({property: 0}) +// @ts-expect-error +const noThemeArg3ClassesFail2 = noThemeArg3({...expectedCustomProps, theme: expectedCustomTheme}) +const noThemeArg3ClassesPass = noThemeArg3(expectedCustomProps) +const noThemeArg3ClassesPass2 = noThemeArg3({...expectedCustomProps, theme: expectedDefaultTheme}) + +// Props and Theme types are properly acknowledged when supplied +const noThemeArg4 = createUseStyles({ + propsAndTheme: ({property, theme}) => ({ + fontWeight: 'bold', + // @ts-expect-error + nothingAllowed: ({theme: innerTheme, ...innerProps}) => '', + '& > *': { + color: 'red', + // @ts-expect-error + nothingAllowed: ({theme: innerMostTheme, ...innerMostProps}) => '' + } + }) +}) +// @ts-expect-error +const noThemeArg4ClassesFail = noThemeArg4({property: 0}) +// @ts-expect-error +const noThemeArg4ClassesFail2 = noThemeArg4({...expectedCustomProps, theme: expectedDefaultTheme}) +const noThemeArg4ClassesPass = noThemeArg4(expectedCustomProps) +const noThemeArg4ClassesPass2 = noThemeArg4({...expectedCustomProps, theme: expectedCustomTheme}) + +// Nested declarations are banned (single nest test) +const noThemeArg5 = createUseStyles({ + singleNest: { + fontWeight: 'bold', + singleValue: ({property, theme}) => '', + nestOne: ({property, theme}) => ({ + color: 'red', + // @ts-expect-error + nothingAllowed: ({theme: innerTheme, ...innerProps}) => '' + }) + } +}) + +// Nested declarations are banned (double nest test) +const noThemeArg6 = createUseStyles({ + doubleNest: { + fontWeight: 'bold', + singleValue: ({property, theme}) => '', + firstNest: { + color: 'red', + innerSingleValue: ({property, theme}) => '', + secondNest: ({property, theme}) => ({ + backgroundColor: 'blue', + // @ts-expect-error + nothingAllowed: ({theme: innerTheme, ...innerProps}) => '' + }) + } + } +}) + +// Nested declarations are banned (triple nest test) +const noThemeArg7 = createUseStyles({ + tripleNest: { + fontWeight: 'bold', + singleValue: ({property, theme}) => '', + firstNest: { + color: 'red', + innerSingleValue: ({property, theme}) => '', + secondNest: { + backgroundColor: 'blue', + innerMostSingleValue: ({property, theme}) => '', + thirdNest: ({property, theme}) => ({ + display: 'block', + // @ts-expect-error + nothingAllowed: ({theme: innerMostTheme, ...innerMostProps}) => '' + }) + } + } + } +}) + +// Props can be determined implicitly +const noThemeArg8 = createUseStyles({ + checkbox: ({property}: MyProps) => ({ + borderColor: property + }) +}) + +// @ts-expect-error invalid props +const noThemeArg8ClassesFail = noThemeArg8({colour: 'green'}) +// @ts-expect-error extraneous props +const noThemeArg8ClassesFail2 = noThemeArg8({...expectedCustomProps, someUnTypedProp: 1}) +const noThemeArg8ClassesPass = noThemeArg8(expectedCustomProps) + +// Classes check +const noThemeArg8ClassesString: string = noThemeArg8ClassesPass.checkbox +// @ts-expect-error invalid className +noThemeArg8ClassesPass.doesntExist diff --git a/packages/react-jss/tests/types/withStyles.tsx b/packages/react-jss/tests/types/withStyles.tsx new file mode 100644 index 000000000..b7dfa9aa1 --- /dev/null +++ b/packages/react-jss/tests/types/withStyles.tsx @@ -0,0 +1,170 @@ +import React from 'react' +import withStyles, {Styles} from '../../src' + +// Note: Styles type is thoroughly tested in `jss/tests/types/Styles` and `react-jss/tests/types/createUseStyles`. +// This is simply a test to make sure `withStyles` accepts and rejects the correct arguments. + +// Note: Testing default theme vs. custom theme is unnecessary here since the user will +// always have to specify the theme anyway. + +interface MyProps { + property: string +} + +interface MyTheme { + color: 'red' +} + +function SimpleComponent(props: MyProps) { + return
{props.property}
+} + +// Intended to test the output of withStyles to make sure the props are still valid +let ResultingComponent: React.ComponentType +let ComponentTest: React.FC + +/* -------------------- Function Argument Passing Cases -------------------- */ +// Plain Object (no type supplied) +function functionPlainObject(theme: MyTheme) { + return { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } + } +} +ResultingComponent = withStyles(functionPlainObject)(SimpleComponent) +ComponentTest = () => + +// Plain Styles +function functionPlainStyles(theme: MyTheme): Styles { + return { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } + } +} +ResultingComponent = withStyles(functionPlainStyles)(SimpleComponent) +ComponentTest = () => + +// With Props +function functionProps(theme: MyTheme): Styles { + return { + someClassName: ({property}) => '', + anotherClassName: { + fontWeight: 'bold' + } + } +} +ResultingComponent = withStyles(functionProps)(SimpleComponent) +ComponentTest = () => + +// With Props and ClassName rules +function functionPropsAndName(theme: MyTheme): Styles { + return { + [1]: ({property}) => '', + [2]: { + fontWeight: 'bold' + } + } +} +ResultingComponent = withStyles(functionPropsAndName)(SimpleComponent) +ComponentTest = () => + +/* -------------------- Regular Object Passing Cases -------------------- */ + +// Plain Object (no type supplied) +const plainObject = { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(plainObject)(SimpleComponent) +ComponentTest = () => + +// Plain Styles +const stylesPlain: Styles = { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(stylesPlain)(SimpleComponent) +ComponentTest = () => + +// With Props +const stylesProps: Styles = { + someClassName: ({property}) => '', + anotherClassName: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(stylesProps)(SimpleComponent) +ComponentTest = () => + +// With Theme +const stylesTheme: Styles = { + someClassName: ({theme}) => '', + anotherClassName: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(stylesTheme)(SimpleComponent) +ComponentTest = () => + +// With Props and Theme +const stylesPropsAndTheme: Styles = { + someClassName: ({property, theme}) => '', + anotherClassName: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(stylesPropsAndTheme)(SimpleComponent) +ComponentTest = () => + +// With Props and Theme and ClassName rules +const stylesPropsAndThemeAndName: Styles = { + [1]: ({property, theme}) => '', + [2]: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(stylesPropsAndThemeAndName)(SimpleComponent) +ComponentTest = () => + +/* -------------------- Failing Cases -------------------- */ + +// A function argument cannot provide another defined theme type conflicting with `undefined` +function failingFunctionRedefineTheme(theme: MyTheme): Styles { + return { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } + } +} + +function passingFunctionUnknownTheme(theme: MyTheme): Styles { + return { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } + } +} + +function passingFunctionNullTheme(theme: MyTheme): Styles { + return { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } + } +} + +// @ts-expect-error +withStyles(failingFunctionRedefineTheme)(SimpleComponent) +withStyles(passingFunctionUnknownTheme)(SimpleComponent) +withStyles(passingFunctionNullTheme)(SimpleComponent)