Skip to content

Commit

Permalink
[RFC] Use global class names
Browse files Browse the repository at this point in the history
  • Loading branch information
oliviertassinari committed Apr 14, 2019
1 parent 8fd19b8 commit a694362
Show file tree
Hide file tree
Showing 6 changed files with 409 additions and 125 deletions.
16 changes: 12 additions & 4 deletions packages/material-ui-styles/src/ThemeProvider/ThemeProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ function mergeOuterLocalTheme(outerTheme, localTheme) {
return { ...outerTheme, ...localTheme };
}

export const nested = Symbol('nested');

/**
* This component takes a `theme` property.
* It makes the `theme` available down the React tree thanks to React context.
Expand All @@ -46,10 +48,16 @@ function ThemeProvider(props) {
].join('\n'),
);

const theme = React.useMemo(
() => (outerTheme === null ? localTheme : mergeOuterLocalTheme(outerTheme, localTheme)),
[localTheme, outerTheme],
);
const theme = React.useMemo(() => {
const output = outerTheme === null ? localTheme : mergeOuterLocalTheme(outerTheme, localTheme);

if (outerTheme !== null) {
output[nested] = true;
}

return output;
}, [localTheme, outerTheme]);

return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,54 @@
import warning from 'warning';
import { nested } from '../ThemeProvider/ThemeProvider';

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

/**
* This is the list of the style rule name we use as drop in replacement for the built-in
* pseudo classes (:checked, :disabled, :focused, etc.).
*
* Why do they exist in the first place?
* These classes are used for styling the dynamic states of the components.
*/
const pseudoClasses = [
'checked',
'disabled',
'error',
'focused',
'focusVisible',
'required',
'selected',
];

// Returns a function which generates unique class names based on counters.
// When new generator function is created, rule counter is reset.
// We need to reset the rule counter for SSR for each request.
//
// It's inspired by
// https://github.com/cssinjs/jss/blob/4e6a05dd3f7b6572fdd3ab216861d9e446c20331/src/utils/createGenerateClassName.js
export default function createGenerateClassName(options = {}) {
const { dangerouslyUseGlobalCSS = false, productionPrefix = 'jss', seed = '' } = options;
const { productionPrefix = 'jss', seed = '' } = options;
let ruleCounter = 0;

return (rule, styleSheet) => {
const isStatic = !styleSheet.options.link;
const isStatic =
!styleSheet.options.link && styleSheet.options.name && !styleSheet.options.theme[nested];
const prefix = isStatic ? safePrefix(styleSheet.options.name) : null;

if (isStatic && rule.key === 'root') {
return prefix;
}

if (dangerouslyUseGlobalCSS && styleSheet && styleSheet.options.name && isStatic) {
return `${safePrefix(styleSheet.options.name)}-${rule.key}`;
if (isStatic && pseudoClasses.indexOf(rule.key) !== -1) {
return rule.key;
}

if (isStatic) {
return `${prefix}-${rule.key}`;
}

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

if (process.env.NODE_ENV === 'production') {
if (process.env.NODE_ENV === 'production' && productionPrefix !== '') {
return `${productionPrefix}${seed}${ruleCounter}`;
}

const suffix = `${rule.key}-${seed}${ruleCounter}`;

// Help with debuggability.
if (styleSheet.options.classNamePrefix) {
return `${safePrefix(styleSheet.options.classNamePrefix)}-${rule.key}-${seed}${ruleCounter}`;
return `${safePrefix(styleSheet.options.classNamePrefix)}-${suffix}`;
}

return `${rule.key}-${seed}${ruleCounter}`;
return suffix;
};
}
Original file line number Diff line number Diff line change
@@ -1,66 +1,115 @@
import warning from 'warning';
import hash from '@emotion/hash';
import { assert } from 'chai';
import consoleErrorMock from 'test/utils/consoleErrorMock';
import createGenerateClassName from './createGenerateClassName';

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

const themeHashCache = {};
it('should generate a class name', () => {
assert.strictEqual(
generateClassName(
{
key: 'key',
},
{
options: {
theme: {},
classNamePrefix: 'classNamePrefix',
},
},
),
'classNamePrefix-key-1',
);
});

// Returns a function which generates unique class names based on counters.
// When new generator function is created, rule counter is reset.
// We need to reset the rule counter for SSR for each request.
//
// It's inspired by
// https://github.com/cssinjs/jss/blob/4e6a05dd3f7b6572fdd3ab216861d9e446c20331/src/utils/createGenerateClassName.js
export default function createGenerateClassName(options = {}) {
const { dangerouslyUseGlobalCSS = false, productionPrefix = 'jss', seed = '' } = options;
let ruleCounter = 0;
it('should increase the counter only when needed', () => {
assert.strictEqual(
generateClassName(
{
key: 'key',
},
{
options: {
theme: {},
classNamePrefix: 'classNamePrefix',
},
},
),
'classNamePrefix-key-2',
);
assert.strictEqual(
generateClassName(
{
key: 'key',
},
{
options: {
link: true,
classNamePrefix: 'classNamePrefix',
},
},
),
'classNamePrefix-key-3',
);
assert.strictEqual(
generateClassName(
{
key: 'key',
},
{
options: {
link: true,
classNamePrefix: 'classNamePrefix',
},
},
),
'classNamePrefix-key-4',
);
});

return (rule, styleSheet) => {
const isStatic = !styleSheet.options.link;

if (dangerouslyUseGlobalCSS && styleSheet && styleSheet.options.name && isStatic) {
return `${safePrefix(styleSheet.options.name)}-${rule.key}`;
}

let suffix;

// It's a static rule.
if (isStatic) {
let themeHash = themeHashCache[styleSheet.options.theme];
if (!themeHash) {
themeHash = hash(JSON.stringify(styleSheet.options.theme));
themeHashCache[styleSheet.theme] = themeHash;
}
const raw = styleSheet.rules.raw[rule.key];
suffix = hash(`${themeHash}${rule.key}${JSON.stringify(raw)}`);
}

if (!suffix) {
ruleCounter += 1;
warning(
ruleCounter < 1e10,
[
'Material-UI: you might have a memory leak.',
'The ruleCounter is not supposed to grow that much.',
].join(''),
describe('classNamePrefix', () => {
it('should work without a classNamePrefix', () => {
const generateClassName2 = createGenerateClassName();
assert.strictEqual(
generateClassName2(
{ key: 'root' },
{
options: {},
},
),
'root-1',
);
});
});

suffix = ruleCounter;
describe('production', () => {
// Only run the test on node.
if (!/jsdom/.test(window.navigator.userAgent)) {
return;
}

if (process.env.NODE_ENV === 'production') {
return `${productionPrefix}${seed}${suffix}`;
}
let nodeEnv;
const env = process.env;

// Help with debuggability.
if (styleSheet.options.classNamePrefix) {
return `${safePrefix(styleSheet.options.classNamePrefix)}-${rule.key}-${seed}${suffix}`;
}
before(() => {
nodeEnv = env.NODE_ENV;
env.NODE_ENV = 'production';
consoleErrorMock.spy();
});

after(() => {
env.NODE_ENV = nodeEnv;
consoleErrorMock.reset();
});

return `${rule.key}-${seed}${suffix}`;
};
}
it('should output a short representation', () => {
const rule = { key: 'root' };
const styleSheet = {
rules: { raw: {} },
options: {},
};
const generateClassName2 = createGenerateClassName();
assert.strictEqual(generateClassName2(rule, styleSheet), 'jss1');
});
});
});
Loading

0 comments on commit a694362

Please sign in to comment.