Skip to content

Commit a694362

Browse files
[RFC] Use global class names
1 parent 8fd19b8 commit a694362

File tree

6 files changed

+409
-125
lines changed

6 files changed

+409
-125
lines changed

packages/material-ui-styles/src/ThemeProvider/ThemeProvider.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ function mergeOuterLocalTheme(outerTheme, localTheme) {
2424
return { ...outerTheme, ...localTheme };
2525
}
2626

27+
export const nested = Symbol('nested');
28+
2729
/**
2830
* This component takes a `theme` property.
2931
* It makes the `theme` available down the React tree thanks to React context.
@@ -46,10 +48,16 @@ function ThemeProvider(props) {
4648
].join('\n'),
4749
);
4850

49-
const theme = React.useMemo(
50-
() => (outerTheme === null ? localTheme : mergeOuterLocalTheme(outerTheme, localTheme)),
51-
[localTheme, outerTheme],
52-
);
51+
const theme = React.useMemo(() => {
52+
const output = outerTheme === null ? localTheme : mergeOuterLocalTheme(outerTheme, localTheme);
53+
54+
if (outerTheme !== null) {
55+
output[nested] = true;
56+
}
57+
58+
return output;
59+
}, [localTheme, outerTheme]);
60+
5361
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
5462
}
5563

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,54 @@
11
import warning from 'warning';
2+
import { nested } from '../ThemeProvider/ThemeProvider';
23

34
function safePrefix(classNamePrefix) {
45
const prefix = String(classNamePrefix);
56
warning(prefix.length < 256, `Material-UI: the class name prefix is too long: ${prefix}.`);
67
return prefix;
78
}
89

10+
/**
11+
* This is the list of the style rule name we use as drop in replacement for the built-in
12+
* pseudo classes (:checked, :disabled, :focused, etc.).
13+
*
14+
* Why do they exist in the first place?
15+
* These classes are used for styling the dynamic states of the components.
16+
*/
17+
const pseudoClasses = [
18+
'checked',
19+
'disabled',
20+
'error',
21+
'focused',
22+
'focusVisible',
23+
'required',
24+
'selected',
25+
];
26+
927
// Returns a function which generates unique class names based on counters.
1028
// When new generator function is created, rule counter is reset.
1129
// We need to reset the rule counter for SSR for each request.
1230
//
1331
// It's inspired by
1432
// https://github.com/cssinjs/jss/blob/4e6a05dd3f7b6572fdd3ab216861d9e446c20331/src/utils/createGenerateClassName.js
1533
export default function createGenerateClassName(options = {}) {
16-
const { dangerouslyUseGlobalCSS = false, productionPrefix = 'jss', seed = '' } = options;
34+
const { productionPrefix = 'jss', seed = '' } = options;
1735
let ruleCounter = 0;
1836

1937
return (rule, styleSheet) => {
20-
const isStatic = !styleSheet.options.link;
38+
const isStatic =
39+
!styleSheet.options.link && styleSheet.options.name && !styleSheet.options.theme[nested];
40+
const prefix = isStatic ? safePrefix(styleSheet.options.name) : null;
41+
42+
if (isStatic && rule.key === 'root') {
43+
return prefix;
44+
}
2145

22-
if (dangerouslyUseGlobalCSS && styleSheet && styleSheet.options.name && isStatic) {
23-
return `${safePrefix(styleSheet.options.name)}-${rule.key}`;
46+
if (isStatic && pseudoClasses.indexOf(rule.key) !== -1) {
47+
return rule.key;
48+
}
49+
50+
if (isStatic) {
51+
return `${prefix}-${rule.key}`;
2452
}
2553

2654
ruleCounter += 1;
@@ -32,15 +60,17 @@ export default function createGenerateClassName(options = {}) {
3260
].join(''),
3361
);
3462

35-
if (process.env.NODE_ENV === 'production') {
63+
if (process.env.NODE_ENV === 'production' && productionPrefix !== '') {
3664
return `${productionPrefix}${seed}${ruleCounter}`;
3765
}
3866

67+
const suffix = `${rule.key}-${seed}${ruleCounter}`;
68+
3969
// Help with debuggability.
4070
if (styleSheet.options.classNamePrefix) {
41-
return `${safePrefix(styleSheet.options.classNamePrefix)}-${rule.key}-${seed}${ruleCounter}`;
71+
return `${safePrefix(styleSheet.options.classNamePrefix)}-${suffix}`;
4272
}
4373

44-
return `${rule.key}-${seed}${ruleCounter}`;
74+
return suffix;
4575
};
4676
}
Lines changed: 105 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,115 @@
1-
import warning from 'warning';
2-
import hash from '@emotion/hash';
1+
import { assert } from 'chai';
2+
import consoleErrorMock from 'test/utils/consoleErrorMock';
3+
import createGenerateClassName from './createGenerateClassName';
34

4-
function safePrefix(classNamePrefix) {
5-
const prefix = String(classNamePrefix);
6-
warning(prefix.length < 256, `Material-UI: the class name prefix is too long: ${prefix}.`);
7-
return prefix;
8-
}
5+
describe('createGenerateClassName', () => {
6+
const generateClassName = createGenerateClassName();
97

10-
const themeHashCache = {};
8+
it('should generate a class name', () => {
9+
assert.strictEqual(
10+
generateClassName(
11+
{
12+
key: 'key',
13+
},
14+
{
15+
options: {
16+
theme: {},
17+
classNamePrefix: 'classNamePrefix',
18+
},
19+
},
20+
),
21+
'classNamePrefix-key-1',
22+
);
23+
});
1124

12-
// Returns a function which generates unique class names based on counters.
13-
// When new generator function is created, rule counter is reset.
14-
// We need to reset the rule counter for SSR for each request.
15-
//
16-
// It's inspired by
17-
// https://github.com/cssinjs/jss/blob/4e6a05dd3f7b6572fdd3ab216861d9e446c20331/src/utils/createGenerateClassName.js
18-
export default function createGenerateClassName(options = {}) {
19-
const { dangerouslyUseGlobalCSS = false, productionPrefix = 'jss', seed = '' } = options;
20-
let ruleCounter = 0;
25+
it('should increase the counter only when needed', () => {
26+
assert.strictEqual(
27+
generateClassName(
28+
{
29+
key: 'key',
30+
},
31+
{
32+
options: {
33+
theme: {},
34+
classNamePrefix: 'classNamePrefix',
35+
},
36+
},
37+
),
38+
'classNamePrefix-key-2',
39+
);
40+
assert.strictEqual(
41+
generateClassName(
42+
{
43+
key: 'key',
44+
},
45+
{
46+
options: {
47+
link: true,
48+
classNamePrefix: 'classNamePrefix',
49+
},
50+
},
51+
),
52+
'classNamePrefix-key-3',
53+
);
54+
assert.strictEqual(
55+
generateClassName(
56+
{
57+
key: 'key',
58+
},
59+
{
60+
options: {
61+
link: true,
62+
classNamePrefix: 'classNamePrefix',
63+
},
64+
},
65+
),
66+
'classNamePrefix-key-4',
67+
);
68+
});
2169

22-
return (rule, styleSheet) => {
23-
const isStatic = !styleSheet.options.link;
24-
25-
if (dangerouslyUseGlobalCSS && styleSheet && styleSheet.options.name && isStatic) {
26-
return `${safePrefix(styleSheet.options.name)}-${rule.key}`;
27-
}
28-
29-
let suffix;
30-
31-
// It's a static rule.
32-
if (isStatic) {
33-
let themeHash = themeHashCache[styleSheet.options.theme];
34-
if (!themeHash) {
35-
themeHash = hash(JSON.stringify(styleSheet.options.theme));
36-
themeHashCache[styleSheet.theme] = themeHash;
37-
}
38-
const raw = styleSheet.rules.raw[rule.key];
39-
suffix = hash(`${themeHash}${rule.key}${JSON.stringify(raw)}`);
40-
}
41-
42-
if (!suffix) {
43-
ruleCounter += 1;
44-
warning(
45-
ruleCounter < 1e10,
46-
[
47-
'Material-UI: you might have a memory leak.',
48-
'The ruleCounter is not supposed to grow that much.',
49-
].join(''),
70+
describe('classNamePrefix', () => {
71+
it('should work without a classNamePrefix', () => {
72+
const generateClassName2 = createGenerateClassName();
73+
assert.strictEqual(
74+
generateClassName2(
75+
{ key: 'root' },
76+
{
77+
options: {},
78+
},
79+
),
80+
'root-1',
5081
);
82+
});
83+
});
5184

52-
suffix = ruleCounter;
85+
describe('production', () => {
86+
// Only run the test on node.
87+
if (!/jsdom/.test(window.navigator.userAgent)) {
88+
return;
5389
}
5490

55-
if (process.env.NODE_ENV === 'production') {
56-
return `${productionPrefix}${seed}${suffix}`;
57-
}
91+
let nodeEnv;
92+
const env = process.env;
5893

59-
// Help with debuggability.
60-
if (styleSheet.options.classNamePrefix) {
61-
return `${safePrefix(styleSheet.options.classNamePrefix)}-${rule.key}-${seed}${suffix}`;
62-
}
94+
before(() => {
95+
nodeEnv = env.NODE_ENV;
96+
env.NODE_ENV = 'production';
97+
consoleErrorMock.spy();
98+
});
99+
100+
after(() => {
101+
env.NODE_ENV = nodeEnv;
102+
consoleErrorMock.reset();
103+
});
63104

64-
return `${rule.key}-${seed}${suffix}`;
65-
};
66-
}
105+
it('should output a short representation', () => {
106+
const rule = { key: 'root' };
107+
const styleSheet = {
108+
rules: { raw: {} },
109+
options: {},
110+
};
111+
const generateClassName2 = createGenerateClassName();
112+
assert.strictEqual(generateClassName2(rule, styleSheet), 'jss1');
113+
});
114+
});
115+
});

0 commit comments

Comments
 (0)