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

Add Caret component #58

Merged
merged 18 commits into from
Jun 7, 2018
Merged
Show file tree
Hide file tree
Changes from 18 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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ Run app with: `npm run start`

Build docs before publishing: `npm run build`

### Testing

We test our components with [Jest](https://facebook.github.io/jest/) and
[react-test-renderer](https://reactjs.org/docs/test-renderer.html). You can run
the tests locally with `npm test` (or `npm t`). To run the tests as you work,
run Jest in watch mode with:

```
npm t -- --watch
```

See [`src/__tests__/example.js`](src/__tests/example.js) for an
example of how we're testing our components.

## Principles

Expand Down
30 changes: 15 additions & 15 deletions docs/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/index.html

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions examples/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ButtonOutline,
ButtonLink,
BranchName,
Caret,
CounterLabel,
Details,
DonutChart,
Expand Down Expand Up @@ -122,6 +123,22 @@ const Index = props => (
<ButtonLink href='https://www.goatslive.com/'>This is an {'<a>'} styled as a button</ButtonLink>
</Block>
</Example>
<Example name='Caret'>
<Block p={4}>
{['top', 'right', 'bottom', 'left'].map((edge, i) => (
<Box p={2} mb={4} position='relative' maxWidth={400} key={i}>
<Text mono>edge='{edge}'</Text>
<Caret edge={edge} />
</Box>
))}
{['top', 'right', 'bottom', 'left'].map((edge, i) => (
<Box shadow='medium' p={2} mb={4} position='relative' maxWidth={400} key={i}>
<Text mono>edge='{edge}' in shadow='medium'</Text>
<Caret edge={edge} />
</Box>
))}
</Block>
</Example>
<Example name='Colors'>
{['gray', 'blue', 'green', 'purple', 'yellow', 'orange'].map((hue, i) => (
<div className='d-flex' key={i}>
Expand Down
35 changes: 32 additions & 3 deletions src/Block.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,40 @@ const classifyBlockProps = classifier({
bg: value => `bg-${value}`,
border: expander(valueMapper({
true: 'border',
false: 'border-0',
false: 'border-0'
}, null, value => `border-${value}`)),
fg: value => `text-${value}`,
round: value => `rounded-${value}`
position: value => `position-${value}`,
round: value => `rounded-${value}`,
shadow: valueMapper({
true: 'box-shadow'
}, null, value => `box-shadow-${value}`)
})

const mapBlockProps = props => classifyBlockProps(map(props))
const stylize = props => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This seems like something we might want a helper for in props.js because it's kind of a pain to maintain. I'd like to see something like:

import {passToStyle} from './props'

const stylize = passToStyle([
  'width', 'minWidth', 'maxWidth',
  'height', 'minHeight', 'maxHeight'
])

const mapBlockProps = props => classifyBlockProps(map(stylize(props))

Copy link

Choose a reason for hiding this comment

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

Agreed, I was running into this when I was working on the Dropdown stuff

const {
width,
minWidth,
maxWidth,
height,
minHeight,
maxHeight,
...rest
} = props
return {
style: {
width,
minWidth,
maxWidth,
height,
minHeight,
maxHeight
},
...rest
}
}

const mapBlockProps = props => classifyBlockProps(map(stylize(props)))

const Block = chameleon('div', mapBlockProps)

Expand All @@ -24,7 +51,9 @@ Block.propTypes = {
PropTypes.bool
])),
fg: PropTypes.string,
position: PropTypes.oneOf(['absolute', 'fixed', 'relative']),
round: PropTypes.number,
shadow: PropTypes.oneOf([true, 'medium', 'large', 'extra-large']),
...map.propTypes
}

Expand Down
158 changes: 158 additions & 0 deletions src/Caret.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React from 'react'
import PropTypes from 'prop-types'
import theme from './theme'

const oppositeEdge = {
top: 'Bottom',
right: 'Left',
bottom: 'Top',
left: 'Right'
}

const perpendicularEdge = {
top: 'Left',
right: 'Top',
bottom: 'Left',
left: 'Top'
}

function getPosition({edge, align}) {
const opposite = oppositeEdge[edge].toLowerCase()
const perp = perpendicularEdge[edge].toLowerCase()
return {
[opposite]: '100%',
[perp]: '50%'
}
}

export default function Caret({css, ...rest}) {
// TODO: should the svg switch even be configurable,
// or do we use feature detection here?
return css
? <CaretCSS {...rest} />
: <CaretSVG {...rest} />
}

Caret.defaultProps = {
align: 'center',
borderColor: theme.colors.gray[2],
borderWidth: 1,
edge: 'bottom',
fill: theme.colors.white,
position: 'absolute',
size: 8,
css: false
}

Caret.propTypes = {
align: PropTypes.oneOf(['start', 'center', 'end']),
borderColor: PropTypes.string,
borderWidth: PropTypes.number,
edge: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
fill: PropTypes.string,
position: PropTypes.oneOf(['absolute', 'relative']),
size: PropTypes.number,
css: PropTypes.bool
}

function CaretCSS(props) {
const {
align,
borderColor,
borderWidth,
edge,
fill,
position,
size
} = props

const opposite = oppositeEdge[edge]
const perp = perpendicularEdge[edge]

const style = {
pointerEvents: 'none',
position,
...getPosition({edge, align})
}

const common = {
borderStyle: 'solid',
borderWidth,
position: 'absolute',
[opposite.toLowerCase()]: '100%',
}

const after = {
...common,
borderColor: 'transparent',
borderWidth: size,
[`border${opposite}Color`]: fill,
[`margin${perp}`]: -size
}

const before = {
...common,
borderColor: 'transparent',
[`border${opposite}Color`]: borderColor,
borderWidth: borderWidth + size,
[`margin${perp}`]: -(size + borderWidth)
}

return (
<div style={style}>
<div style={before} />
<div style={after} />
</div>
)
}

function CaretSVG(props) {
const {
align,
borderColor,
borderWidth,
edge,
fill,
position,
size
} = props

const perp = perpendicularEdge[edge]

const style = {
pointerEvents: 'none',
position,
...getPosition({edge, align}),
[`margin${perp}`]: -size
}

// note: these arrays represent points in the form [x, y]
const a = [-size, 0]
const b = [0, size]
const c = [size, 0]

// spaces are optional in path `d` attribute, and points are
// represented in the form `x,y` -- which is what the arrays above
// become when stringified!
const triangle = `M${a}L${b}L${c}L${a}Z`
const line = `M${a}L${b}L${c}`

const transform = {
top: `translate(${[size, size * 2]}) rotate(180)`,
right: `translate(${[0, size]}) rotate(-90)`,
bottom: `translate(${[size, 0]})`,
left: `translate(${[size * 2, size]}) rotate(90)`
}[edge]
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 thought that this was easier to read than a switch statement, but can change it if it seems too clever.

Copy link

Choose a reason for hiding this comment

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

I think it's fine this way!


return (
<svg width={size * 2} height={size * 2} style={style}>
<g transform={transform}>
<path d={triangle} fill={fill} />
<path d={line}
fill='none'
stroke={borderColor}
stroke-width={borderWidth} />
</g>
</svg>
)
}
15 changes: 15 additions & 0 deletions src/Text.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import chameleon from './chameleon'
import map, {classifier, valueMapper} from './props'

Expand All @@ -15,10 +16,24 @@ const classifyTextProps = classifier({
}, value => `f${value}`, true),
fontWeight: value => `text-${value}`,
lineHeight: value => `lh-${value}`,
mono: 'text-mono'
})

const textProps = props => classifyTextProps(map(props))

const Text = chameleon('span', textProps, true)

Text.propTypes = {
// TODO: constrain with PropTypes.oneOf()
color: PropTypes.string,
// TODO constrain with PropTypes.oneOf()
fontSize: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]),
fontWeight: PropTypes.oneOf(['normal', 'bold']),
lineHeight: PropTypes.oneOf(['normal', 'condensed', 'condensed-ultra']),
mono: PropTypes.bool
}

export default Text
18 changes: 18 additions & 0 deletions src/__tests__/Block.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,22 @@ describe('Block', () => {
expect(renderClasses(<Block border={['left', 'green']} />))
.toEqual(['border-left', 'border-green'])
})

it('renders position', () => {
expect(renderClasses(<Block position='absolute' />))
.toEqual(['position-absolute'])
expect(renderClasses(<Block position='relative' />))
.toEqual(['position-relative'])
})

it('renders shadow', () => {
expect(renderClasses(<Block shadow />))
.toEqual(['box-shadow'])
expect(renderClasses(<Block shadow='medium' />))
.toEqual(['box-shadow-medium'])
expect(renderClasses(<Block shadow='large' />))
.toEqual(['box-shadow-large'])
expect(renderClasses(<Block shadow='extra-large' />))
.toEqual(['box-shadow-extra-large'])
})
})
13 changes: 13 additions & 0 deletions src/__tests__/Caret.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react'
import Caret from '../Caret'
import {render} from '../utils/testing'

describe('Caret', () => {
it('outputs <svg> by default', () => {
expect(render(<Caret />).type).toEqual('svg')
})

it('outputs a <div> if the "css" prop is present', () => {
expect(render(<Caret css />).type).toEqual('div')
})
})
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export { default as ButtonPrimary } from './ButtonPrimary'
export { default as ButtonOutline } from './ButtonOutline'
export { default as ButtonLink } from './ButtonLink'

export { default as Caret } from './Caret'

export { default as Details } from './Details'

export { default as DonutChart} from './DonutChart'
Expand Down