Browse Source

feat(ui): add Dropdown component

renovate/lint-staged-8.x
Tom Kirkpatrick 6 years ago
parent
commit
b552aa60f1
No known key found for this signature in database GPG Key ID: 72203A8EC5967EA8
  1. 181
      app/components/UI/Dropdown.js
  2. 1
      package.json
  3. 58
      stories/components/dropdown.stories.js
  4. 30
      test/unit/components/UI/Dropdown.spec.js
  5. 81
      test/unit/components/UI/__snapshots__/Dropdown.spec.js.snap
  6. 9
      yarn.lock

181
app/components/UI/Dropdown.js

@ -0,0 +1,181 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Box, Flex } from 'rebass'
import styled, { withTheme } from 'styled-components'
import FaAngleDown from 'react-icons/lib/fa/angle-down'
import FaAngleUp from 'react-icons/lib/fa/angle-up'
import Check from 'components/Icon/Check'
import Text from 'components/UI/Text'
/**
* Container
*/
const DropdownContainer = styled(Flex)({})
DropdownContainer.defaultProps = {
flexDirection: 'column',
flexWrap: 'none',
display: 'relative'
}
/**
* Button
*/
const DropdownButton = styled(Box)({
appearance: 'none',
display: 'inline-block',
textAlign: 'center',
lineHeight: 'inherit',
textDecoration: 'none',
border: 'none',
outline: 'none',
background: 'transparent',
color: 'inherit',
cursor: 'pointer'
})
DropdownButton.defaultProps = {
as: 'button',
m: 0,
px: 0,
py: 2,
textAlign: 'left'
}
/**
* Menu
*/
const MenuContainer = styled(Box)({
display: 'relative'
})
const Menu = styled(Box)({
cursor: 'pointer',
display: 'inline-block',
position: 'absolute',
'z-index': '999',
'min-width': '70px',
'list-style-type': 'none',
'border-radius': '3px',
'box-shadow': '0 3px 4px 0 rgba(30, 30, 30, 0.5)'
})
Menu.defaultProps = {
as: 'ul',
m: 0,
p: 0,
bg: 'lightestBackground'
}
/**
* MenuItem
*/
const MenuItem = styled(Box)`
cursor: pointer;
&:hover {
background-color: ${props => props.theme.colors.darkestBackground};
}
`
MenuItem.defaultProps = {
as: 'li',
px: 2,
py: 2
}
/**
* @render react
* @name Dropdown
* @example
* <Dropdown items={[
* {name: 'Item 1', key: 'key1'},
* {name: 'Item 2', key: 'key2'}
* ]} activeKey="key1" />
*/
class Dropdown extends React.Component {
state = {
isOpen: false
}
onChange = this.onChange.bind(this)
toggleMenu = this.toggleMenu.bind(this)
setWrapperRef = this.setWrapperRef.bind(this)
handleClickOutside = this.handleClickOutside.bind(this)
static propTypes = {
activeKey: PropTypes.string.isRequired,
items: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
})
).isRequired,
onChange: PropTypes.func
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside)
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside)
}
onChange(key) {
const { onChange, activeKey } = this.props
if (key !== activeKey) {
if (onChange) {
onChange(key)
}
}
this.setState({ isOpen: false })
}
setWrapperRef(node) {
this.wrapperRef = node
}
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
this.setState({ isOpen: false })
}
}
toggleMenu() {
const { isOpen } = this.state
this.setState({ isOpen: !isOpen })
}
render() {
const { isOpen } = this.state
const { activeKey, items, theme, ...rest } = this.props
const selectedItem = items.find(c => c.key === activeKey)
return (
<DropdownContainer ref={this.setWrapperRef} {...rest}>
<DropdownButton type="button" onClick={this.toggleMenu}>
<Text textAlign="left">
{selectedItem ? selectedItem.name : activeKey}{' '}
{isOpen ? <FaAngleUp /> : <FaAngleDown />}
</Text>
</DropdownButton>
{isOpen && (
<MenuContainer>
<Menu>
{items.map(item => {
return (
<MenuItem key={item.key} onClick={() => this.onChange(item.key)}>
<Flex alignItems="center">
<Text width="18px">
{activeKey === item.key && (
<Check height="0.95em" color={theme.colors.superGreen} />
)}
</Text>
<Text>{item.name}</Text>
</Flex>
</MenuItem>
)
})}
</Menu>
</MenuContainer>
)}
</DropdownContainer>
)
}
}
export default withTheme(Dropdown)

1
package.json

@ -211,6 +211,7 @@
"@babel/register": "^7.0.0",
"@commitlint/cli": "^7.2.1",
"@commitlint/config-conventional": "^7.1.2",
"@sambego/storybook-state": "^1.3.1",
"@storybook/addon-actions": "^4.0.2",
"@storybook/addon-console": "^1.1.0",
"@storybook/addon-info": "^4.0.2",

58
stories/components/dropdown.stories.js

@ -0,0 +1,58 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import { StateDecorator, Store } from '@sambego/storybook-state'
import Dropdown from 'components/UI/Dropdown'
const store = new Store({
crypto: 'btc',
fiat: 'usd',
cryptoCurrencies: [
{
key: 'btc',
name: 'BTC'
},
{
key: 'bits',
name: 'bits'
},
{
key: 'sats',
name: 'satoshis'
}
],
fiatCurrencies: [
{
key: 'usd',
name: 'USD'
},
{
key: 'eur',
name: 'EUR'
},
{
key: 'gbp',
name: 'GBP'
}
]
})
storiesOf('Components.Dropdown', module)
.addDecorator(StateDecorator(store))
.add('Crypto', () => {
return (
<Dropdown
activeKey={store.get('crypto')}
items={store.get('cryptoCurrencies')}
onChange={crypto => store.set({ crypto })}
/>
)
})
.add('Fiat', () => {
return (
<Dropdown
activeKey={store.get('fiat')}
items={store.get('fiatCurrencies')}
onChange={fiat => store.set({ fiat })}
/>
)
})

30
test/unit/components/UI/Dropdown.spec.js

@ -0,0 +1,30 @@
import React from 'react'
import Dropdown from 'components/UI/Dropdown'
import renderer from 'react-test-renderer'
import { dark } from 'themes'
const currencies = [
{
key: 'btc',
name: 'BTC'
},
{
key: 'bits',
name: 'bits'
},
{
key: 'sats',
name: 'satoshis'
}
]
const setCurrency = jest.fn()
describe('component.Dropdown', () => {
it('should render correctly', () => {
const tree = renderer
.create(<Dropdown theme={dark} activeKey="btc" items={currencies} onClick={setCurrency} />)
.toJSON()
expect(tree).toMatchSnapshot()
})
})

81
test/unit/components/UI/__snapshots__/Dropdown.spec.js.snap

@ -0,0 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`component.Dropdown should render correctly 1`] = `
.c2 {
font-size: m;
text-align: left;
}
.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-wrap: none;
-ms-flex-wrap: none;
flex-wrap: none;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c1 {
margin: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 8px;
padding-bottom: 8px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: inline-block;
text-align: center;
line-height: inherit;
-webkit-text-decoration: none;
text-decoration: none;
border: none;
outline: none;
background: transparent;
color: inherit;
cursor: pointer;
}
<div
className="c0"
display="relative"
onClick={[MockFunction]}
>
<button
className="c1"
onClick={[Function]}
type="button"
>
<div
className="c2"
fontSize="m"
>
BTC
<svg
fill="currentColor"
height="1em"
preserveAspectRatio="xMidYMid meet"
style={
Object {
"color": undefined,
"verticalAlign": "middle",
}
}
viewBox="0 0 40 40"
width="1em"
>
<g>
<path
d="m31 16.4q0 0.3-0.2 0.5l-10.4 10.4q-0.3 0.3-0.5 0.3t-0.6-0.3l-10.4-10.4q-0.2-0.2-0.2-0.5t0.2-0.5l1.2-1.1q0.2-0.2 0.5-0.2t0.5 0.2l8.8 8.8 8.7-8.8q0.3-0.2 0.5-0.2t0.6 0.2l1.1 1.1q0.2 0.2 0.2 0.5z"
/>
</g>
</svg>
</div>
</button>
</div>
`;

9
yarn.lock

@ -1101,6 +1101,13 @@
dependencies:
styled-system "^3.0.1"
"@sambego/storybook-state@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@sambego/storybook-state/-/storybook-state-1.3.1.tgz#9aec5f8e10e9df3f689eb70eff6903d41d9ce2a7"
integrity sha512-JFnu/AuAcpk8trsxUweFUGRF92a/qpg8ZpAclusOVs5jp7XvhiEsJehqGlqueytZpZyzFgTkla8Mjh/D98FaXQ==
dependencies:
uuid "^3.1.0"
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
@ -16369,7 +16376,7 @@ uuid@3.2.1:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==
uuid@^3.0.1, uuid@^3.2.1, uuid@^3.3.2:
uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==

Loading…
Cancel
Save