Skip to content

Commit

Permalink
Emotion: Custom instance to avoid global conflict
Browse files Browse the repository at this point in the history
This update adjusts the (custom) Emotion namespace to avoid collision with
potential stock instances of Emotion. Tests have been added for this case.

A `emotion.cssWithScope` guard + fallback is added to `create-emotion-styled`
that defaults to `emotion.css` if that custom function is not available.
  • Loading branch information
ItsJonQ committed Jul 25, 2018
1 parent 51eb988 commit 221e826
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 28 deletions.
50 changes: 31 additions & 19 deletions src/create-emotion-styled/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// @flow
import PropTypes from 'prop-types'
import type { ElementType } from 'react'
import type {ElementType} from 'react'
import typeof ReactType from 'react'
import type { CreateStyled, StyledOptions } from './utils'
import type {CreateStyled, StyledOptions} from './utils'
import {
themeChannel as channel,
testPickPropsOnComponent,
Expand All @@ -13,9 +13,9 @@ import {
setFrame,
} from './utils'
import FrameManager from './FrameManager'
import { getDocumentFromReactComponent } from '../utils'
import { channel as frameChannel } from '../FrameProvider'
import { channel as scopeChannel } from '../ScopeProvider'
import {getDocumentFromReactComponent} from '../utils'
import {channel as frameChannel} from '../FrameProvider'
import {channel as scopeChannel} from '../ScopeProvider'

const contextTypes = {
[channel]: PropTypes.object,
Expand All @@ -28,7 +28,7 @@ function createEmotionStyled(emotion: Object, view: ReactType) {
if (process.env.NODE_ENV !== 'production') {
if (tag === undefined) {
throw new Error(
'You are trying to create a styled element with an undefined component.\nYou may have forgotten to import it.'
'You are trying to create a styled element with an undefined component.\nYou may have forgotten to import it.',
)
}
}
Expand Down Expand Up @@ -84,7 +84,7 @@ function createEmotionStyled(emotion: Object, view: ReactType) {
}
}

class Styled extends view.Component<*, { theme: Object }> {
class Styled extends view.Component<*, {theme: Object}> {
unsubscribe: number
unsubscribeFrame: number
mergedProps: Object
Expand All @@ -95,6 +95,7 @@ function createEmotionStyled(emotion: Object, view: ReactType) {
static __emotion_target: string
static __emotion_forwardProp: void | (string => boolean)
static withComponent: (ElementType, options?: StyledOptions) => any
// $FlowFixMe
state = {}
// Custom instance properties
emotion = emotion
Expand All @@ -103,15 +104,15 @@ function createEmotionStyled(emotion: Object, view: ReactType) {
componentWillMount() {
if (this.context[channel] !== undefined) {
this.unsubscribe = this.context[channel].subscribe(
setTheme.bind(this)
setTheme.bind(this),
)
}
/**
* Extra channel for the Frame
*/
if (this.context[frameChannel] !== undefined) {
this.unsubscribeFrame = this.context[frameChannel].subscribe(
setFrame.bind(this)
setFrame.bind(this),
)
}
}
Expand All @@ -135,6 +136,7 @@ function createEmotionStyled(emotion: Object, view: ReactType) {
* custom container.
*/
setEmotion() {
// $FlowFixMe
const frame = this.state.frame

if (!frame) return
Expand Down Expand Up @@ -166,7 +168,7 @@ function createEmotionStyled(emotion: Object, view: ReactType) {
}
}
render() {
const { props, state } = this
const {props, state} = this
this.mergedProps = pickAssign(testAlwaysTrue, {}, props, {
theme: (state !== null && state.theme) || props.theme || {},
})
Expand All @@ -179,17 +181,27 @@ function createEmotionStyled(emotion: Object, view: ReactType) {
if (staticClassName === undefined) {
className += this.emotion.getRegisteredStyles(
classInterpolations,
props.className
props.className,
)
} else {
className += `${props.className} `
}
}
if (staticClassName === undefined) {
className += this.emotion
/* Replaces emotion.css, with enhanced emotion.cssWithScope */
.cssWithScope(this.getScope())
.apply(this, styles.concat(classInterpolations))
/* Replaces emotion.css, with enhanced emotion.cssWithScope */
if (
this.emotion.hasOwnProperty('cssWithScope') &&
typeof this.emotion.cssWithScope === 'function'
) {
className += this.emotion
.cssWithScope(this.getScope())
.apply(this, styles.concat(classInterpolations))
} else {
className += this.emotion.css.apply(
this,
styles.concat(classInterpolations),
)
}
} else {
className += staticClassName
}
Expand All @@ -204,7 +216,7 @@ function createEmotionStyled(emotion: Object, view: ReactType) {
pickAssign(shouldForwardProp, {}, props, {
className,
ref: props.innerRef,
})
}),
)
}
}
Expand Down Expand Up @@ -242,14 +254,14 @@ function createEmotionStyled(emotion: Object, view: ReactType) {

Styled.withComponent = (
nextTag: ElementType,
nextOptions?: StyledOptions
nextOptions?: StyledOptions,
) => {
return createStyled(
nextTag,
nextOptions !== undefined
? // $FlowFixMe
pickAssign(testAlwaysTrue, {}, options, nextOptions)
: options
: options,
)(...styles)
}

Expand All @@ -270,7 +282,7 @@ function createEmotionStyled(emotion: Object, view: ReactType) {
default: {
throw new Error(
`You're trying to use the styled shorthand without babel-plugin-this.` +
`\nPlease install and setup babel-plugin-emotion or use the function call syntax(\`styled('${property}')\` instead of \`styled.${property}\`)`
`\nPlease install and setup babel-plugin-emotion or use the function call syntax(\`styled('${property}')\` instead of \`styled.${property}\`)`,
)
}
}
Expand Down
24 changes: 24 additions & 0 deletions src/create-emotion/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import createEmotion from '../index'

describe('createEmotion', () => {
afterEach(() => {
global.__SECRET_EMOTION__ = undefined
global.__SECRET_FANCY_EMOTION__ = undefined
})

test('Creats unique global instance of Emotion', () => {
const inst = createEmotion(global)

expect(global.__SECRET_FANCY_EMOTION__).toBe(inst)
})

test('Does not collide with stock instance of Emotion', () => {
const mockEmotion = {}
global.__SECRET_EMOTION__ = mockEmotion

const inst = createEmotion(global)

expect(global.__SECRET_EMOTION__).toBe(mockEmotion)
expect(global.__SECRET_FANCY_EMOTION__).toBe(inst)
})
})
9 changes: 5 additions & 4 deletions src/create-emotion/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type CreateStyles<ReturnValue> = (...args: Interpolations) => ReturnValue

export type Emotion = {
css: CreateStyles<string>,
cssWithScope: Function,
cx: (...classNames: Array<ClassNameArg>) => string,
flush: () => void,
getRegisteredStyles: (
Expand All @@ -62,11 +63,11 @@ type EmotionOptions = {
}

function createEmotion(
context: {__SECRET_EMOTION__?: Emotion},
context: {__SECRET_FANCY_EMOTION__?: Emotion},
options?: EmotionOptions,
): Emotion {
if (context.__SECRET_EMOTION__ !== undefined) {
return context.__SECRET_EMOTION__
if (context.__SECRET_FANCY_EMOTION__ !== undefined) {
return context.__SECRET_FANCY_EMOTION__
}
if (options === undefined) options = {}
let key = options.key || 'css'
Expand Down Expand Up @@ -399,7 +400,7 @@ function createEmotion(
sheet,
caches,
}
context.__SECRET_EMOTION__ = emotion
context.__SECRET_FANCY_EMOTION__ = emotion
return emotion
}

Expand Down
24 changes: 21 additions & 3 deletions src/styled/__tests__/styled.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { mount } from 'enzyme'
import {mount} from 'enzyme'
import styled from '../index'
import { getStyleProp, resetStyleTags } from '../../utils/testHelpers'
import {getStyleProp, resetStyleTags} from '../../utils/testHelpers'

describe('styled', () => {
afterEach(() => {
Expand Down Expand Up @@ -82,10 +82,28 @@ describe('styled', () => {
expect(getStyleProp(el, 'background')).toBe('yellow')
expect(getStyleProp(el, 'color')).not.toBe('red')

wrapper.setProps({ title: 'Clever' })
wrapper.setProps({title: 'Clever'})

expect(getStyleProp(el, 'background')).toBe('yellow')
expect(getStyleProp(el, 'color')).toBe('red')
})

test('Falls back to emotion.css if emotion.cssWithScope is unavailable', () => {
const spy = jest.fn()
const Compo = styled('span')`
background: yellow;
${props => props.title && 'color: red;'};
`

const wrapper = mount(<Compo />)
const el = wrapper.find('span').getNode()

wrapper.instance().emotion.cssWithScope = undefined
wrapper.instance().emotion.css = spy

wrapper.setProps({title: 'Clever'})

expect(spy).toHaveBeenCalled()
})
})
})
4 changes: 2 additions & 2 deletions src/utils/testHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export const getStyleProp = (node: HTMLElement, prop: string = 'display') =>
* Resets the <head> tag to remove stray <style> tags.
*/
export const resetStyleTags = () => {
if (global.__SECRET_EMOTION__) {
global.__SECRET_EMOTION__.flush()
if (global.__SECRET_FANCY_EMOTION__) {
global.__SECRET_FANCY_EMOTION__.flush()
} else {
global.document.head.innerHTML = ''
}
Expand Down

0 comments on commit 221e826

Please sign in to comment.