Skip to content

Commit

Permalink
[styles] Add a new injectFirst prop
Browse files Browse the repository at this point in the history
  • Loading branch information
oliviertassinari committed Mar 24, 2019
1 parent e6594d2 commit addb1ac
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 36 deletions.
27 changes: 21 additions & 6 deletions docs/src/pages/css-in-js/advanced/advanced.md
Expand Up @@ -142,13 +142,28 @@ const useStyles = makeStyles({

## CSS injection order

The CSS injected by Material-UI to style a component has the highest specificity possible as the `<link>` is injected at the bottom of the `<head>` to ensure the components always render correctly.
By default, the styles are injected **last** in the `<head>` element of your page.
They gain more specificity than any other style sheet on your page e.g. CSS modules, styled components.

You might, however, also want to override these styles, for example with styled-components.
If you are experiencing a CSS injection order issue, JSS [provides a mechanism](https://github.com/cssinjs/jss/blob/master/docs/setup.md#specify-the-dom-insertion-point) to handle this situation.
### injectFirst

The `StylesProvider` component has a `injectFirst` prop to inject the styles **first**:

```js
import { StylesProvider } from '@material-ui/styles';

<StylesProvider injectFirst>
{/* Your component tree.
Styled components can override Material-UI's styles. */}
</StylesProvider>
```

### insertionPoint

JSS [provides a mechanism](https://github.com/cssinjs/jss/blob/master/docs/setup.md#specify-the-dom-insertion-point) to gain more control on this situation.
By adjusting the placement of the `insertionPoint` within your HTML head you can [control the order](https://cssinjs.org/jss-api#attach-style-sheets-in-a-specific-order) that the CSS rules are applied to your components.

### HTML comment
#### HTML comment

The simplest approach is to add an HTML comment that determines where JSS will inject the styles:

Expand Down Expand Up @@ -176,7 +191,7 @@ function App() {
export default App;
```

### Other HTML element
#### Other HTML element

[Create React App](https://github.com/facebook/create-react-app) strips HTML comments when creating the production build.
To get around the issue, you can provide a DOM element (other than a comment) as the JSS insertion point.
Expand Down Expand Up @@ -207,7 +222,7 @@ function App() {
export default App;
```

### JS createComment
#### JS createComment

codesandbox.io prevents the access to the `<head>` element.
To get around the issue, you can use the JavaScript `document.createComment()` API:
Expand Down
42 changes: 29 additions & 13 deletions docs/src/pages/css-in-js/api/api.md
Expand Up @@ -39,7 +39,7 @@ export default function App() {

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`.
style rules to `makeStyles`/`withStyles` which are a function of the `Theme`.

#### Arguments

Expand All @@ -52,21 +52,20 @@ style rules to `withStyles` which are a function of the `Theme`.
#### Examples

```jsx
import { withStyles, createStyles } from '@material-ui/styles';
import { makeStyles, createStyles } from '@material-ui/styles';

const styles = createStyles({
const styles = makeStyles((theme: Theme) => createStyles({
root: {
backgroundColor: 'red',
backgroundColor: theme.color.red,
},
});
}));

class MyComponent extends React.Component {
render () {
return <div className={this.props.classes.root} />;
}
function MyComponent {
const classes = useStyles();
return <div className={classes.root} />;
}

export default withStyles(styles)(MyComponent);
export default MyComponent;
```

## `makeStyles(styles, [options]) => hook`
Expand Down Expand Up @@ -149,10 +148,20 @@ export default function StyledComponents() {

## `StylesProvider`

This component allows you to change the behavior of the styling solution. It makes the options available down the React tree thanks to React context.
This component allows you to change the behavior of the styling solution. It makes the options available down the React tree thanks to the context.

It should preferably be used at **the root of your component tree**.

#### Props

| Name | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| <span class="prop-name required">children *</span> | <span class="prop-type">node</span> |   | Your component tree. |
| <span class="prop-name">disableGeneration</span> | <span class="prop-type">bool</span> | false | You can disable the generation of the styles with this option. It can be useful when traversing the React tree outside of the HTML rendering step on the server. Let's say you are using react-apollo to extract all the queries made by the interface server-side. You can significantly speed up the traversal with this property. |
| <span class="prop-name">generateClassName</span> | <span class="prop-type">func</span> |   | JSS's class name generator. |
| <span class="prop-name">injectFirst</span> | <span class="prop-type">bool</span> | false | By default, the styles are injected last in the <head> element of your page. They gain more specificity than any other style sheet on your page e.g. CSS modules, styled components. If you want to override the Material-UI's styles, set this prop. |
| <span class="prop-name">jss</span> | <span class="prop-type">object</span> | | JSS's instance. |

#### Examples

```jsx
Expand All @@ -171,9 +180,16 @@ ReactDOM.render(<App />, document.querySelector('#app'));

## `ThemeProvider`

This component takes a `theme` property, and makes the `theme` available down the React tree thanks to React context.
This component takes a `theme` property, and makes it available down the React tree thanks to the context.
It should preferably be used at **the root of your component tree**.

#### Props

| Name | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| <span class="prop-name required">children *</span> | <span class="prop-type">node</span> |   | Your component tree. |
| <span class="prop-name required">theme *</span> | <span class="prop-type">union:&nbsp;object&nbsp;&#124;&nbsp;func</span> | | A theme object. You can provide a function to extend the outer theme. |

#### Examples

```jsx
Expand All @@ -198,7 +214,7 @@ This hook returns the `theme` object so it can be used inside a function compone

#### Returns

`theme`: The theme object.
`theme`: The theme object previously injected in the context.

#### Examples

Expand Down
60 changes: 47 additions & 13 deletions packages/material-ui-styles/src/StylesProvider/StylesProvider.js
Expand Up @@ -29,29 +29,49 @@ const defaultOptions = {

export const StylesContext = React.createContext(defaultOptions);

let injectFirstNode;

function StylesProvider(props) {
const { children, ...localOptions } = props;
const {
children,
generateClassName: generateClassNameProp,
injectFirst,
...localOptions
} = props;

const outerOptions = React.useContext(StylesContext);
const context = { ...outerOptions, ...localOptions };

warning(
typeof window !== 'undefined' || localOptions.sheetsManager,
[
'Material-UI: you need to provide a sheetsManager to the <StyleProvider> ' +
'when rendering on the server.',
].join('\n'),
typeof window !== 'undefined' || context.sheetsManager,
'Material-UI: you need to provide a sheetsManager to the <StyleProvider> ' +
'when rendering on the server.',
);

const outerOptions = React.useContext(StylesContext);
if (generateClassNameProp) {
context.generateClassName = generateClassNameProp;
}

return (
<StylesContext.Provider value={{ ...outerOptions, ...localOptions }}>
{children}
</StylesContext.Provider>
warning(
!context.jss.options.insertionPoint || !injectFirst,
'Material-UI: you cannot use a custom insertionPoint and <StylesContext injectFirst> at the same time.',
);

if (!context.jss.options.insertionPoint && injectFirst && typeof window !== 'undefined') {
if (!injectFirstNode) {
const head = document.head;
injectFirstNode = document.createComment('mui-inject-first');
head.insertBefore(injectFirstNode, head.firstChild);
}
context.jss.options.insertionPoint = injectFirstNode;
}

return <StylesContext.Provider value={context}>{children}</StylesContext.Provider>;
}

StylesProvider.propTypes = {
/**
* You can wrap a node.
* Your component tree.
*/
children: PropTypes.node.isRequired,
/**
Expand All @@ -67,22 +87,35 @@ StylesProvider.propTypes = {
* JSS's class name generator.
*/
generateClassName: PropTypes.func,
/**
* By default, the styles are injected last in the <head> element of your page.
* They gain more specificity than any other style sheet on your page e.g. CSS modules, styled components.
* If you want to override the Material-UI's styles, set this prop.
*/
injectFirst: PropTypes.bool,
/**
* JSS's instance.
*/
jss: PropTypes.object,
/**
* @ignore
*
* In beta.
* Cache the sheets
*/
sheetsCache: PropTypes.object,
/**
* @ignore.
*
* The sheetsManager is used to deduplicate style sheet injection in the page.
* It's deduplicating using the (theme, styles) couple.
* On the server, you should provide a new instance for each request.
*/
sheetsManager: PropTypes.object,
/**
* @ignore
*
* Collect the sheets.
*/
sheetsRegistry: PropTypes.object,
};

Expand All @@ -92,6 +125,7 @@ if (process.env.NODE_ENV !== 'production') {

StylesProvider.defaultProps = {
disableGeneration: false,
injectFirst: false,
};

export default StylesProvider;
Expand Up @@ -11,6 +11,10 @@ function Test() {
return <span options={options} />;
}

function getOptions(wrapper) {
return wrapper.find('span').props().options;
}

describe('StylesProvider', () => {
let mount;

Expand All @@ -28,7 +32,7 @@ describe('StylesProvider', () => {
<Test />
</StylesProvider>,
);
assert.strictEqual(wrapper.find('span').props().options.disableGeneration, true);
assert.strictEqual(getOptions(wrapper).disableGeneration, true);
});

it('should merge the themes', () => {
Expand All @@ -39,7 +43,16 @@ describe('StylesProvider', () => {
</StylesProvider>
</StylesProvider>,
);
assert.strictEqual(wrapper.find('span').props().options.disableGeneration, true);
assert.strictEqual(getOptions(wrapper).disableGeneration, true);
});

it('should handle injectFirst', () => {
const wrapper = mount(
<StylesProvider injectFirst>
<Test />
</StylesProvider>,
);
assert.strictEqual(getOptions(wrapper).jss.options.insertionPoint.nodeType, 8);
});

describe('server-side', () => {
Expand Down
Expand Up @@ -52,11 +52,11 @@ function ThemeProvider(props) {

ThemeProvider.propTypes = {
/**
* You can wrap a node.
* Your component tree
*/
children: PropTypes.node.isRequired,
/**
* A theme object.
* A theme object. You can provide a function to extend the outer theme.
*/
theme: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
};
Expand Down

0 comments on commit addb1ac

Please sign in to comment.