-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
components: Add useCx #33172
components: Add useCx #33172
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import { useCx } from '..'; | ||
import StyleProvider from '../../../style-provider'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useState, createPortal } from '@wordpress/element'; | ||
/** | ||
* External dependencies | ||
*/ | ||
import { css } from '@emotion/react'; | ||
|
||
export default { | ||
title: 'Components (Experimental)/useCx', | ||
}; | ||
|
||
const IFrame = ( { children } ) => { | ||
const [ iframeDocument, setIframeDocument ] = useState(); | ||
|
||
const handleRef = ( node ) => { | ||
if ( ! node ) { | ||
return null; | ||
} | ||
|
||
function setIfReady() { | ||
const { contentDocument } = node; | ||
const { readyState } = contentDocument; | ||
|
||
if ( readyState !== 'interactive' && readyState !== 'complete' ) { | ||
return false; | ||
} | ||
|
||
setIframeDocument( contentDocument ); | ||
} | ||
|
||
if ( setIfReady() ) { | ||
return; | ||
} | ||
|
||
node.addEventListener( 'load', () => { | ||
// iframe isn't immediately ready in Firefox | ||
setIfReady(); | ||
} ); | ||
}; | ||
|
||
return ( | ||
<iframe ref={ handleRef } title="use-cx-test-frame"> | ||
{ iframeDocument && | ||
createPortal( | ||
<StyleProvider document={ iframeDocument }> | ||
{ children } | ||
</StyleProvider>, | ||
iframeDocument.body | ||
) } | ||
</iframe> | ||
); | ||
}; | ||
|
||
const Example = ( { args, children } ) => { | ||
const cx = useCx(); | ||
const classes = cx( ...args ); | ||
return <span className={ classes }>{ children }</span>; | ||
}; | ||
|
||
export const _default = () => { | ||
const redText = css` | ||
color: red; | ||
`; | ||
return ( | ||
<IFrame> | ||
<Example args={ [ redText ] }> | ||
This text is inside an iframe and is red! | ||
</Example> | ||
</IFrame> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
// eslint-disable-next-line no-restricted-imports | ||
import { cx as innerCx } from '@emotion/css'; | ||
import { insertStyles } from '@emotion/utils'; | ||
import { render } from '@testing-library/react'; | ||
import { css, CacheProvider } from '@emotion/react'; | ||
import createCache from '@emotion/cache'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { useCx } from '..'; | ||
|
||
jest.mock( '@emotion/css', () => ( { | ||
cx: jest.fn(), | ||
} ) ); | ||
|
||
jest.mock( '@emotion/utils', () => ( { | ||
insertStyles: jest.fn(), | ||
} ) ); | ||
|
||
function Example( { args } ) { | ||
const cx = useCx(); | ||
|
||
return <div className={ cx( ...args ) } />; | ||
} | ||
|
||
describe( 'useCx', () => { | ||
it( 'should call cx with the built style name and pass serialized styles to insertStyles', () => { | ||
const serializedStyle = css` | ||
color: red; | ||
`; | ||
const className = 'component-example'; | ||
const object = { | ||
'component-example-focused': true, | ||
}; | ||
|
||
const key = 'test-cache-key'; | ||
|
||
const container = document.createElement( 'head' ); | ||
|
||
const cache = createCache( { container, key } ); | ||
|
||
render( | ||
<CacheProvider value={ cache }> | ||
<Example args={ [ className, serializedStyle, object ] } /> | ||
</CacheProvider> | ||
); | ||
|
||
expect( innerCx ).toHaveBeenCalledWith( | ||
className, | ||
`${ key }-${ serializedStyle.name }`, | ||
object | ||
); | ||
|
||
expect( insertStyles ).toHaveBeenCalledWith( | ||
cache, | ||
serializedStyle, | ||
false | ||
); | ||
} ); | ||
} ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
// eslint-disable-next-line no-restricted-imports | ||
import type { Context } from 'react'; | ||
import { CacheProvider, EmotionCache } from '@emotion/react'; | ||
import type { SerializedStyles } from '@emotion/serialize'; | ||
import { insertStyles } from '@emotion/utils'; | ||
// eslint-disable-next-line no-restricted-imports | ||
import { cx as innerCx, ClassNamesArg } from '@emotion/css'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useContext, useCallback } from '@wordpress/element'; | ||
|
||
// @ts-ignore Private property | ||
const EmotionCacheContext: Context< EmotionCache > = CacheProvider._context; | ||
|
||
const useEmotionCacheContext = () => useContext( EmotionCacheContext ); | ||
|
||
const isSerializedStyles = ( o: any ): o is SerializedStyles => | ||
[ 'name', 'styles' ].every( ( p ) => typeof o[ p ] !== 'undefined' ); | ||
|
||
/** | ||
* Retrieve a `cx` function that knows how to handle `SerializedStyles` | ||
* returned by the `@emotion/react` `css` function in addition to what | ||
* `cx` normally knows how to handle. It also hooks into the Emotion | ||
* Cache, allowing `css` calls to work inside iframes. | ||
* | ||
* @example | ||
* import { css } from '@emotion/react'; | ||
* | ||
* const styles = css` | ||
* color: red | ||
* `; | ||
* | ||
* function RedText( { className, ...props } ) { | ||
* const cx = useCx(); | ||
* | ||
* const classes = cx(styles, className); | ||
* | ||
* return <span className={classes} {...props} />; | ||
* } | ||
*/ | ||
export const useCx = () => { | ||
const cache = useEmotionCacheContext(); | ||
|
||
const cx = useCallback( | ||
( ...classNames: ( ClassNamesArg | SerializedStyles )[] ) => { | ||
return innerCx( | ||
...classNames.map( ( arg ) => { | ||
if ( isSerializedStyles( arg ) ) { | ||
insertStyles( cache, arg, false ); | ||
return `${ cache.key }-${ arg.name }`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any chance that this interpolation technique may change in emotion and potentially break this hook? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I doubt it, but we'd notice it quickly I would think. Testing that iframes are working with Emotion should be something we do every Emotion upgrade. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is actually a good point. Should we also add a unit test and/or storybook story to showcase iframe compatibility? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah, that'd be good to do, I'll add it. |
||
} | ||
return arg; | ||
} ) | ||
); | ||
}, | ||
[ cache ] | ||
); | ||
|
||
return cx; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may not need to access a private property if emotion-js/emotion#2418 gets merged 🤞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. Unfortunately it seems that my PR isn't gaining any traction.
Looking at the React internals, they seem to use this property a lot, so I would be surprised if it went away completely in the future: https://github.com/facebook/react/search?q=._context