Skip to content

Refactor StateLabel 🚀 #311

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

Merged
merged 28 commits into from
Oct 12, 2018
Merged
Show file tree
Hide file tree
Changes from 24 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
5 changes: 2 additions & 3 deletions pages/components/docs/StateLabel.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Use StateLabel components to show the status of an issue or pull request.
## Default example

```.jsx
<StateLabel state="open">Open</StateLabel>
<StateLabel scheme="issueOpened">Open</StateLabel>
```

## System props
Expand All @@ -16,8 +16,7 @@ StateLabel components get `COMMON` system props. Read our [System Props](/compon

| Name | Type | Default | Description |
| :- | :- | :-: | :- |
| icon | Node or Boolean | | Provide a component for the Icon or set to `true` to match the icon to the `state` prop |
| small | Boolean | | Used to create a smaller version of the default StateLabel |
| state | String | | Can be one of `open`, `reopened`, `closed`, or `merged`. Used to set background color and Octicon.
| scheme | String | | Can be one of `issueOpened`, `issueClosed`, `pullOpened`, `pullClosed` or `pullMerged`.

export const meta = {displayName: 'StateLabel'}
68 changes: 40 additions & 28 deletions src/Label.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,50 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import sass from 'sass.macro'
import {injectGlobal} from 'emotion'
import {color} from 'styled-system'
import styled from 'react-emotion'
import {withSystemProps} from './system-props'
import theme, {colors} from './theme'

injectGlobal(sass`
@import "primer-support/index.scss";
@import "primer-labels/lib/labels.scss";
`)
const sizeMap = {
small: `padding 0.125em ${theme.space[1]}px; font-size: ${theme.fontSizes[0]}px;`,
medium: `padding: 3px ${theme.space[1]}px; font-size: ${theme.fontSizes[0]}px;`,
large: `padding: 4px ${theme.space[2]}px; ${theme.fontSizes[1]}px;`,
xl: `padding: ${theme.space[1]}px; ${theme.space[2]}px; ${theme.fontSizes[2]}px;`
}

const outlineStyles = `
margin-top: -1px; // offsets the 1px border
margin-bottom: -1px; // offsets the 1px border
font-weight: 400;
color: ${colors.gray[6]};
background-color: transparent;
border: ${theme.borders[1]} ${colors.blackfade15};
box-shadow: none;
`

const colorScheme = (scheme, outline) => {
if (outline) {
return {
'Label--outline-green': scheme === 'green'
}
} else {
return {
'Label--gray': scheme == null || scheme === 'gray',
'Label--gray-darker': scheme === 'gray-darker',
'Label--orange': scheme === 'orange',
'bg-green': scheme === 'green'
}
const styledLabel = styled('span')`
display: inline-block;
font-weight: 600;
line-height: ${theme.lineHeights.condensedUltra};
color: ${colors.white};
border-radius: 2px;
&:hover {
text-decoration: none;
}
}
${color} //eslint-disable-line
Copy link
Author

Choose a reason for hiding this comment

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

I had to put this here in order to get eslint to shut up about this being on it's own line 😭

Copy link
Contributor

Choose a reason for hiding this comment

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

That's crazy. What if you put a semicolon at the end?

${props => (props.dropshadow ? 'box-shadow: inset 0 -1px 0 rgba(27, 31, 35, 0.12)' : '')};
${props => sizeMap[props.size]};
${props => (props.outline ? outlineStyles : '')}; // must be last to override other values
`

function Label({className, outline, scheme, ...rest}) {
const classes = classnames(className, 'Label', outline && 'Label--outline', colorScheme(scheme, outline))
return <span className={classes} {...rest} />
styledLabel.defaultProps = {
theme,
bg: 'gray.5',
size: 'medium'
}

Label.propTypes = {
outline: PropTypes.bool,
scheme: PropTypes.oneOf(['gray', 'gray-darker', 'green', 'orange'])
styledLabel.propTypes = {
dropshadow: PropTypes.bool,
outline: PropTypes.bool
}

export default withSystemProps(Label, ['space'])
export default withSystemProps(styledLabel, ['space'])
105 changes: 39 additions & 66 deletions src/StateLabel.js
Original file line number Diff line number Diff line change
@@ -1,83 +1,56 @@
import React from 'react'
import PropTypes from 'prop-types'
import Octicon, {GitMerge, IssueClosed, IssueOpened, IssueReopened} from '@githubprimer/octicons-react'
import classnames from 'classnames'
import sass from 'sass.macro'
import {injectGlobal} from 'emotion'
import {colors} from './theme'
import styled from 'react-emotion'
import {GitMerge, GitPullRequest, IssueClosed, IssueOpened} from '@githubprimer/octicons-react'
import theme, {colors} from './theme'
import {withSystemProps, COMMON} from './system-props'

injectGlobal(sass`
@import "primer-support/index.scss";
@import "primer-labels/lib/states.scss";
`)

const stateColorMap = {
open: 'green',
opened: 'green',
reopened: 'green',
closed: 'red',
merged: 'purple'
import Octicon from './Octicon'

const schemeMap = {
issueClosed: colors.red[6],
pullClosed: colors.red[6],
pullMerged: colors.purple[5],
issueOpened: '#2cbe4e', // This was generated by a sass function in Primer, so using a hex here
pullOpened: '#2cbe4e', // This was generated by a sass function in Primer, so using a hex here
gray: colors.gray[5]
}

const stateOcticonMap = {
open: IssueOpened,
opened: IssueOpened,
reopened: IssueReopened,
closed: IssueClosed,
merged: GitMerge
const octiconMap = {
issueOpened: IssueOpened,
pullOpened: GitPullRequest,
issueClosed: IssueClosed,
pullClosed: GitPullRequest,
pullMerged: GitMerge
}

function getOcticon(state) {
if (!state) {
return null
}
return <Octicon icon={stateOcticonMap[state]} />
}

function getIconComponent(icon, children) {
if (icon && children) {
return <span className="mr-1">{icon}</span>
} else if (icon) {
return <span className="d-flex m-1">{icon}</span>
}
return null
}

function StateLabel({state, className, scheme, icon, small, children}) {
if (icon !== false) {
icon = icon || getOcticon(state)
}

const color = scheme || stateColorMap[state]
const styleProps = {}
if (color === 'yellow') {
styleProps.style = {backgroundColor: colors.yellow[7]}
}
const iconComponent = getIconComponent(icon, children)
const classes = classnames(
className,
'State',
{
'State--small': small
},
color && color !== 'yellow' ? `State--${color}` : null
)
const getOcticon = (scheme, small) =>
small ? <Octicon mr={1} width="1em" icon={octiconMap[scheme]} /> : <Octicon mr={1} icon={octiconMap[scheme]} />
Copy link
Contributor

Choose a reason for hiding this comment

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

This smells funky to me since all it's actually varying is the width prop. Could we inline it back in the render function like so?

<Octicon icon={octiconMap[scheme]} mr={1} width={small ? '1em' : null} />

Copy link
Author

Choose a reason for hiding this comment

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

No, because setting the width to null here overrides the width set in the SVG and makes it the wrong size. I tried that first!

Copy link
Contributor

Choose a reason for hiding this comment

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

Oof. Maybe something like this? 😬

const octiconProps = small ? {width: '1em'} : {}
// ...
<Octicon mr={1} icon={octiconMap[scheme]} {...octiconProps} />


function StateLabel({className, scheme, small = false, children}) {
return (
<span className={classes} {...styleProps}>
{iconComponent}
<span className={className}>
{scheme && getOcticon(scheme, small)}
{children}
</span>
)
}

const styledLabel = styled(StateLabel)`
display: inline-flex;
align-items: center;
padding: ${props => (props.small ? `0.125em ${theme.space[1]}px` : `${theme.space[1]}px ${theme.space[2]}px`)};
font-weight: 600;
Copy link
Contributor

@broccolini broccolini Oct 11, 2018

Choose a reason for hiding this comment

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

If we have fontWeight defined in our theme can we reference the theme value here?

Copy link
Author

Choose a reason for hiding this comment

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

We could, but it's not defined yet - I can add it in, what font weights should we use?

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed: we should add fontWeight to the list of system props and set defaultProps.fontWeight = 'bold' rather than hard-coding this.

Copy link
Author

Choose a reason for hiding this comment

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

I don't think we want fontWeight to be a prop on this component, but yeah I agree we should add font weight to our theme! We can do that as a follow up, and replace any other instances at that time.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, so maybe: font-weight: ${themeGet('fontWeights.bold')}?

line-height: 20px;
color: ${colors.white};
font-size: ${props => (props.small ? `${theme.fontSizes[0]}px` : `${theme.fontSizes[1]}px`)};
text-align: center;
background-color: ${props => (props.scheme ? schemeMap[props.scheme] : schemeMap.gray)};
border-radius: ${theme.radii[1]}px;
`

StateLabel.propTypes = {
children: PropTypes.node,
icon: PropTypes.oneOfType([PropTypes.node, PropTypes.bool]),
scheme: PropTypes.string,
small: PropTypes.bool,
state: PropTypes.oneOf(Object.keys(stateOcticonMap))
scheme: PropTypes.oneOf(['issueOpened', 'pullOpened', 'issueClosed', 'pullClosed', 'pullMerged']),
small: PropTypes.bool
}

export default withSystemProps(StateLabel, COMMON)
export default withSystemProps(styledLabel, COMMON)
15 changes: 1 addition & 14 deletions src/__tests__/Label.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,7 @@ describe('Label', () => {
})

it('respects the "outline" prop', () => {
expect(render(<Label />)).toHaveClasses(['Label', 'Label--gray'])
expect(render(<Label outline />)).toHaveClasses(['Label', 'Label--outline'])
})

it('respects the "scheme" prop', () => {
expect(render(<Label scheme={null} />)).toHaveClasses(['Label', 'Label--gray'])
expect(render(<Label scheme="gray" />)).toHaveClasses(['Label', 'Label--gray'])
expect(render(<Label scheme="gray-darker" />)).toHaveClasses(['Label', 'Label--gray-darker'])
expect(render(<Label scheme="orange" />)).toHaveClasses(['Label', 'Label--orange'])
expect(render(<Label scheme="green" />)).toHaveClasses(['Label', 'bg-green'])
})

it('respects scheme="green" + outline', () => {
expect(render(<Label outline scheme="green" />)).toHaveClasses(['Label', 'Label--outline', 'Label--outline-green'])
expect(render(<Label outline />)).toMatchSnapshot()
})

it('implements space system props', () => {
Expand Down
25 changes: 3 additions & 22 deletions src/__tests__/StateLabel.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react'
import Octicon, {Check} from '@githubprimer/octicons-react'
import StateLabel from '../StateLabel'
import {render} from '../utils/testing'
import {COMMON} from '../system-props'
Expand All @@ -14,41 +13,23 @@ describe('StateLabel', () => {
})

it('respects the scheme prop', () => {
expect(render(<StateLabel scheme="green" />)).toMatchSnapshot()
expect(render(<StateLabel scheme="red" />)).toMatchSnapshot()
expect(render(<StateLabel scheme="purple" />)).toMatchSnapshot()
expect(render(<StateLabel scheme="issueOpened" />)).toMatchSnapshot()
expect(render(<StateLabel scheme="issueClosed" />)).toMatchSnapshot()
expect(render(<StateLabel scheme="pullMerged" />)).toMatchSnapshot()
})

it('respects the small flag', () => {
expect(render(<StateLabel small />)).toMatchSnapshot()
expect(render(<StateLabel small={false} />)).toMatchSnapshot()
})

it('renders states as specific colors', () => {
for (const state of ['open', 'reopened', 'merged', 'closed']) {
expect(render(<StateLabel state={state} />)).toMatchSnapshot()
}
})

it('renders children', () => {
expect(render(<StateLabel>hi</StateLabel>)).toMatchSnapshot()
})

it('renders icon with children', () => {
expect(render(<StateLabel state="open">hi</StateLabel>)).toMatchSnapshot()
})

it('does not pass on arbitrary attributes', () => {
const defaultOutput = render(<StateLabel />)
expect(render(<StateLabel data-foo="bar" />)).toEqual(defaultOutput)
expect(render(<StateLabel hidden />)).toEqual(defaultOutput)
})

it('respects the icon prop', () => {
expect(render(<StateLabel icon={<Octicon icon={Check} />} state="open" />)).toMatchSnapshot()
})

it('respects icon={false}', () => {
expect(render(<StateLabel state="open" icon={false} />)).toMatchSnapshot()
})
})
31 changes: 31 additions & 0 deletions src/__tests__/__snapshots__/Label.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Label respects the "outline" prop 1`] = `
.emotion-0 {
display: inline-block;
font-weight: 600;
line-height: 1;
color: #fff;
border-radius: 2px;
background-color: #6a737d;
margin-top: -1px;
margin-bottom: -1px;
font-weight: 400;
color: #586069;
background-color: transparent;
border: 1px solid rgba(27,31,35,0.15);
box-shadow: none;
padding: 3px 4px;
font-size: 12px;
}

.emotion-0:hover {
-webkit-text-decoration: none;
text-decoration: none;
}

<span
className="emotion-0"
size="medium"
/>
`;
Loading