Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[styles] Allow CSS properties to be functions #15546

Merged
merged 18 commits into from
Jun 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;