Skip to content

Commit

Permalink
components: Add useCx (#33172)
Browse files Browse the repository at this point in the history
* components: Add useCx

* Add tests

* Add JSDoc

* Add story for useCx
  • Loading branch information
sarayourfriend committed Jul 8, 2021
1 parent dacdf41 commit ad0cc77
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 0 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@emotion/css": "^11.1.3",
"@emotion/react": "^11.1.5",
"@emotion/styled": "^11.3.0",
"@emotion/utils": "1.0.0",
"@wordpress/a11y": "file:../a11y",
"@wordpress/compose": "file:../compose",
"@wordpress/date": "file:../date",
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/utils/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { default as useControlledState } from './use-controlled-state';
export { default as useJumpStep } from './use-jump-step';
export { default as useUpdateEffect } from './use-update-effect';
export { useControlledValue } from './use-controlled-value';
export { useCx } from './use-cx';
79 changes: 79 additions & 0 deletions packages/components/src/utils/hooks/stories/use-cx.js
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>
);
};
64 changes: 64 additions & 0 deletions packages/components/src/utils/hooks/test/use-cx.js
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
);
} );
} );
65 changes: 65 additions & 0 deletions packages/components/src/utils/hooks/use-cx.ts
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 }`;
}
return arg;
} )
);
},
[ cache ]
);

return cx;
};

0 comments on commit ad0cc77

Please sign in to comment.