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

[system] Add styled primitives #27207

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/material-ui-styled-engine-sc/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export default function styled(tag, options) {
return stylesFactory;
}

Object.keys(scStyled).forEach((tag) => {
styled[tag] = scStyled[tag];
});

export { ThemeContext, keyframes, css } from 'styled-components';
export { default as StyledEngineProvider } from './StyledEngineProvider';
export { default as GlobalStyles } from './GlobalStyles';
5 changes: 5 additions & 0 deletions packages/material-ui-styled-engine-sc/src/styled.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { expect } from 'chai';
import { createClientRender } from 'test/utils';
import scStyled from 'styled-components';
import styled from '@material-ui/styled-engine-sc';

describe('styled', () => {
Expand Down Expand Up @@ -38,4 +39,8 @@ describe('styled', () => {
expect(container.firstChild).not.to.have.attribute('color');
expect(container.querySelector('[class^=TestComponent]')).not.to.equal(null);
});

it('has primitive', () => {
expect(styled.div).to.equal(scStyled.div);
});
});
4 changes: 4 additions & 0 deletions packages/material-ui-styled-engine/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export default function styled(tag, options) {
return stylesFactory;
}

Object.keys(emStyled).forEach((tag) => {
Copy link
Member

@mnajdova mnajdova Jul 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why I didn’t add this before. 👍

styled[tag] = emStyled[tag];
});

export { ThemeContext, keyframes, css } from '@emotion/react';
export { default as StyledEngineProvider } from './StyledEngineProvider';
export { default as GlobalStyles } from './GlobalStyles';
5 changes: 5 additions & 0 deletions packages/material-ui-styled-engine/src/styled.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from 'chai';
import emStyled from '@emotion/styled';
import styled from './index';

describe('styled', () => {
Expand All @@ -15,4 +16,8 @@ describe('styled', () => {
'Material-UI: the styled("span")(...args) API requires all its args to be defined',
);
});

it('has primitive', () => {
expect(styled.div).to.equal(emStyled.div);
});
});
29 changes: 25 additions & 4 deletions packages/material-ui-system/src/createStyled.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import * as CSS from 'csstype';
import { SxProps } from './styleFunctionSx';
import { Theme as DefaultTheme } from './createTheme';
import { SystemProps } from './Box';

export interface SerializedStyles {
name: string;
Expand Down Expand Up @@ -75,18 +76,18 @@ export type PropsOf<C extends keyof JSX.IntrinsicElements | React.JSXElementCons

export type Overwrapped<T, U> = Pick<T, Extract<keyof T, keyof U>>;

export interface StyledComponent<InnerProps, StyleProps, Theme extends object>
extends React.FunctionComponent<InnerProps & StyleProps & { theme?: Theme }>,
export interface StyledComponent<InnerProps, StyleProps, JSXProps>
extends React.FunctionComponent<InnerProps & StyleProps & JSXProps>,
ComponentSelector {
/**
* @desc this method is type-unsafe
*/
withComponent<NewTag extends keyof JSX.IntrinsicElements>(
tag: NewTag,
): StyledComponent<JSX.IntrinsicElements[NewTag], StyleProps, Theme>;
): StyledComponent<JSX.IntrinsicElements[NewTag], StyleProps, JSXProps>;
withComponent<Tag extends React.JSXElementConstructor<any>>(
tag: Tag,
): StyledComponent<PropsOf<Tag>, StyleProps, Theme>;
): StyledComponent<PropsOf<Tag>, StyleProps, JSXProps>;
}

export interface StyledOptions {
Expand Down Expand Up @@ -237,6 +238,26 @@ export interface CreateMUIStyled<Theme extends object = DefaultTheme> {
>;
}

export type StyledPrimitive<
Tag extends keyof JSX.IntrinsicElements,
Theme extends object = DefaultTheme,
> = React.FunctionComponent<
SystemProps<Theme> & {
theme?: Theme;
as?: React.ElementType;
sx?: SxProps<Theme>;
} & JSX.IntrinsicElements[Tag]
>;

export interface CreatePrimitiveStyled<Theme extends object = DefaultTheme> {
<Tag extends keyof JSX.IntrinsicElements>(tag: Tag): () => StyledPrimitive<Tag, Theme>;
htmlTags: Array<keyof JSX.IntrinsicElements>;
}

export function createPrimitiveStyled<T extends object = DefaultTheme>(
defaultTheme?: T,
): CreatePrimitiveStyled<T>;

export default function createStyled<T extends object = DefaultTheme>(options?: {
defaultTheme?: T;
rootShouldForwardProp?: (prop: string) => boolean;
Expand Down
23 changes: 22 additions & 1 deletion packages/material-ui-system/src/createStyled.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import styledEngineStyled from '@material-ui/styled-engine';
import createTheme from './createTheme';
import styleFunctionSx from './styleFunctionSx';
import styleFunctionSx, { extendSxProp } from './styleFunctionSx';
import propsToClassKey from './propsToClassKey';
import { propToStyleFunction } from './getThemeValue';

function isEmpty(obj) {
return Object.keys(obj).length === 0;
Expand Down Expand Up @@ -62,6 +63,26 @@ const lowercaseFirstLetter = (string) => {
return string.charAt(0).toLowerCase() + string.slice(1);
};

export function createPrimitiveStyled(defaultTheme = systemDefaultTheme) {
function shouldPrimitiveForwardProp(prop) {
return prop !== 'theme' && prop !== 'sx' && prop !== 'as' && !propToStyleFunction[prop];
}
function styled(tag) {
return () =>
styledEngineStyled(tag, {
shouldForwardProp: shouldPrimitiveForwardProp,
})((props) => {
const theme = isEmpty(props.theme) ? defaultTheme : props.theme;
const { sx } = extendSxProp(props);
return styleFunctionSx({ ...props, sx, theme });
});
}

styled.htmlTags = Object.keys(styledEngineStyled);

return styled;
}

export default function createStyled(input = {}) {
const {
defaultTheme = systemDefaultTheme,
Expand Down
3 changes: 3 additions & 0 deletions packages/material-ui/src/primitives/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './primitives';

export { default as mui } from './primitives';
54 changes: 54 additions & 0 deletions packages/material-ui/src/primitives/primitives.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react';
import { expect } from 'chai';
import { mui } from '@material-ui/core/primitives';
import { createTheme, hexToRgb, ThemeProvider } from '@material-ui/core/styles';
import { createClientRender } from 'test/utils';

describe('StyledPrimitive', () => {
const theme = createTheme();
const render = createClientRender();

it('accepts sx prop', () => {
const { container } = render(<mui.a sx={{ color: 'primary.main', pt: 2 }}>Foo</mui.a>);
expect(container.firstChild).toHaveComputedStyle({
color: hexToRgb(theme.palette.primary.main),
paddingTop: theme.spacing(2),
});
});

it('extend prop', () => {
const { container } = render(
<mui.a color="primary.main" pt={2}>
Foo
</mui.a>,
);
expect(container.firstChild).toHaveComputedStyle({
color: hexToRgb(theme.palette.primary.main),
paddingTop: theme.spacing(2),
});
});

it('only html prop can spread to DOM', () => {
const { container } = render(
<mui.a alignItems="center" pt={2} href="/home" data-tag="a">
Foo
</mui.a>,
);
expect(container.firstChild).to.have.attribute('href', '/home');
expect(container.firstChild).to.have.attribute('data-tag', 'a');
expect(container.firstChild).not.to.have.attribute('alignItems', 'center');
});

it('use value from custom theme', () => {
const { container } = render(
<ThemeProvider theme={createTheme({ palette: { primary: { main: '#ff5252' } } })}>
<mui.a color="primary.main" pt={2}>
Foo
</mui.a>
</ThemeProvider>,
);
expect(container.firstChild).toHaveComputedStyle({
color: hexToRgb('#ff5252'),
});
});
});
22 changes: 22 additions & 0 deletions packages/material-ui/src/primitives/primitives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createPrimitiveStyled, StyledPrimitive } from '@material-ui/system';
import createTheme, { Theme } from '../styles/createTheme';

export type Mui = {
[Key in keyof JSX.IntrinsicElements]: StyledPrimitive<Key, Theme>;
};

const generatePrimitives = () => {
const styled = createPrimitiveStyled(createTheme());
const mui = {};

styled.htmlTags.forEach((tag) => {
// @ts-ignore
mui[tag] = styled(tag)();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering what is the bundle size of this when we import it

});

return mui as Mui;
};

const mui = generatePrimitives();

export default mui;
48 changes: 26 additions & 22 deletions test/utils/initMatchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,39 @@ declare global {
* @example expect(element).toHaveInlineStyle({ width: '200px' })
*/
toHaveInlineStyle(
expectedStyle: Record<
Exclude<
keyof CSSStyleDeclaration,
| 'getPropertyPriority'
| 'getPropertyValue'
| 'item'
| 'removeProperty'
| 'setProperty'
| number
>,
string
expectedStyle: Partial<
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should have Partial, otherwise it expects all of the css properties.

Record<
Exclude<
keyof CSSStyleDeclaration,
| 'getPropertyPriority'
| 'getPropertyValue'
| 'item'
| 'removeProperty'
| 'setProperty'
| number
>,
string
>
>,
): void;
/**
* Checks `expectedStyle` is a subset of the elements computed style i.e. `window.getComputedStyle(element)`.
* @example expect(element).toHaveComputedStyle({ width: '200px' })
*/
toHaveComputedStyle(
expectedStyle: Record<
Exclude<
keyof CSSStyleDeclaration,
| 'getPropertyPriority'
| 'getPropertyValue'
| 'item'
| 'removeProperty'
| 'setProperty'
| number
>,
string
expectedStyle: Partial<
Record<
Exclude<
keyof CSSStyleDeclaration,
| 'getPropertyPriority'
| 'getPropertyValue'
| 'item'
| 'removeProperty'
| 'setProperty'
| number
>,
string
>
>,
): void;
/**
Expand Down