Browse Source
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
7 changed files with 401 additions and 1 deletions
@ -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) |
@ -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 |
@ -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 |
@ -0,0 +1,3 @@ |
|||
// @flow
|
|||
|
|||
export { default as SideBarList } from './SideBarList' |
@ -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')} |
|||
/> |
|||
)) |
@ -0,0 +1,5 @@ |
|||
import styled from 'styled-components' |
|||
|
|||
export default styled.div` |
|||
height: ${p => p.of}px; |
|||
` |
Loading…
Reference in new issue