Skip to content

Commit

Permalink
[styles] Allow CSS properties to be functions (#15546)
Browse files Browse the repository at this point in the history
* [styles] Update type definition to let CSS properties be functions

* Restore AdaptingHook

* [docs] Updated AdaptingHook

* [docs] Added AdaptingStyledComponents typescript demo

* Consistent Props base type

* [styles] Add test case

* docs:typescript:formatted

* Fix types for core

* Reuse definition from styles

* Remove unused definition

* Reuse styleRules

* Rename base

* Reuse StyleRulesCallback

* Reuse Styles and ClassKeyOfStyles

* [core] Cleanup withStyles

* Name generic arguments

* Make StyleRules from core backwards compatible


Co-authored-by: Sebastian Silbermann <silbermann.sebastian@gmail.com>
  • Loading branch information
merceyz and eps1lon committed Jun 7, 2019
1 parent 2bfe663 commit 557b690
Show file tree
Hide file tree
Showing 13 changed files with 374 additions and 83 deletions.
44 changes: 44 additions & 0 deletions docs/src/pages/css-in-js/basics/AdaptingHook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/styles';
import Button from '@material-ui/core/Button';

const useStyles = makeStyles({
root: {
background: props =>
props.color === 'red'
? 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)'
: 'linear-gradient(45deg, #2196F3 30%, #21CBF3 90%)',
border: 0,
borderRadius: 3,
boxShadow: props =>
props.color === 'red'
? '0 3px 5px 2px rgba(255, 105, 135, .3)'
: '0 3px 5px 2px rgba(33, 203, 243, .3)',
color: 'white',
height: 48,
padding: '0 30px',
margin: 8,
},
});

function MyButton(props) {
const { color, ...other } = props;
const classes = useStyles(props);
return <Button className={classes.root} {...other} />;
}

MyButton.propTypes = {
color: PropTypes.oneOf(['red', 'blue']).isRequired,
};

function AdaptingHook() {
return (
<React.Fragment>
<MyButton color="red">Red</MyButton>
<MyButton color="blue">Blue</MyButton>
</React.Fragment>
);
}

export default AdaptingHook;
52 changes: 52 additions & 0 deletions docs/src/pages/css-in-js/basics/AdaptingHook.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/styles';
import Button, { ButtonProps as MuiButtonProps } from '@material-ui/core/Button';

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

interface Props {
color: 'red' | 'blue';
}

type MyButtonProps = Props & Omit<MuiButtonProps, keyof Props>;

const useStyles = makeStyles({
root: {
background: (props: Props) =>
props.color === 'red'
? 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)'
: 'linear-gradient(45deg, #2196F3 30%, #21CBF3 90%)',
border: 0,
borderRadius: 3,
boxShadow: (props: Props) =>
props.color === 'red'
? '0 3px 5px 2px rgba(255, 105, 135, .3)'
: '0 3px 5px 2px rgba(33, 203, 243, .3)',
color: 'white',
height: 48,
padding: '0 30px',
margin: 8,
},
});

function MyButton(props: MyButtonProps) {
const { color, ...other } = props;
const classes = useStyles(props);
return <Button className={classes.root} {...other} />;
}

(MyButton as any).propTypes = {
color: PropTypes.oneOf(['red', 'blue']).isRequired,
};

function AdaptingHook() {
return (
<React.Fragment>
<MyButton color="red">Red</MyButton>
<MyButton color="blue">Blue</MyButton>
</React.Fragment>
);
}

export default AdaptingHook;
31 changes: 31 additions & 0 deletions docs/src/pages/css-in-js/basics/AdaptingStyledComponents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { styled } from '@material-ui/styles';
import Button from '@material-ui/core/Button';

const MyButton = styled(({ color, ...other }) => <Button {...other} />)({
background: props =>
props.color === 'red'
? 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)'
: 'linear-gradient(45deg, #2196F3 30%, #21CBF3 90%)',
border: 0,
borderRadius: 3,
boxShadow: props =>
props.color === 'red'
? '0 3px 5px 2px rgba(255, 105, 135, .3)'
: '0 3px 5px 2px rgba(33, 203, 243, .3)',
color: 'white',
height: 48,
padding: '0 30px',
margin: 8,
});

function AdaptingStyledComponents() {
return (
<React.Fragment>
<MyButton color="red">Red</MyButton>
<MyButton color="blue">Blue</MyButton>
</React.Fragment>
);
}

export default AdaptingStyledComponents;
39 changes: 39 additions & 0 deletions docs/src/pages/css-in-js/basics/AdaptingStyledComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { styled } from '@material-ui/styles';
import Button, { ButtonProps as MuiButtonProps } from '@material-ui/core/Button';

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

interface Props {
color: 'red' | 'blue';
}

type MyButtonProps = Props & Omit<MuiButtonProps, keyof Props>;

const MyButton = styled(({ color, ...other }: MyButtonProps) => <Button {...other} />)({
background: (props: Props) =>
props.color === 'red'
? 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)'
: 'linear-gradient(45deg, #2196F3 30%, #21CBF3 90%)',
border: 0,
borderRadius: 3,
boxShadow: (props: Props) =>
props.color === 'red'
? '0 3px 5px 2px rgba(255, 105, 135, .3)'
: '0 3px 5px 2px rgba(33, 203, 243, .3)',
color: 'white',
height: 48,
padding: '0 30px',
margin: 8,
});

function AdaptingStyledComponents() {
return (
<React.Fragment>
<MyButton color="red">Red</MyButton>
<MyButton color="blue">Blue</MyButton>
</React.Fragment>
);
}

export default AdaptingStyledComponents;
2 changes: 1 addition & 1 deletion docs/src/pages/styles/basics/AdaptingHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function MyButton(props) {
}

MyButton.propTypes = {
color: PropTypes.string.isRequired,
color: PropTypes.oneOf(['red', 'blue']).isRequired,
};

function AdaptingHook() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { StyleRules } from '@material-ui/styles/withStyles';
* @param styles a set of style mappings
* @returns the same styles that were passed in
*/
// For TypeScript v3.5 P has to extend {} instead of object
// For TypeScript v3.5 Props has to extend {} instead of object
// See https://github.com/mui-org/material-ui/issues/15942
// and https://github.com/microsoft/TypeScript/issues/31735
export default function createStyles<C extends string, P extends {}>(
styles: StyleRules<P, C>,
): StyleRules<P, C>;
export default function createStyles<ClassKey extends string, Props extends {}>(
styles: StyleRules<Props, ClassKey>,
): StyleRules<Props, ClassKey>;
17 changes: 12 additions & 5 deletions packages/material-ui-styles/src/styled/styled.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Omit } from '@material-ui/types';
import {
CSSProperties,
CreateCSSProperties,
StyledComponentProps,
WithStylesOptions,
} from '@material-ui/styles/withStyles';
Expand All @@ -9,16 +9,23 @@ import * as React from 'react';
/**
* @internal
*/
export type ComponentCreator<C extends React.ElementType> = <Theme, Props extends {} = any>(
styles: CSSProperties | (({ theme, ...props }: { theme: Theme } & Props) => CSSProperties),
export type ComponentCreator<Component extends React.ElementType> = <Theme, Props extends {} = any>(
styles:
| CreateCSSProperties<Props>
| (({ theme, ...props }: { theme: Theme } & Props) => CreateCSSProperties<Props>),
options?: WithStylesOptions<Theme>,
) => React.ComponentType<
Omit<JSX.LibraryManagedAttributes<C, React.ComponentProps<C>>, 'classes' | 'className'> &
Omit<
JSX.LibraryManagedAttributes<Component, React.ComponentProps<Component>>,
'classes' | 'className'
> &
StyledComponentProps<'root'> & { className?: string }
>;

export interface StyledProps {
className: string;
}

export default function styled<C extends React.ElementType>(Component: C): ComponentCreator<C>;
export default function styled<Component extends React.ElementType>(
Component: Component,
): ComponentCreator<Component>;
57 changes: 37 additions & 20 deletions packages/material-ui-styles/src/withStyles/withStyles.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ export interface CSSProperties extends CSS.Properties<number | string> {
[k: string]: CSS.Properties<number | string>[keyof CSS.Properties] | CSSProperties;
}

export type BaseCreateCSSProperties<Props extends object = {}> = {
[P in keyof CSS.Properties<number | string>]:
| CSS.Properties<number | string>[P]
| ((props: Props) => CSS.Properties<number | string>[P])
};

export interface CreateCSSProperties<Props extends object = {}>
extends BaseCreateCSSProperties<Props> {
// Allow pseudo selectors and media queries
[k: string]:
| BaseCreateCSSProperties<Props>[keyof BaseCreateCSSProperties<Props>]
| CreateCSSProperties<Props>;
}

/**
* This is basically the API of JSS. It defines a Map<string, CSS>,
* where
Expand All @@ -16,9 +30,9 @@ export interface CSSProperties extends CSS.Properties<number | string> {
*
* if only `CSSProperties` are matched `Props` are inferred to `any`
*/
export type StyleRules<Props extends object, ClassKey extends string = string> = Record<
export type StyleRules<Props extends object = {}, ClassKey extends string = string> = Record<
ClassKey,
CSSProperties | ((props: Props) => CSSProperties)
CreateCSSProperties<Props> | ((props: Props) => CreateCSSProperties<Props>)
>;

/**
Expand All @@ -28,7 +42,7 @@ export type StyleRulesCallback<Theme, Props extends object, ClassKey extends str
theme: Theme,
) => StyleRules<Props, ClassKey>;

export type Styles<Theme, Props extends {}, ClassKey extends string = string> =
export type Styles<Theme, Props extends object, ClassKey extends string = string> =
| StyleRules<Props, ClassKey>
| StyleRulesCallback<Theme, Props, ClassKey>;

Expand All @@ -44,41 +58,44 @@ export type ClassNameMap<ClassKey extends string = string> = Record<ClassKey, st
/**
* @internal
*/
export type ClassKeyInferable<Theme, Props extends {}> = string | Styles<Theme, Props>;
export type ClassKeyOfStyles<S> = S extends string
? S
: S extends StyleRulesCallback<any, any, infer K>
? K
: S extends StyleRules<any, infer K>
? K
export type ClassKeyInferable<Theme, Props extends object> = string | Styles<Theme, Props>;
export type ClassKeyOfStyles<StylesOrClassKey> = StylesOrClassKey extends string
? StylesOrClassKey
: StylesOrClassKey extends StyleRulesCallback<any, any, infer ClassKey>
? ClassKey
: StylesOrClassKey extends StyleRules<any, infer ClassKey>
? ClassKey
: never;

/**
* infers the type of the theme used in the styles
*/
export type PropsOfStyles<S> = S extends Styles<any, infer Props> ? Props : {};
export type PropsOfStyles<StylesType> = StylesType extends Styles<any, infer Props> ? Props : {};
/**
* infers the type of the props used in the styles
*/
export type ThemeOfStyles<S> = S extends Styles<infer Theme, any> ? Theme : {};
export type ThemeOfStyles<StylesType> = StylesType extends Styles<infer Theme, any> ? Theme : {};

export type WithStyles<
S extends ClassKeyInferable<any, any>,
StylesType extends ClassKeyInferable<any, any>,
IncludeTheme extends boolean | undefined = false
> = (IncludeTheme extends true ? { theme: ThemeOfStyles<S> } : {}) & {
classes: ClassNameMap<ClassKeyOfStyles<S>>;
> = (IncludeTheme extends true ? { theme: ThemeOfStyles<StylesType> } : {}) & {
classes: ClassNameMap<ClassKeyOfStyles<StylesType>>;
innerRef?: React.Ref<any> | React.RefObject<any>;
} & PropsOfStyles<S>;
} & PropsOfStyles<StylesType>;

export interface StyledComponentProps<ClassKey extends string = string> {
classes?: Partial<ClassNameMap<ClassKey>>;
innerRef?: React.Ref<any> | React.RefObject<any>;
}

export default function withStyles<
S extends Styles<any, any>,
Options extends WithStylesOptions<ThemeOfStyles<S>> = {}
StylesType extends Styles<any, any>,
Options extends WithStylesOptions<ThemeOfStyles<StylesType>> = {}
>(
style: S,
style: StylesType,
options?: Options,
): PropInjector<WithStyles<S, Options['withTheme']>, StyledComponentProps<ClassKeyOfStyles<S>>>;
): PropInjector<
WithStyles<StylesType, Options['withTheme']>,
StyledComponentProps<ClassKeyOfStyles<StylesType>>
>;
32 changes: 31 additions & 1 deletion packages/material-ui-styles/test/styles.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as React from 'react';
import { createStyles, withStyles, withTheme, WithTheme, WithStyles } from '@material-ui/styles';
import {
createStyles,
withStyles,
withTheme,
WithTheme,
WithStyles,
makeStyles,
} from '@material-ui/styles';
import Button from '@material-ui/core/Button';
import { Theme } from '@material-ui/core/styles';

Expand Down Expand Up @@ -419,3 +426,26 @@ function forwardRefTest() {
// especially since `innerRef` will be removed in v5 and is equivalent to `ref`
<StyledRefableAnchor innerRef={buttonRef} />;
}

{
// https://github.com/mui-org/material-ui/pull/15546
// Update type definition to let CSS properties be functions
interface testProps {
foo: boolean;
}
const useStyles = makeStyles((theme: Theme) => ({
root: {
width: (prop: testProps) => (prop.foo ? 100 : 0),
},
root2: (prop2: testProps) => ({
width: (prop: testProps) => (prop.foo && prop2.foo ? 100 : 0),
height: 100,
}),
}));

const styles = useStyles({ foo: true });
// $ExpectType string
const root = styles.root;
// $ExpectType string
const root2 = styles.root2;
}
12 changes: 2 additions & 10 deletions packages/material-ui/src/styles/createStyles.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
import { StyleRules } from './withStyles';
import createStyles from '@material-ui/styles/createStyles';

/**
* This function doesn't really "do anything" at runtime, it's just the identity
* function. Its only purpose is to defeat TypeScript's type widening when providing
* style rules to `withStyles` which are a function of the `Theme`.
*
* @param styles a set of style mappings
* @returns the same styles that were passed in
*/
export default function createStyles<C extends string>(styles: StyleRules<C>): StyleRules<C>;
export default createStyles;
Loading

0 comments on commit 557b690

Please sign in to comment.