Browse Source

Rewrite SideBar component

which was causing troubles, especially when watching routes change and
matching location path. this commit separate logic/presentational
components for sidebar & main sidebar

also added a subtle "focus" state for items, for accessibility &
keyboard navigation. open to ideas for design change.
master
meriadec 7 years ago
parent
commit
7dfa5a0b1d
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 172
      src/components/MainSideBar.js
  2. 64
      src/components/base/SideBar/SideBarList.js
  3. 94
      src/components/base/SideBar/SideBarListItem.js
  4. 3
      src/components/base/SideBar/index.js
  5. 62
      src/components/base/SideBar/stories.js
  6. 5
      src/components/base/Space.js
  7. 2
      src/components/layout/Default.js

172
src/components/MainSideBar.js

@ -0,0 +1,172 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { compose } from 'redux'
import { withRouter } from 'react-router'
import { push } from 'react-router-redux'
import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react'
import type { Location } from 'react-router'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import type { UpdateStatus } from 'reducers/update'
import { MODAL_RECEIVE, MODAL_SEND } from 'config/constants'
import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import { getUpdateStatus } from 'reducers/update'
import { SideBarList } from 'components/base/SideBar'
import Box, { Tabbable } from 'components/base/Box'
import Space from 'components/base/Space'
import FormattedVal from 'components/base/FormattedVal'
import IconManager from 'icons/Manager'
import IconPieChart from 'icons/PieChart'
import IconPlus from 'icons/Plus'
import IconReceive from 'icons/Receive'
import IconSend from 'icons/Send'
import IconExchange from 'icons/Exchange'
const mapStateToProps = state => ({
accounts: accountsSelector(state),
updateStatus: getUpdateStatus(state),
})
const mapDispatchToProps = {
push,
openModal,
}
type Props = {
t: T,
accounts: Account[],
location: Location,
push: string => void,
openModal: string => void,
updateStatus: UpdateStatus,
}
class MainSideBar extends PureComponent<Props> {
push(to: string) {
const { push } = this.props
const {
location: { pathname },
} = this.props
if (pathname === to) {
return
}
push(to)
}
render() {
const { t, accounts, openModal, location, updateStatus } = this.props
const { pathname } = location
const navigationItems = [
{
value: 'dashboard',
label: t('dashboard:title'),
icon: IconPieChart,
iconActiveColor: 'wallet',
onClick: () => this.push('/'),
isActive: pathname === '/',
hasNotif: updateStatus === 'downloaded',
},
{
value: 'send',
label: t('send:title'),
icon: IconSend,
iconActiveColor: 'wallet',
onClick: () => openModal(MODAL_SEND),
},
{
value: 'receive',
label: t('receive:title'),
icon: IconReceive,
iconActiveColor: 'wallet',
onClick: () => openModal(MODAL_RECEIVE),
},
{
value: 'manager',
label: t('sidebar:manager'),
icon: IconManager,
iconActiveColor: 'wallet',
onClick: () => this.push('/manager'),
isActive: pathname === '/manager',
},
{
value: 'exchange',
label: t('sidebar:exchange'),
icon: IconExchange,
iconActiveColor: 'wallet',
onClick: () => this.push('/exchange'),
isActive: pathname === '/exchange',
},
]
const accountsItems = accounts.map(account => {
const accountURL = `/account/${account.id}`
return {
value: account.id,
label: account.name,
desc: () => (
<FormattedVal
alwaysShowSign={false}
color="graphite"
unit={account.unit}
showCode
val={account.balance || 0}
/>
),
iconActiveColor: account.currency.color,
icon: getCryptoCurrencyIcon(account.currency),
onClick: () => this.push(accountURL),
isActive: pathname === accountURL,
}
})
return (
<Box bg="white" style={{ width: 230 }}>
<Space of={60} />
<SideBarList items={navigationItems} />
<Space of={40} />
<SideBarList
scroll
title={t('sidebar:menu')}
titleRight={
<PlusWrapper onClick={() => openModal('importAccounts')}>
<IconPlus size={16} />
</PlusWrapper>
}
items={accountsItems}
/>
</Box>
)
}
}
const PlusWrapper = styled(Tabbable).attrs({
p: 1,
cursor: 'pointer',
borderRadius: 1,
})`
opacity: 0.4;
&:hover {
opacity: 1;
}
border: 1px dashed rgba(0, 0, 0, 0);
&:focus {
border: 1px dashed rgba(0, 0, 0, 0.2);
outline: none;
}
`
const decorate = compose(withRouter, translate(), connect(mapStateToProps, mapDispatchToProps))
export default decorate(MainSideBar)

64
src/components/base/SideBar/SideBarList.js

@ -0,0 +1,64 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
import GrowScroll from 'components/base/GrowScroll'
import Box from 'components/base/Box'
import Space from 'components/base/Space'
import SideBarListItem from './SideBarListItem'
import type { Item } from './SideBarListItem'
type Props = {
items: Item[],
title?: Node | string,
activeValue?: string,
scroll?: boolean,
titleRight?: any, // TODO: type should be more precise, but, eh ¯\_(ツ)_/¯
}
class SideBarList extends PureComponent<Props> {
render() {
const { items, title, activeValue, scroll, titleRight, ...props } = this.props
const ListWrapper = scroll ? GrowScroll : Box
return (
<Fragment>
{!!title && (
<Fragment>
<SideBarListTitle>
{title}
{!!titleRight && <Box ml="auto">{titleRight}</Box>}
</SideBarListTitle>
<Space of={10} />
</Fragment>
)}
<ListWrapper flow={2} px={3} fontSize={3} {...props}>
{items.map(item => {
const itemProps = {
item,
isActive: item.isActive || (!!activeValue && activeValue === item.value),
}
return <SideBarListItem key={item.value} {...itemProps} />
})}
</ListWrapper>
</Fragment>
)
}
}
const SideBarListTitle = styled(Box).attrs({
horizontal: true,
align: 'center',
color: 'dark',
ff: 'Museo Sans|ExtraBold',
fontSize: 1,
px: 4,
})`
cursor: default;
letter-spacing: 2px;
text-transform: uppercase;
`
export default SideBarList

94
src/components/base/SideBar/SideBarListItem.js

@ -0,0 +1,94 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import Box, { Tabbable } from 'components/base/Box'
export type Item = {
value: string,
label: string | (Props => React$Element<any>),
desc?: Props => any, // TODO: type should be more precise, but, eh ¯\_(ツ)_/¯
icon?: any, // TODO: type should be more precise, but, eh ¯\_(ツ)_/¯
iconActiveColor: ?string,
hasNotif?: boolean,
isActive?: boolean,
onClick?: void => void,
}
export type Props = {
item: Item,
isActive: boolean,
}
class SideBarListItem extends PureComponent<Props> {
render() {
const {
item: { icon: Icon, label, desc, iconActiveColor, hasNotif, onClick },
isActive,
} = this.props
return (
<Container isActive={isActive} iconActiveColor={iconActiveColor} onClick={onClick}>
{!!Icon && <Icon size={16} />}
<Box grow shrink>
{typeof label === 'function' ? (
label(this.props)
) : (
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{label}
</span>
)}
{!!desc && desc(this.props)}
</Box>
{!!hasNotif && <Bullet />}
</Container>
)
}
}
const Container = styled(Tabbable).attrs({
align: 'center',
borderRadius: 1,
ff: 'Open Sans|SemiBold',
flow: 3,
horizontal: true,
px: 3,
py: 2,
})`
cursor: ${p => (p.isActive ? 'default' : 'pointer')};
color: ${p => p.theme.colors.dark};
background: ${p => (p.isActive ? p.theme.colors.lightGrey : '')};
opacity: ${p => (p.isActive ? 1 : 0.4)};
&:active {
background: ${p => p.theme.colors.lightGrey};
}
&:hover {
opacity: ${p => (p.isActive ? 1 : 0.7)};
}
border: 1px dashed rgba(0, 0, 0, 0);
&:focus {
border: 1px dashed rgba(0, 0, 0, 0.2);
outline: none;
}
${p => {
const iconActiveColor = p.theme.colors[p.iconActiveColor] || p.iconActiveColor
return `
svg { color: ${p.isActive ? iconActiveColor : ''}; }
&:hover svg { color: ${iconActiveColor}; }
`
}};
`
const Bullet = styled.div`
background: ${p => p.theme.colors.wallet};
width: 8px;
height: 8px;
border-radius: 100%;
`
export default SideBarListItem

3
src/components/base/SideBar/index.js

@ -0,0 +1,3 @@
// @flow
export { default as SideBarList } from './SideBarList'

62
src/components/base/SideBar/stories.js

@ -0,0 +1,62 @@
// @flow
import React from 'react'
import { storiesOf } from '@storybook/react'
import { select } from '@storybook/addon-knobs'
import { SideBarList } from 'components/base/SideBar'
import Box from 'components/base/Box'
import IconAccountSettings from 'icons/AccountSettings'
import IconPrint from 'icons/Print'
import IconControls from 'icons/Controls'
import IconCurrencies from 'icons/Currencies'
import IconExclamationCircle from 'icons/ExclamationCircle'
const stories = storiesOf('Components/base/SideBar', module)
const SIDEBAR_ITEMS = [
{
value: 'first',
label: 'First',
icon: IconAccountSettings,
iconActiveColor: '#ffae35',
},
{
value: 'second',
label: 'Second',
icon: IconPrint,
iconActiveColor: '#0ebdcd',
},
{
value: 'third',
label: 'Third very very very very long text very long text very long',
icon: IconControls,
iconActiveColor: '#27a2db',
hasNotif: true,
},
{
value: 'fourth',
label: () => (
<Box>
{'custom'}
<Box>{'render'}</Box>
</Box>
),
icon: IconCurrencies,
iconActiveColor: '#3ca569',
},
{
value: 'fifth',
label: 'Fifth',
icon: IconExclamationCircle,
iconActiveColor: '#0e76aa',
},
]
stories.add('SideBarList', () => (
<SideBarList
items={SIDEBAR_ITEMS}
activeValue={select('activeValue', [...SIDEBAR_ITEMS.map(i => i.value), null], 'third')}
/>
))

5
src/components/base/Space.js

@ -0,0 +1,5 @@
import styled from 'styled-components'
export default styled.div`
height: ${p => p.of}px;
`

2
src/components/layout/Default.js

@ -21,7 +21,7 @@ import SettingsPage from 'components/SettingsPage'
import AppRegionDrag from 'components/AppRegionDrag'
import IsUnlocked from 'components/IsUnlocked'
import SideBar from 'components/SideBar'
import SideBar from 'components/MainSideBar'
import TopBar from 'components/TopBar'
const Container = styled(GrowScroll).attrs({

Loading…
Cancel
Save