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

Automatically add data-cy and displayName classNames #40

Merged
merged 3 commits into from
Apr 8, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .storybook/.babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"presets": ["@helpscout/zero/babel"]
"presets": ["@helpscout/zero/babel"],
"plugins": ["emotion"]
}
2,389 changes: 1,152 additions & 1,237 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,14 @@
"stylis-rule-sheet": "0.0.10"
},
"devDependencies": {
"@helpscout/hsds-react": "^2.16.1",
"babel-plugin-emotion": "^9.2.11",
"@helpscout/hsds-react": "^2.5.6",
"@helpscout/zero": "3.0.2",
"@storybook/addon-actions": "^4",
"@storybook/addon-links": "^4",
"@storybook/addons": "^4",
"@storybook/cli": "^4",
"@storybook/react": "^4",
"@storybook/addon-actions": "4.1.6",
"@storybook/addon-links": "4.1.6",
"@storybook/addons": "4.1.6",
"@storybook/cli": "4.1.6",
"@storybook/react": "4.1.6",
"coveralls": "3.0.0",
"csstype": "2.5.7",
"enzyme": "2.9.1",
Expand Down
39 changes: 29 additions & 10 deletions src/create-emotion-styled/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import typeof ReactType from 'react'
import type { CreateStyled, StyledOptions } from './utils'
import hoistNonReactStatics from '@helpscout/react-utils/dist/hoistNonReactStatics'
import {
createDataCy,
createHashedDisplayClassName,
themeChannel as channel,
testPickPropsOnComponent,
testAlwaysTrue,
Expand Down Expand Up @@ -94,6 +96,11 @@ function createEmotionStyled(
}
}

const displayName =
typeof baseTag === 'string'
? baseTag
: baseTag.displayName || baseTag.name || 'Component'

const OuterBaseComponent = pure ? view.PureComponent : view.Component

class Styled extends OuterBaseComponent<*, { theme: Object }> {
Expand Down Expand Up @@ -181,14 +188,17 @@ function createEmotionStyled(

let className = ''
let classInterpolations = []
let hashedClassName
let hashedDisplayClassName
this.setEmotion()

if (props.className) {
if (staticClassName === undefined) {
className += this.emotion.getRegisteredStyles(
hashedClassName = this.emotion.getRegisteredStyles(
classInterpolations,
props.className,
)
className += hashedClassName
} else {
className += `${props.className} `
}
Expand All @@ -199,14 +209,28 @@ function createEmotionStyled(
this.emotion.hasOwnProperty('cssWithScope') &&
typeof this.emotion.cssWithScope === 'function'
) {
className += this.emotion
hashedClassName = this.emotion
.cssWithScope(this.getScope())
.apply(this, styles.concat(classInterpolations))

hashedDisplayClassName = createHashedDisplayClassName(
hashedClassName,
baseTag,
)

className += hashedClassName
className += hashedDisplayClassName
} else {
className += this.emotion.css.apply(
hashedClassName = this.emotion.css.apply(
this,
styles.concat(classInterpolations),
)
hashedDisplayClassName = createHashedDisplayClassName(
hashedClassName,
baseTag,
)
className += hashedClassName
className += hashedDisplayClassName
}
} else {
className += staticClassName
Expand All @@ -221,19 +245,14 @@ function createEmotionStyled(
// $FlowFixMe
pickAssign(shouldForwardProp, {}, props, {
className,
'data-cy': createDataCy(props, baseTag),
ref: props.innerRef,
}),
)
}
}
Styled.displayName =
identifierName !== undefined
? identifierName
: `Styled(${
typeof baseTag === 'string'
? baseTag
: baseTag.displayName || baseTag.name || 'Component'
})`
identifierName !== undefined ? identifierName : `Styled(${displayName})`

if (tag.defaultProps !== undefined) {
// $FlowFixMe
Expand Down
28 changes: 25 additions & 3 deletions src/create-emotion-styled/utils.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
// @flow
import isPropValid from '@helpscout/react-utils/dist/isPropValid'
import type {Interpolations} from 'create-emotion'
import type { Interpolations } from 'create-emotion'

export const themeChannel = '__EMOTION_THEMING__'

export const getDisplayName = Component =>
Component.displayName || Component.name

export const createDataCy = (props, Component) => {
if (props['data-cy']) {
return props['data-cy']
}

return getDisplayName(Component)
}

export const createHashedDisplayClassName = (className, Component) => {
const displayName =
typeof Component === 'string' ? Component : getDisplayName(Component)

if (!displayName) {
return ''
} else {
return ` ${className}-${displayName}`
}
}

/**
* Sets the Frame (document) supplied by the FrameProvider
* @param {*} frame
*/
export function setFrame(frame: Object) {
this.setState({frame})
this.setState({ frame })
}

export function setTheme(theme: Object) {
this.setState({theme})
this.setState({ theme })
}

export const testPickPropsOnStringTag = isPropValid
Expand Down
12 changes: 7 additions & 5 deletions src/create-emotion/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import {
isBrowser,
} from './utils'
import StyleSheet from './sheet'
import type {PrefixOption, ClassNameArg} from './utils'
import type { PrefixOption, ClassNameArg } from './utils'

type StylisPlugins = Function[] | null | Function

type EmotionCaches = {|
registered: {[key: string]: string},
inserted: {[key: string]: string | true},
registered: { [key: string]: string },
inserted: { [key: string]: string | true },
nonce?: string,
key: string,
|}
Expand Down Expand Up @@ -63,7 +63,7 @@ type EmotionOptions = {
}

function createEmotion(
context: {__SECRET_FANCY_EMOTION__?: Emotion},
context: { __SECRET_FANCY_EMOTION__?: Emotion },

Choose a reason for hiding this comment

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

What would a "Secret Fancy Emotion" be? "Elated", "Discombobulated"? Don't mind me it's Friday

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hahaha... it's a secret ;)

For context... By default, Emotion generates a global variable called __SECRET_EMOTION__. We had an issue where a Fancy powered app (Beacon2) rendered into a user environment that happened to use Emotion as well.

Because of this... our modified version of Emotion broke their Emotion 😅

So! To prevent this from happening, we created our own __SECRET_FANCY_EMOTION__ namespace :)

options?: EmotionOptions,
): Emotion {
if (context.__SECRET_FANCY_EMOTION__ !== undefined) {
Expand Down Expand Up @@ -167,7 +167,9 @@ function createEmotion(

const objectToStringCache = new WeakMap()

function createStringFromObject(obj: {[key: string]: Interpolation}): string {
function createStringFromObject(obj: {
[key: string]: Interpolation,
}): string {
if (objectToStringCache.has(obj)) {
// $FlowFixMe
return objectToStringCache.get(obj)
Expand Down
62 changes: 58 additions & 4 deletions src/styled/__tests__/styled.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,60 @@ describe('styled', () => {
expect(el.classList.toString()).toContain('css-')
expect(el.classList.toString()).toContain('custom')
})

test('Autogenerates a hashed className with component name', () => {
const SomeBase = props => <span {...props} />
SomeBase.displayName = 'SomeBase'

const Compo = styled(SomeBase)(`
display: block;
`)
const wrapper = mount(<Compo className="custom" />)
const el = wrapper.find('span').getNode()

expect(el.classList.toString()).toContain('css-')
expect(el.classList.toString()).toContain('custom')
expect(el.classList.toString()).toContain('SomeBase')
})
})

describe('data attribute', () => {
test('Autogenerates a data-cy attribute, if applicable', () => {
const BaseCompo = props => <span {...props} />
BaseCompo.displayName = 'Compo'
const Compo = styled(BaseCompo)(`
display: block;
`)

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

expect(el.prop('data-cy')).toBe('Compo')
})

test('Does not add data-cy if creating a baseTag', () => {
const Compo = styled('span')(`
display: block;
`)

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

expect(el.prop('data-cy')).toBeFalsy()
})

test('Attempts to use name if displayName is not available', () => {
const BaseCompo = props => <span {...props} />
BaseCompo.displayName = undefined
const Compo = styled(BaseCompo)(`
display: block;
`)

const wrapper = mount(<Compo className="test a b c" />)
const el = wrapper.find('span')

expect(el.prop('data-cy')).toBe('BaseCompo')
})
})

describe('Statics', () => {
Expand Down Expand Up @@ -316,10 +370,10 @@ describe('styled', () => {
//
// First, check to see if the className is extended, instead of added on.
//
expect(baseNode.classList.length).toBe(2)
expect(cardNode.classList.length).toBe(2)
expect(superNode.classList.length).toBe(2)
expect(fancyNode.classList.length).toBe(2)
expect(baseNode.classList.length).toBe(3)
expect(cardNode.classList.length).toBe(3)
expect(superNode.classList.length).toBe(3)
expect(fancyNode.classList.length).toBe(4)

//
// Second, check to see if the extended classNames are correct.
Expand Down
23 changes: 3 additions & 20 deletions stories/HOC.stories.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react'
import {storiesOf} from '@storybook/react'
import { storiesOf } from '@storybook/react'
import propConnect from '@helpscout/hsds-react/components/PropProvider/propConnect'
import {namespaceComponent} from '@helpscout/hsds-react/utilities/component'
import styled, {ScopeProvider} from '../src'
import { namespaceComponent } from '@helpscout/hsds-react/utilities/component'
import styled, { ScopeProvider } from '../src'
import Card from './CardCo'

const stories = storiesOf('HOC', module)
Expand All @@ -17,23 +17,6 @@ const Thing = styled('div')`
`

class Test extends React.Component {
componentDidMount() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No longer relevant

const cardNode = document.querySelector('.card')
const styledCardNode = document.querySelector('.styledCard')

const a = cardNode.classList.length
const l = styledCardNode.classList.length

this.node.innerHTML = `
✅ PASS: cardNode: ${a};<br /><br />
${l === a ? '✅ PASS' : '🔥 FAIL'}: styledCardNode: ${l};<br /><br />
<hr />
${cardNode.classList.toString()}
<hr />
${styledCardNode.classList.toString()}
<hr />
`
}
render() {
return (
<ScopeProvider scope="#APP">
Expand Down