Skip to content
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

Merged
merged 4 commits into from
Jul 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

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 🤞

Copy link
Contributor Author

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


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 }`;
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing that iframes are working with Emotion

This is actually a good point. Should we also add a unit test and/or storybook story to showcase iframe compatibility?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
};