From 7dfa5a0b1dbfecb036c5e661b44016512b15d334 Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 3 Jun 2018 22:49:27 +0200 Subject: [PATCH] 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. --- src/components/MainSideBar.js | 172 ++++++++++++++++++ src/components/base/SideBar/SideBarList.js | 64 +++++++ .../base/SideBar/SideBarListItem.js | 94 ++++++++++ src/components/base/SideBar/index.js | 3 + src/components/base/SideBar/stories.js | 62 +++++++ src/components/base/Space.js | 5 + src/components/layout/Default.js | 2 +- 7 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 src/components/MainSideBar.js create mode 100644 src/components/base/SideBar/SideBarList.js create mode 100644 src/components/base/SideBar/SideBarListItem.js create mode 100644 src/components/base/SideBar/index.js create mode 100644 src/components/base/SideBar/stories.js create mode 100644 src/components/base/Space.js diff --git a/src/components/MainSideBar.js b/src/components/MainSideBar.js new file mode 100644 index 00000000..dbe20018 --- /dev/null +++ b/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 { + 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: () => ( + + ), + iconActiveColor: account.currency.color, + icon: getCryptoCurrencyIcon(account.currency), + onClick: () => this.push(accountURL), + isActive: pathname === accountURL, + } + }) + + return ( + + + + + openModal('importAccounts')}> + + + } + items={accountsItems} + /> + + ) + } +} + +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) diff --git a/src/components/base/SideBar/SideBarList.js b/src/components/base/SideBar/SideBarList.js new file mode 100644 index 00000000..d0572bd3 --- /dev/null +++ b/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 { + render() { + const { items, title, activeValue, scroll, titleRight, ...props } = this.props + const ListWrapper = scroll ? GrowScroll : Box + return ( + + {!!title && ( + + + {title} + {!!titleRight && {titleRight}} + + + + )} + + {items.map(item => { + const itemProps = { + item, + isActive: item.isActive || (!!activeValue && activeValue === item.value), + } + return + })} + + + ) + } +} + +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 diff --git a/src/components/base/SideBar/SideBarListItem.js b/src/components/base/SideBar/SideBarListItem.js new file mode 100644 index 00000000..56d09ddf --- /dev/null +++ b/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), + 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 { + render() { + const { + item: { icon: Icon, label, desc, iconActiveColor, hasNotif, onClick }, + isActive, + } = this.props + return ( + + {!!Icon && } + + {typeof label === 'function' ? ( + label(this.props) + ) : ( + + {label} + + )} + {!!desc && desc(this.props)} + + {!!hasNotif && } + + ) + } +} + +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 diff --git a/src/components/base/SideBar/index.js b/src/components/base/SideBar/index.js new file mode 100644 index 00000000..0461a8d0 --- /dev/null +++ b/src/components/base/SideBar/index.js @@ -0,0 +1,3 @@ +// @flow + +export { default as SideBarList } from './SideBarList' diff --git a/src/components/base/SideBar/stories.js b/src/components/base/SideBar/stories.js new file mode 100644 index 00000000..ce7e0505 --- /dev/null +++ b/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: () => ( + + {'custom'} + {'render'} + + ), + icon: IconCurrencies, + iconActiveColor: '#3ca569', + }, + { + value: 'fifth', + label: 'Fifth', + icon: IconExclamationCircle, + iconActiveColor: '#0e76aa', + }, +] + +stories.add('SideBarList', () => ( + i.value), null], 'third')} + /> +)) diff --git a/src/components/base/Space.js b/src/components/base/Space.js new file mode 100644 index 00000000..a78a0af4 --- /dev/null +++ b/src/components/base/Space.js @@ -0,0 +1,5 @@ +import styled from 'styled-components' + +export default styled.div` + height: ${p => p.of}px; +` diff --git a/src/components/layout/Default.js b/src/components/layout/Default.js index 7e109f7b..cece3bb3 100644 --- a/src/components/layout/Default.js +++ b/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({