Skip to content

Commit 1e3bc96

Browse files
[RFC] Use global class names
1 parent bbfbb0c commit 1e3bc96

File tree

7 files changed

+426
-136
lines changed

7 files changed

+426
-136
lines changed

docs/src/pages/demos/text-fields/CustomizedInputs.js

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,17 @@ const styles = theme => ({
2121
margin: theme.spacing(1),
2222
},
2323
cssLabel: {
24-
'&$cssFocused': {
24+
'&.focused': {
2525
color: purple[500],
2626
},
2727
},
28-
cssFocused: {},
2928
cssUnderline: {
3029
'&:after': {
3130
borderBottomColor: purple[500],
3231
},
3332
},
3433
cssOutlinedInput: {
35-
'&$cssFocused $notchedOutline': {
34+
'&.focused $notchedOutline': {
3635
borderColor: purple[500],
3736
},
3837
},
@@ -45,7 +44,7 @@ const styles = theme => ({
4544
bootstrapInput: {
4645
borderRadius: 4,
4746
position: 'relative',
48-
backgroundColor: theme.palette.common.white,
47+
backgroundColor: theme.palette.background.paper,
4948
border: '1px solid #ced4da',
5049
fontSize: 16,
5150
width: 'auto',
@@ -76,24 +75,26 @@ const styles = theme => ({
7675
border: '1px solid #e2e2e1',
7776
overflow: 'hidden',
7877
borderRadius: 4,
79-
backgroundColor: '#fcfcfb',
78+
backgroundColor: theme.palette.background.default,
8079
transition: theme.transitions.create(['border-color', 'box-shadow']),
8180
'&:hover': {
82-
backgroundColor: '#fff',
81+
backgroundColor: theme.palette.background.paper,
8382
},
8483
'&.focused': {
85-
backgroundColor: '#fff',
84+
backgroundColor: theme.palette.background.paper,
8685
boxShadow: `${fade(theme.palette.primary.main, 0.25)} 0 0 0 2px`,
8786
borderColor: theme.palette.primary.main,
8887
},
8988
},
9089
});
9190

92-
const theme = createMuiTheme({
93-
palette: {
94-
primary: green,
95-
},
96-
});
91+
const theme = outerTheme =>
92+
createMuiTheme({
93+
palette: {
94+
...outerTheme.palette,
95+
primary: green,
96+
},
97+
});
9798

9899
function CustomizedInputs(props) {
99100
const { classes } = props;
@@ -105,7 +106,6 @@ function CustomizedInputs(props) {
105106
htmlFor="custom-css-standard-input"
106107
classes={{
107108
root: classes.cssLabel,
108-
focused: classes.cssFocused,
109109
}}
110110
>
111111
Custom CSS
@@ -122,13 +122,11 @@ function CustomizedInputs(props) {
122122
InputLabelProps={{
123123
classes: {
124124
root: classes.cssLabel,
125-
focused: classes.cssFocused,
126125
},
127126
}}
128127
InputProps={{
129128
classes: {
130129
root: classes.cssOutlinedInput,
131-
focused: classes.cssFocused,
132130
notchedOutline: classes.notchedOutline,
133131
},
134132
}}
@@ -170,7 +168,6 @@ function CustomizedInputs(props) {
170168
disableUnderline
171169
classes={{
172170
root: classes.redditRoot,
173-
focused: 'focused',
174171
}}
175172
/>
176173
</FormControl>

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

Lines changed: 7 additions & 0 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.
@@ -47,6 +49,11 @@ function ThemeProvider(props) {
4749
);
4850

4951
const theme = outerTheme === null ? localTheme : mergeOuterLocalTheme(outerTheme, localTheme);
52+
53+
if (theme && outerTheme !== null) {
54+
theme[nested] = true
55+
}
56+
5057
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
5158
}
5259

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

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,64 @@
11
import warning from 'warning';
2+
import { nested } from '../ThemeProvider/ThemeProvider';
3+
4+
const uppercasePattern = /[A-Z]/g;
5+
const cache = {};
6+
7+
function toHyphenLower(match) {
8+
return `-${match.toLowerCase()}`;
9+
}
10+
11+
function camelCase(name) {
12+
if (cache[name]) {
13+
return cache[name];
14+
}
15+
16+
const hName = name.replace(uppercasePattern, toHyphenLower).replace(/^-/, '');
17+
cache[name] = hName;
18+
return hName;
19+
}
220

321
function safePrefix(classNamePrefix) {
422
const prefix = String(classNamePrefix);
523
warning(prefix.length < 256, `Material-UI: the class name prefix is too long: ${prefix}.`);
624
return prefix;
725
}
826

27+
const pseudoClasses = [
28+
'checked',
29+
'disabled',
30+
'error',
31+
'focused',
32+
'focusVisible',
33+
'required',
34+
'selected',
35+
];
36+
937
// Returns a function which generates unique class names based on counters.
1038
// When new generator function is created, rule counter is reset.
1139
// We need to reset the rule counter for SSR for each request.
1240
//
1341
// It's inspired by
1442
// https://github.com/cssinjs/jss/blob/4e6a05dd3f7b6572fdd3ab216861d9e446c20331/src/utils/createGenerateClassName.js
1543
export default function createGenerateClassName(options = {}) {
16-
const { dangerouslyUseGlobalCSS = false, productionPrefix = 'jss', seed = '' } = options;
44+
const { productionPrefix = 'jss', seed = '' } = options;
1745
let ruleCounter = 0;
1846

1947
return (rule, styleSheet) => {
20-
const isStatic = !styleSheet.options.link;
48+
const isStatic =
49+
!styleSheet.options.link && styleSheet.options.name && !styleSheet.options.theme[nested];
50+
const prefix = isStatic ? camelCase(safePrefix(styleSheet.options.name)) : null;
51+
52+
if (isStatic && rule.key === 'root') {
53+
return prefix;
54+
}
2155

22-
if (dangerouslyUseGlobalCSS && styleSheet && styleSheet.options.name && isStatic) {
23-
return `${safePrefix(styleSheet.options.name)}-${rule.key}`;
56+
if (isStatic && pseudoClasses.indexOf(rule.key) !== -1) {
57+
return rule.key;
58+
}
59+
60+
if (isStatic) {
61+
return `${prefix}--${camelCase(rule.key)}`;
2462
}
2563

2664
ruleCounter += 1;
@@ -36,11 +74,13 @@ export default function createGenerateClassName(options = {}) {
3674
return `${productionPrefix}${seed}${ruleCounter}`;
3775
}
3876

77+
const suffix = `${rule.key}-${seed}${ruleCounter}`;
78+
3979
// Help with debuggability.
4080
if (styleSheet.options.classNamePrefix) {
41-
return `${safePrefix(styleSheet.options.classNamePrefix)}-${rule.key}-${seed}${ruleCounter}`;
81+
return `${safePrefix(styleSheet.options.classNamePrefix)}-${suffix}`;
4282
}
4383

44-
return `${rule.key}-${seed}${ruleCounter}`;
84+
return suffix;
4585
};
4686
}
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)