From 9eab1520d7366828ec96e545d49213ef49ee9ac6 Mon Sep 17 00:00:00 2001 From: garronej Date: Thu, 31 Mar 2022 03:19:22 +0200 Subject: [PATCH 1/2] Use cx instead of clsx --- packages/mui-material/src/Button/Button.js | 10 +- .../mui-material/src/ButtonBase/ButtonBase.js | 6 +- packages/mui-styled-engine-sc/src/cx.d.ts | 8 + packages/mui-styled-engine-sc/src/cx.js | 8 + packages/mui-styled-engine-sc/src/index.d.ts | 1 + packages/mui-styled-engine-sc/src/index.js | 1 + .../src/tools/classnames.d.ts | 13 ++ .../src/tools/classnames.js | 58 +++++++ packages/mui-styled-engine/src/cx.d.ts | 8 + packages/mui-styled-engine/src/cx.js | 149 ++++++++++++++++++ packages/mui-styled-engine/src/index.d.ts | 1 + packages/mui-styled-engine/src/index.js | 1 + .../src/tools/classnames.d.ts | 13 ++ .../mui-styled-engine/src/tools/classnames.js | 58 +++++++ test/regressions/fixtures/Cx/Cx.js | 24 +++ 15 files changed, 353 insertions(+), 6 deletions(-) create mode 100644 packages/mui-styled-engine-sc/src/cx.d.ts create mode 100644 packages/mui-styled-engine-sc/src/cx.js create mode 100644 packages/mui-styled-engine-sc/src/tools/classnames.d.ts create mode 100644 packages/mui-styled-engine-sc/src/tools/classnames.js create mode 100644 packages/mui-styled-engine/src/cx.d.ts create mode 100644 packages/mui-styled-engine/src/cx.js create mode 100644 packages/mui-styled-engine/src/tools/classnames.d.ts create mode 100644 packages/mui-styled-engine/src/tools/classnames.js create mode 100644 test/regressions/fixtures/Cx/Cx.js diff --git a/packages/mui-material/src/Button/Button.js b/packages/mui-material/src/Button/Button.js index 4a8dd782d87d1f..377b00b843cd4a 100644 --- a/packages/mui-material/src/Button/Button.js +++ b/packages/mui-material/src/Button/Button.js @@ -1,9 +1,9 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import clsx from 'clsx'; import { internal_resolveProps as resolveProps } from '@mui/utils'; import { unstable_composeClasses as composeClasses } from '@mui/base'; import { alpha } from '@mui/system'; +import { useCx } from '@mui/styled-engine'; import styled, { rootShouldForwardProp } from '../styles/styled'; import useThemeProps from '../styles/useThemeProps'; import ButtonBase from '../ButtonBase'; @@ -322,7 +322,7 @@ const Button = React.forwardRef(function Button(inProps, ref) { variant, }; - const classes = useUtilityClasses(ownerState); + const { root: classes_root, ...classes } = useUtilityClasses(ownerState); const startIcon = startIconProp && ( @@ -336,14 +336,16 @@ const Button = React.forwardRef(function Button(inProps, ref) { ); + const { cx } = useCx(); + return ( string; + +export declare function useCx(): { + cx: Cx; +}; diff --git a/packages/mui-styled-engine-sc/src/cx.js b/packages/mui-styled-engine-sc/src/cx.js new file mode 100644 index 00000000000000..3555304bb37e73 --- /dev/null +++ b/packages/mui-styled-engine-sc/src/cx.js @@ -0,0 +1,8 @@ +/* eslint-disable import/prefer-default-export */ +import { classnames } from './tools/classnames'; + +const cx = (...args) => classnames(args); + +export function useCx() { + return { cx }; +} diff --git a/packages/mui-styled-engine-sc/src/index.d.ts b/packages/mui-styled-engine-sc/src/index.d.ts index 83ddd5adb662c7..f3ed21e770044a 100644 --- a/packages/mui-styled-engine-sc/src/index.d.ts +++ b/packages/mui-styled-engine-sc/src/index.d.ts @@ -20,6 +20,7 @@ export { default as StyledEngineProvider } from './StyledEngineProvider'; export { default as GlobalStyles } from './GlobalStyles'; export * from './GlobalStyles'; +export * from './cx'; // These are the same as the ones in @mui/styled-engine // CSS.PropertiesFallback are necessary so that we support spreading of the mixins. For example: diff --git a/packages/mui-styled-engine-sc/src/index.js b/packages/mui-styled-engine-sc/src/index.js index a9613fe3b85335..b387a25233d17f 100644 --- a/packages/mui-styled-engine-sc/src/index.js +++ b/packages/mui-styled-engine-sc/src/index.js @@ -39,3 +39,4 @@ export default function styled(tag, options) { export { ThemeContext, keyframes, css } from 'styled-components'; export { default as StyledEngineProvider } from './StyledEngineProvider'; export { default as GlobalStyles } from './GlobalStyles'; +export * from './cx'; diff --git a/packages/mui-styled-engine-sc/src/tools/classnames.d.ts b/packages/mui-styled-engine-sc/src/tools/classnames.d.ts new file mode 100644 index 00000000000000..fb60757aeb0061 --- /dev/null +++ b/packages/mui-styled-engine-sc/src/tools/classnames.d.ts @@ -0,0 +1,13 @@ +export declare type CxArg = + | undefined + | null + | string + | boolean + | { + [className: string]: boolean | null | undefined; + } + | readonly CxArg[]; +/** Copy pasted from + * https://github.com/emotion-js/emotion/blob/23f43ab9f24d44219b0b007a00f4ac681fe8712e/packages/react/src/class-names.js#L17-L63 + * */ +export declare const classnames: (args: CxArg[]) => string; diff --git a/packages/mui-styled-engine-sc/src/tools/classnames.js b/packages/mui-styled-engine-sc/src/tools/classnames.js new file mode 100644 index 00000000000000..17f66eba1a3127 --- /dev/null +++ b/packages/mui-styled-engine-sc/src/tools/classnames.js @@ -0,0 +1,58 @@ +/* eslint-disable no-continue */ +/* eslint-disable no-plusplus */ +/* eslint-disable import/prefer-default-export */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +/* eslint-disable no-restricted-syntax */ + +/** Copy pasted from + * https://github.com/emotion-js/emotion/blob/23f43ab9f24d44219b0b007a00f4ac681fe8712e/packages/react/src/class-names.js#L17-L63 + * */ +export const classnames = (args) => { + const len = args.length; + let i = 0; + let cls = ''; + for (; i < len; i++) { + const arg = args[i]; + if (arg == null) { + continue; + } + + let toAdd; + switch (typeof arg) { + case 'boolean': + break; + case 'object': { + if (Array.isArray(arg)) { + toAdd = classnames(arg); + } else { + if ( + process.env.NODE_ENV !== 'production' && + arg.styles !== undefined && + arg.name !== undefined + ) { + console.error( + 'You have passed styles created with `css` from `@emotion/react` package to the `cx`.\n' + + '`cx` is meant to compose class names (strings) so you should convert those styles to a class name by passing them to the `css` received from component.', + ); + } + toAdd = ''; + for (const k in arg) { + if (arg[k] && k) { + toAdd && (toAdd += ' '); + toAdd += k; + } + } + } + break; + } + default: { + toAdd = arg; + } + } + if (toAdd) { + cls && (cls += ' '); + cls += toAdd; + } + } + return cls; +}; diff --git a/packages/mui-styled-engine/src/cx.d.ts b/packages/mui-styled-engine/src/cx.d.ts new file mode 100644 index 00000000000000..e3fa3d667d0680 --- /dev/null +++ b/packages/mui-styled-engine/src/cx.d.ts @@ -0,0 +1,8 @@ +import type { CxArg } from './tools/classnames'; + +export type { CxArg }; +export declare type Cx = (...classNames: CxArg[]) => string; + +export declare function useCx(): { + cx: Cx; +}; diff --git a/packages/mui-styled-engine/src/cx.js b/packages/mui-styled-engine/src/cx.js new file mode 100644 index 00000000000000..156cf1bc1c84f7 --- /dev/null +++ b/packages/mui-styled-engine/src/cx.js @@ -0,0 +1,149 @@ +/* eslint-disable import/prefer-default-export */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable import/first */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable no-labels */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable @typescript-eslint/no-shadow */ +import { useMemo } from 'react'; +import { serializeStyles } from '@emotion/serialize'; +import { insertStyles, getRegisteredStyles } from '@emotion/utils'; +import { __unsafe_useEmotionCache as useEmotionCache } from '@emotion/react'; +import createCache from '@emotion/cache'; +import { classnames } from './tools/classnames'; + +const { createCx } = (() => { + function merge(registered, css, className) { + const registeredStyles = []; + + const rawClassName = getRegisteredStyles(registered, registeredStyles, className); + + if (registeredStyles.length < 2) { + return className; + } + + return rawClassName + css(registeredStyles); + } + + function createCx(params) { + const { cache } = params; + + const css = (...args) => { + const serialized = serializeStyles(args, cache.registered); + insertStyles(cache, serialized, false); + const className = `${cache.key}-${serialized.name}`; + + scope: { + const arg = args[0]; + + if (!matchCSSObject(arg)) { + break scope; + } + + increaseSpecificityToTakePrecedenceOverMediaQueries.saveClassNameCSSObjectMapping( + cache, + className, + arg, + ); + } + + return className; + }; + + const cx = (...args) => { + const className = classnames(args); + + const feat27FixedClassnames = + increaseSpecificityToTakePrecedenceOverMediaQueries.fixClassName(cache, className, css); + + return merge(cache.registered, css, feat27FixedClassnames); + }; + + return { cx }; + } + + return { createCx }; +})(); + +/** Will pickup the contextual cache if any */ +export function useCx() { + const cache = useEmotionCache(); + + const { cx } = useMemo( + () => createCx({ cache: cache ?? createCache({ key: 'never' }) }), + [cache], + ); + + return { cx }; +} + +// https://github.com/garronej/tss-react/issues/27 +const increaseSpecificityToTakePrecedenceOverMediaQueries = (() => { + const cssObjectMapByCache = new WeakMap(); + + return { + saveClassNameCSSObjectMapping: (cache, className, cssObject) => { + let cssObjectMap = cssObjectMapByCache.get(cache); + + if (cssObjectMap === undefined) { + cssObjectMap = new Map(); + cssObjectMapByCache.set(cache, cssObjectMap); + } + + cssObjectMap.set(className, cssObject); + }, + fixClassName: (() => { + function fix(classNameCSSObjects) { + let isThereAnyMediaQueriesInPreviousClasses = false; + + return classNameCSSObjects.map(([className, cssObject]) => { + if (cssObject === undefined) { + return className; + } + + let out; + + if (!isThereAnyMediaQueriesInPreviousClasses) { + out = className; + + for (const key in cssObject) { + if (key.startsWith('@media')) { + isThereAnyMediaQueriesInPreviousClasses = true; + break; + } + } + } else { + out = { + '&&': cssObject, + }; + } + + return out; + }); + } + + return (cache, className, css) => { + const cssObjectMap = cssObjectMapByCache.get(cache); + + return classnames( + fix( + className.split(' ').map((className) => [className, cssObjectMap?.get(className)]), + ).map((classNameOrCSSObject) => + typeof classNameOrCSSObject === 'string' + ? classNameOrCSSObject + : css(classNameOrCSSObject), + ), + ); + }; + })(), + }; +})(); + +function matchCSSObject(arg) { + return ( + arg instanceof Object && + !('styles' in arg) && + !('length' in arg) && + !('__emotion_styles' in arg) + ); +} diff --git a/packages/mui-styled-engine/src/index.d.ts b/packages/mui-styled-engine/src/index.d.ts index 7c06edb0ca40de..2135bb49d3ab3d 100644 --- a/packages/mui-styled-engine/src/index.d.ts +++ b/packages/mui-styled-engine/src/index.d.ts @@ -10,6 +10,7 @@ export { default as StyledEngineProvider } from './StyledEngineProvider'; export { default as GlobalStyles } from './GlobalStyles'; export * from './GlobalStyles'; +export * from './cx'; export interface SerializedStyles { name: string; diff --git a/packages/mui-styled-engine/src/index.js b/packages/mui-styled-engine/src/index.js index 696af8f9b80d08..16baa7b83900f9 100644 --- a/packages/mui-styled-engine/src/index.js +++ b/packages/mui-styled-engine/src/index.js @@ -28,3 +28,4 @@ export default function styled(tag, options) { export { ThemeContext, keyframes, css } from '@emotion/react'; export { default as StyledEngineProvider } from './StyledEngineProvider'; export { default as GlobalStyles } from './GlobalStyles'; +export * from './cx'; diff --git a/packages/mui-styled-engine/src/tools/classnames.d.ts b/packages/mui-styled-engine/src/tools/classnames.d.ts new file mode 100644 index 00000000000000..fb60757aeb0061 --- /dev/null +++ b/packages/mui-styled-engine/src/tools/classnames.d.ts @@ -0,0 +1,13 @@ +export declare type CxArg = + | undefined + | null + | string + | boolean + | { + [className: string]: boolean | null | undefined; + } + | readonly CxArg[]; +/** Copy pasted from + * https://github.com/emotion-js/emotion/blob/23f43ab9f24d44219b0b007a00f4ac681fe8712e/packages/react/src/class-names.js#L17-L63 + * */ +export declare const classnames: (args: CxArg[]) => string; diff --git a/packages/mui-styled-engine/src/tools/classnames.js b/packages/mui-styled-engine/src/tools/classnames.js new file mode 100644 index 00000000000000..17f66eba1a3127 --- /dev/null +++ b/packages/mui-styled-engine/src/tools/classnames.js @@ -0,0 +1,58 @@ +/* eslint-disable no-continue */ +/* eslint-disable no-plusplus */ +/* eslint-disable import/prefer-default-export */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +/* eslint-disable no-restricted-syntax */ + +/** Copy pasted from + * https://github.com/emotion-js/emotion/blob/23f43ab9f24d44219b0b007a00f4ac681fe8712e/packages/react/src/class-names.js#L17-L63 + * */ +export const classnames = (args) => { + const len = args.length; + let i = 0; + let cls = ''; + for (; i < len; i++) { + const arg = args[i]; + if (arg == null) { + continue; + } + + let toAdd; + switch (typeof arg) { + case 'boolean': + break; + case 'object': { + if (Array.isArray(arg)) { + toAdd = classnames(arg); + } else { + if ( + process.env.NODE_ENV !== 'production' && + arg.styles !== undefined && + arg.name !== undefined + ) { + console.error( + 'You have passed styles created with `css` from `@emotion/react` package to the `cx`.\n' + + '`cx` is meant to compose class names (strings) so you should convert those styles to a class name by passing them to the `css` received from component.', + ); + } + toAdd = ''; + for (const k in arg) { + if (arg[k] && k) { + toAdd && (toAdd += ' '); + toAdd += k; + } + } + } + break; + } + default: { + toAdd = arg; + } + } + if (toAdd) { + cls && (cls += ' '); + cls += toAdd; + } + } + return cls; +}; diff --git a/test/regressions/fixtures/Cx/Cx.js b/test/regressions/fixtures/Cx/Cx.js new file mode 100644 index 00000000000000..c2e749799ffc68 --- /dev/null +++ b/test/regressions/fixtures/Cx/Cx.js @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { ClassNames } from '@emotion/react'; +import Button from '@mui/material/Button'; + +export default function Cx() { + return ( + + {({ css }) => ( + + + + + )} + + ); +} From bd80530c640d04fd11564d56955340c485763fea Mon Sep 17 00:00:00 2001 From: garronej Date: Sun, 8 May 2022 22:21:43 +0200 Subject: [PATCH 2/2] Implement suggestions of @mnajdova --- packages/mui-material/src/Button/Button.js | 3 +- .../mui-material/src/ButtonBase/ButtonBase.js | 2 +- packages/mui-styled-engine-sc/src/cx.d.ts | 4 +- packages/mui-styled-engine-sc/src/cx.js | 2 +- .../src/tools/classnames.d.ts | 5 + .../src/tools/classnames.js | 10 -- packages/mui-styled-engine/src/cx.d.ts | 4 +- packages/mui-styled-engine/src/cx.js | 97 +------------------ 8 files changed, 14 insertions(+), 113 deletions(-) diff --git a/packages/mui-material/src/Button/Button.js b/packages/mui-material/src/Button/Button.js index 471308d17b5f4c..eaea98d950e227 100644 --- a/packages/mui-material/src/Button/Button.js +++ b/packages/mui-material/src/Button/Button.js @@ -322,6 +322,7 @@ const Button = React.forwardRef(function Button(inProps, ref) { variant, }; + // eslint-disable-next-line @typescript-eslint/naming-convention const { root: classes_root, ...classes } = useUtilityClasses(ownerState); const startIcon = startIconProp && ( @@ -336,7 +337,7 @@ const Button = React.forwardRef(function Button(inProps, ref) { ); - const { cx } = useCx(); + const cx = useCx(); return ( string; -export declare function useCx(): { - cx: Cx; -}; +export declare function useCx(): Cx; diff --git a/packages/mui-styled-engine-sc/src/cx.js b/packages/mui-styled-engine-sc/src/cx.js index 3555304bb37e73..c83028f0173e8a 100644 --- a/packages/mui-styled-engine-sc/src/cx.js +++ b/packages/mui-styled-engine-sc/src/cx.js @@ -4,5 +4,5 @@ import { classnames } from './tools/classnames'; const cx = (...args) => classnames(args); export function useCx() { - return { cx }; + return cx; } diff --git a/packages/mui-styled-engine-sc/src/tools/classnames.d.ts b/packages/mui-styled-engine-sc/src/tools/classnames.d.ts index fb60757aeb0061..50390332ee8345 100644 --- a/packages/mui-styled-engine-sc/src/tools/classnames.d.ts +++ b/packages/mui-styled-engine-sc/src/tools/classnames.d.ts @@ -1,3 +1,8 @@ + +/** Copy pasted from + * https://github.com/emotion-js/emotion/blob/23f43ab9f24d44219b0b007a00f4ac681fe8712e/packages/react/src/class-names.js#L9-L15 + * (we use void instead of undefined because calling cx with no argument shouldn't be correct typewise) + * */ export declare type CxArg = | undefined | null diff --git a/packages/mui-styled-engine-sc/src/tools/classnames.js b/packages/mui-styled-engine-sc/src/tools/classnames.js index 17f66eba1a3127..5cdc0e54a2d809 100644 --- a/packages/mui-styled-engine-sc/src/tools/classnames.js +++ b/packages/mui-styled-engine-sc/src/tools/classnames.js @@ -25,16 +25,6 @@ export const classnames = (args) => { if (Array.isArray(arg)) { toAdd = classnames(arg); } else { - if ( - process.env.NODE_ENV !== 'production' && - arg.styles !== undefined && - arg.name !== undefined - ) { - console.error( - 'You have passed styles created with `css` from `@emotion/react` package to the `cx`.\n' + - '`cx` is meant to compose class names (strings) so you should convert those styles to a class name by passing them to the `css` received from component.', - ); - } toAdd = ''; for (const k in arg) { if (arg[k] && k) { diff --git a/packages/mui-styled-engine/src/cx.d.ts b/packages/mui-styled-engine/src/cx.d.ts index e3fa3d667d0680..c6b25a15c7e8d9 100644 --- a/packages/mui-styled-engine/src/cx.d.ts +++ b/packages/mui-styled-engine/src/cx.d.ts @@ -3,6 +3,4 @@ import type { CxArg } from './tools/classnames'; export type { CxArg }; export declare type Cx = (...classNames: CxArg[]) => string; -export declare function useCx(): { - cx: Cx; -}; +export declare function useCx(): Cx; diff --git a/packages/mui-styled-engine/src/cx.js b/packages/mui-styled-engine/src/cx.js index 156cf1bc1c84f7..53b8a55997d8f1 100644 --- a/packages/mui-styled-engine/src/cx.js +++ b/packages/mui-styled-engine/src/cx.js @@ -33,31 +33,11 @@ const { createCx } = (() => { insertStyles(cache, serialized, false); const className = `${cache.key}-${serialized.name}`; - scope: { - const arg = args[0]; - - if (!matchCSSObject(arg)) { - break scope; - } - - increaseSpecificityToTakePrecedenceOverMediaQueries.saveClassNameCSSObjectMapping( - cache, - className, - arg, - ); - } - return className; }; - const cx = (...args) => { - const className = classnames(args); - - const feat27FixedClassnames = - increaseSpecificityToTakePrecedenceOverMediaQueries.fixClassName(cache, className, css); - - return merge(cache.registered, css, feat27FixedClassnames); - }; + const cx = (...args) => + merge(cache.registered, css, classnames(args)); return { cx }; } @@ -74,76 +54,5 @@ export function useCx() { [cache], ); - return { cx }; -} - -// https://github.com/garronej/tss-react/issues/27 -const increaseSpecificityToTakePrecedenceOverMediaQueries = (() => { - const cssObjectMapByCache = new WeakMap(); - - return { - saveClassNameCSSObjectMapping: (cache, className, cssObject) => { - let cssObjectMap = cssObjectMapByCache.get(cache); - - if (cssObjectMap === undefined) { - cssObjectMap = new Map(); - cssObjectMapByCache.set(cache, cssObjectMap); - } - - cssObjectMap.set(className, cssObject); - }, - fixClassName: (() => { - function fix(classNameCSSObjects) { - let isThereAnyMediaQueriesInPreviousClasses = false; - - return classNameCSSObjects.map(([className, cssObject]) => { - if (cssObject === undefined) { - return className; - } - - let out; - - if (!isThereAnyMediaQueriesInPreviousClasses) { - out = className; - - for (const key in cssObject) { - if (key.startsWith('@media')) { - isThereAnyMediaQueriesInPreviousClasses = true; - break; - } - } - } else { - out = { - '&&': cssObject, - }; - } - - return out; - }); - } - - return (cache, className, css) => { - const cssObjectMap = cssObjectMapByCache.get(cache); - - return classnames( - fix( - className.split(' ').map((className) => [className, cssObjectMap?.get(className)]), - ).map((classNameOrCSSObject) => - typeof classNameOrCSSObject === 'string' - ? classNameOrCSSObject - : css(classNameOrCSSObject), - ), - ); - }; - })(), - }; -})(); - -function matchCSSObject(arg) { - return ( - arg instanceof Object && - !('styles' in arg) && - !('length' in arg) && - !('__emotion_styles' in arg) - ); + return cx; }