Skip to content

Commit 0f8332d

Browse files
[styled-engine] Add enableCssLayer prop to StyledEngineProvider (@siriwatknp) (#45563)
1 parent db780c0 commit 0f8332d

File tree

3 files changed

+120
-15
lines changed

3 files changed

+120
-15
lines changed

packages/mui-styled-engine/src/StyledEngineProvider/StyledEngineProvider.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22

33
export interface StyledEngineProviderProps {
44
children?: React.ReactNode;
5+
enableCssLayer?: boolean;
56
injectFirst?: boolean;
67
}
78

packages/mui-styled-engine/src/StyledEngineProvider/StyledEngineProvider.js

+67-15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ import { CacheProvider } from '@emotion/react';
55
import createCache from '@emotion/cache';
66
import { StyleSheet } from '@emotion/sheet';
77

8+
// Need to add a private variable to test the generated CSS from Emotion, this is the simplest way to do it.
9+
// We can't test the CSS from `style` tag easily because the `speedy: true` (produce empty text content) is enabled by Emotion.
10+
// Even if we disable it, JSDOM needs extra configuration to be able to parse `@layer` CSS.
11+
export const TEST_INTERNALS_DO_NOT_USE = {
12+
/**
13+
* to intercept the generated CSS before inserting to the style tag, so that we can check the generated CSS.
14+
*
15+
* let rule;
16+
* TEST_INTERNALS_DO_NOT_USE.insert = (...args) => {
17+
* rule = args[0];
18+
* };
19+
*
20+
* expect(rule).to.equal(...);
21+
*/
22+
insert: undefined,
23+
};
24+
825
// We might be able to remove this when this issue is fixed:
926
// https://github.com/emotion-js/emotion/issues/2790
1027
const createEmotionCache = (options, CustomSheet) => {
@@ -23,11 +40,11 @@ const createEmotionCache = (options, CustomSheet) => {
2340
return cache;
2441
};
2542

26-
let cache;
43+
let insertionPoint;
2744
if (typeof document === 'object') {
2845
// Use `insertionPoint` over `prepend`(deprecated) because it can be controlled for GlobalStyles injection order
2946
// For more information, see https://github.com/mui/material-ui/issues/44597
30-
let insertionPoint = document.querySelector('[name="emotion-insertion-point"]');
47+
insertionPoint = document.querySelector('[name="emotion-insertion-point"]');
3148
if (!insertionPoint) {
3249
insertionPoint = document.createElement('meta');
3350
insertionPoint.setAttribute('name', 'emotion-insertion-point');
@@ -37,32 +54,67 @@ if (typeof document === 'object') {
3754
head.prepend(insertionPoint);
3855
}
3956
}
40-
/**
41-
* This is for client-side apps only.
42-
* A custom sheet is required to make the GlobalStyles API injected above the insertion point.
43-
* This is because the [sheet](https://github.com/emotion-js/emotion/blob/main/packages/react/src/global.js#L94-L99) does not consume the options.
44-
*/
45-
class MyStyleSheet extends StyleSheet {
46-
insert(rule, options) {
47-
if (this.key && this.key.endsWith('global')) {
48-
this.before = insertionPoint;
57+
}
58+
59+
function getCache(injectFirst, enableCssLayer) {
60+
if (injectFirst || enableCssLayer) {
61+
/**
62+
* This is for client-side apps only.
63+
* A custom sheet is required to make the GlobalStyles API injected above the insertion point.
64+
* This is because the [sheet](https://github.com/emotion-js/emotion/blob/main/packages/react/src/global.js#L94-L99) does not consume the options.
65+
*/
66+
class MyStyleSheet extends StyleSheet {
67+
insert(rule, options) {
68+
if (TEST_INTERNALS_DO_NOT_USE.insert) {
69+
return TEST_INTERNALS_DO_NOT_USE.insert(rule, options);
70+
}
71+
if (this.key && this.key.endsWith('global')) {
72+
this.before = insertionPoint;
73+
}
74+
return super.insert(rule, options);
4975
}
50-
return super.insert(rule, options);
5176
}
77+
const emotionCache = createEmotionCache(
78+
{
79+
key: 'css',
80+
insertionPoint: injectFirst ? insertionPoint : undefined,
81+
},
82+
MyStyleSheet,
83+
);
84+
if (enableCssLayer) {
85+
const prevInsert = emotionCache.insert;
86+
emotionCache.insert = (...args) => {
87+
if (!args[1].styles.startsWith('@layer')) {
88+
// avoid nested @layer
89+
args[1].styles = `@layer mui {${args[1].styles}}`;
90+
}
91+
return prevInsert(...args);
92+
};
93+
}
94+
return emotionCache;
5295
}
53-
cache = createEmotionCache({ key: 'css', insertionPoint }, MyStyleSheet);
96+
return undefined;
5497
}
5598

5699
export default function StyledEngineProvider(props) {
57-
const { injectFirst, children } = props;
58-
return injectFirst && cache ? <CacheProvider value={cache}>{children}</CacheProvider> : children;
100+
const { injectFirst, enableCssLayer, children } = props;
101+
const cache = React.useMemo(
102+
() => getCache(injectFirst, enableCssLayer),
103+
[injectFirst, enableCssLayer],
104+
);
105+
return cache ? <CacheProvider value={cache}>{children}</CacheProvider> : children;
59106
}
60107

61108
StyledEngineProvider.propTypes = {
62109
/**
63110
* Your component tree.
64111
*/
65112
children: PropTypes.node,
113+
/**
114+
* If `true`, the styles are wrapped in `@layer mui`.
115+
* Learn more about [Cascade layers](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers).
116+
*/
117+
enableCssLayer: PropTypes.bool,
66118
/**
67119
* By default, the styles are injected last in the <head> element of the page.
68120
* As a result, they gain more specificity than any other style sheet.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as React from 'react';
2+
import { StyledEngineProvider, GlobalStyles } from '@mui/styled-engine';
3+
import { createRenderer } from '@mui/internal-test-utils';
4+
import { expect } from 'chai';
5+
import { TEST_INTERNALS_DO_NOT_USE } from './StyledEngineProvider';
6+
7+
describe('[Emotion] StyledEngineProvider', () => {
8+
const { render } = createRenderer();
9+
10+
let rule;
11+
12+
before(() => {
13+
TEST_INTERNALS_DO_NOT_USE.insert = (...args) => {
14+
rule = args[0];
15+
};
16+
});
17+
18+
after(() => {
19+
delete TEST_INTERNALS_DO_NOT_USE.insert;
20+
});
21+
22+
beforeEach(() => {
23+
rule = undefined;
24+
});
25+
26+
it('should create styles with @layer', () => {
27+
render(
28+
<StyledEngineProvider enableCssLayer>
29+
<GlobalStyles styles={{ html: { color: 'red' } }} />
30+
</StyledEngineProvider>,
31+
);
32+
expect(rule).to.equal('@layer mui{html{color:red;}}');
33+
});
34+
35+
it('should do nothing if the styles already in a layer', () => {
36+
render(
37+
<StyledEngineProvider enableCssLayer>
38+
<GlobalStyles styles={{ '@layer components': { html: { color: 'red' } } }} />
39+
</StyledEngineProvider>,
40+
);
41+
expect(rule).to.equal('@layer components{html{color:red;}}');
42+
});
43+
44+
it('able to config layer order through GlobalStyles', () => {
45+
render(
46+
<StyledEngineProvider enableCssLayer>
47+
<GlobalStyles styles="@layer theme, base, mui, components, utilities;" />
48+
</StyledEngineProvider>,
49+
);
50+
expect(rule).to.equal('@layer theme,base,mui,components,utilities;');
51+
});
52+
});

0 commit comments

Comments
 (0)