From a6ed3dc5dcf92d395f740ba9466d25fb92e43d62 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 24 Feb 2017 14:37:56 +0100 Subject: [PATCH] Add SelectionList component to DRY up (#4639) * Added SelectionList component for selections * Use SelectionList in DappPermisions * AddDapps uses SelectionList * Fix AccountCard to consistent height * Convert Signer defaults to SelectionList * Subtle selection border * Convert VaultAccounts to SelectionList * Add tests for SectionList component * Apply scroll fixes from lates commit in #4621 * Remove unneeded logs * Remove extra div, fixing ParityBar overflow --- js/src/modals/AddDapps/addDapps.css | 27 ---- js/src/modals/AddDapps/addDapps.js | 120 +++++++++--------- .../DappPermissions/dappPermissions.css | 45 ------- .../modals/DappPermissions/dappPermissions.js | 70 ++++------ js/src/modals/VaultAccounts/vaultAccounts.js | 70 +++++----- .../VaultAccounts/vaultAccounts.spec.js | 4 +- js/src/ui/AccountCard/accountCard.css | 2 +- js/src/ui/AccountCard/accountCard.js | 8 +- .../ui/Form/AddressSelect/addressSelect.css | 4 + js/src/ui/Form/AddressSelect/addressSelect.js | 1 + js/src/ui/SelectionList/index.js | 17 +++ .../SelectionList/selectionList.css} | 43 ++++--- js/src/ui/SelectionList/selectionList.js | 93 ++++++++++++++ js/src/ui/SelectionList/selectionList.spec.js | 100 +++++++++++++++ js/src/ui/index.js | 1 + js/src/views/ParityBar/accountStore.js | 4 +- js/src/views/ParityBar/parityBar.js | 37 +++--- 17 files changed, 378 insertions(+), 268 deletions(-) create mode 100644 js/src/ui/SelectionList/index.js rename js/src/{modals/VaultAccounts/vaultAccounts.css => ui/SelectionList/selectionList.css} (75%) create mode 100644 js/src/ui/SelectionList/selectionList.js create mode 100644 js/src/ui/SelectionList/selectionList.spec.js diff --git a/js/src/modals/AddDapps/addDapps.css b/js/src/modals/AddDapps/addDapps.css index 1bca5b336..2c8779a84 100644 --- a/js/src/modals/AddDapps/addDapps.css +++ b/js/src/modals/AddDapps/addDapps.css @@ -19,10 +19,6 @@ flex-direction: column; } -.container { - overflow-y: auto; -} - .description { margin-top: .5em !important; } @@ -49,26 +45,3 @@ opacity: 0.75; } } - -.selectIcon { - position: absolute; - right: 0.5em; - top: 0.5em; -} - -.selected, -.unselected { - position: relative; -} - -.unselected { - background: rgba(0, 0, 0, 0.4) !important; - - .selectIcon { - opacity: 0.15; - } -} - -.selected { - background: rgba(255, 255, 255, 0.15) !important; -} diff --git a/js/src/modals/AddDapps/addDapps.js b/js/src/modals/AddDapps/addDapps.js index 9b1fcc760..871b4f505 100644 --- a/js/src/modals/AddDapps/addDapps.js +++ b/js/src/modals/AddDapps/addDapps.js @@ -18,8 +18,7 @@ import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { DappCard, Portal, SectionList } from '~/ui'; -import { CheckIcon } from '~/ui/Icons'; +import { DappCard, Portal, SelectionList } from '~/ui'; import styles from './addDapps.css'; @@ -48,45 +47,43 @@ export default class AddDapps extends Component { /> } > -
-
- { - this.renderList(store.sortedLocal, store.displayApps, - , - - ) - } - { - this.renderList(store.sortedBuiltin, store.displayApps, - , - - ) - } - { - this.renderList(store.sortedNetwork, store.displayApps, - , - - ) - } -
+
+ { + this.renderList(store.sortedLocal, store.displayApps, + , + + ) + } + { + this.renderList(store.sortedBuiltin, store.displayApps, + , + + ) + } + { + this.renderList(store.sortedNetwork, store.displayApps, + , + + ) + } ); } @@ -102,9 +99,11 @@ export default class AddDapps extends Component {
{ header }
{ byline }
-
@@ -112,30 +111,27 @@ export default class AddDapps extends Component { } renderApp = (app) => { - const { store } = this.props; - const isVisible = store.displayApps[app.id].visible; - - const onClick = () => { - if (isVisible) { - store.hideApp(app.id); - } else { - store.showApp(app.id); - } - }; - return ( - - + /> ); } + + isVisible = (app) => { + const { store } = this.props; + + return store.displayApps[app.id].visible; + } + + onSelect = (app) => { + const { store } = this.props; + + if (this.isVisible(app)) { + store.hideApp(app.id); + } else { + store.showApp(app.id); + } + } } diff --git a/js/src/modals/DappPermissions/dappPermissions.css b/js/src/modals/DappPermissions/dappPermissions.css index 3f2559bb3..44d901572 100644 --- a/js/src/modals/DappPermissions/dappPermissions.css +++ b/js/src/modals/DappPermissions/dappPermissions.css @@ -15,51 +15,6 @@ /* along with Parity. If not, see . */ -.container { - overflow-y: auto; -} - -.item { - display: flex; - flex: 1; - position: relative; - - .overlay { - position: absolute; - right: 0.5em; - top: 0.5em; - } -} - -.selected, -.unselected { - margin-bottom: 0.25em; - width: 100%; - - &:focus { - outline: none; - } -} - -.unselected { - background: rgba(0, 0, 0, 0.4) !important; -} - -.selected { - background: rgba(255, 255, 255, 0.15) !important; - - &.default { - background: rgba(255, 255, 255, 0.35) !important; - } -} - -.unselected { -} - -.iconDisabled { - opacity: 0.15; -} - .legend { opacity: 0.75; diff --git a/js/src/modals/DappPermissions/dappPermissions.js b/js/src/modals/DappPermissions/dappPermissions.js index 410dcf4b7..ddb4b040d 100644 --- a/js/src/modals/DappPermissions/dappPermissions.js +++ b/js/src/modals/DappPermissions/dappPermissions.js @@ -19,8 +19,8 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { AccountCard, Portal, SectionList } from '~/ui'; -import { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons'; +import { AccountCard, Portal, SelectionList } from '~/ui'; +import { CheckIcon, StarIcon } from '~/ui/Icons'; import styles from './dappPermissions.css'; @@ -61,60 +61,34 @@ class DappPermissions extends Component { /> } > -
- -
+ ); } + onMakeDefault = (account) => { + this.props.permissionStore.setDefaultAccount(account.address); + } + + onSelect = (account) => { + this.props.permissionStore.selectAccount(account.address); + } + renderAccount = (account) => { - const { balances, permissionStore } = this.props; + const { balances } = this.props; const balance = balances[account.address]; - const onMakeDefault = () => { - permissionStore.setDefaultAccount(account.address); - }; - - const onSelect = () => { - permissionStore.selectAccount(account.address); - }; - - let className; - - if (account.checked) { - className = account.default - ? `${styles.selected} ${styles.default}` - : styles.selected; - } else { - className = styles.unselected; - } - return ( -
- -
- { - account.checked && account.default - ? - : - } - { - account.checked - ? - : - } -
-
+ ); } } diff --git a/js/src/modals/VaultAccounts/vaultAccounts.js b/js/src/modals/VaultAccounts/vaultAccounts.js index 833b5805e..748969ffb 100644 --- a/js/src/modals/VaultAccounts/vaultAccounts.js +++ b/js/src/modals/VaultAccounts/vaultAccounts.js @@ -22,11 +22,9 @@ import { bindActionCreators } from 'redux'; import { newError } from '~/redux/actions'; import { personalAccountsInfo } from '~/redux/providers/personalActions'; -import { AccountCard, Button, Portal, SectionList } from '~/ui'; +import { AccountCard, Button, Portal, SelectionList } from '~/ui'; import { CancelIcon, CheckIcon } from '~/ui/Icons'; -import styles from './vaultAccounts.css'; - @observer class VaultAccounts extends Component { static contextTypes = { @@ -92,55 +90,47 @@ class VaultAccounts extends Component { /> } > - + { this.renderList(vaultAccounts, selectedAccounts) } ); } - // TODO: There are a lot of similarities between the dapp permissions selector - // (although that has defaults) and this one. A genrerix multi-select component - // would be applicable going forward. (Originals passed in, new selections back) + renderList (vaultAccounts) { + return ( + + ); + } + renderAccount = (account) => { const { balances } = this.props; - const { vaultName, selectedAccounts } = this.props.vaultStore; const balance = balances[account.address]; - const isInVault = account.meta.vault === vaultName; - const isSelected = isInVault - ? !selectedAccounts[account.address] - : selectedAccounts[account.address]; - - const onSelect = () => { - this.props.vaultStore.toggleSelectedAccount(account.address); - }; return ( -
- -
- { - isSelected - ? - : - } -
-
+ ); } + isSelected = (account) => { + const { vaultName, selectedAccounts } = this.props.vaultStore; + + return account.meta.vault === vaultName + ? !selectedAccounts[account.address] + : selectedAccounts[account.address]; + } + + onSelect = (account) => { + this.props.vaultStore.toggleSelectedAccount(account.address); + } + onClose = () => { this.props.vaultStore.closeAccountsModal(); } diff --git a/js/src/modals/VaultAccounts/vaultAccounts.spec.js b/js/src/modals/VaultAccounts/vaultAccounts.spec.js index 2dc8e3219..6a0d43b6e 100644 --- a/js/src/modals/VaultAccounts/vaultAccounts.spec.js +++ b/js/src/modals/VaultAccounts/vaultAccounts.spec.js @@ -130,11 +130,11 @@ describe('modals/VaultAccounts', () => { }); describe('components', () => { - describe('SectionList', () => { + describe('SelectionList', () => { let sectionList; beforeEach(() => { - sectionList = component.find('SectionList'); + sectionList = component.find('SelectionList'); }); it('has the filtered accounts', () => { diff --git a/js/src/ui/AccountCard/accountCard.css b/js/src/ui/AccountCard/accountCard.css index ae382374c..44012cab0 100644 --- a/js/src/ui/AccountCard/accountCard.css +++ b/js/src/ui/AccountCard/accountCard.css @@ -20,7 +20,7 @@ background-color: rgba(0, 0, 0, 0.8); display: flex; flex-direction: row; - margin: 0.5em 0; + height: 100%; overflow: hidden; transition: transform ease-out 0.1s; transform: scale(1); diff --git a/js/src/ui/AccountCard/accountCard.js b/js/src/ui/AccountCard/accountCard.js index 9167f38e6..6a6ebb22c 100644 --- a/js/src/ui/AccountCard/accountCard.js +++ b/js/src/ui/AccountCard/accountCard.js @@ -39,7 +39,7 @@ export default class AccountCard extends Component { }; render () { - const { account, balance, className } = this.props; + const { account, balance, className, onFocus } = this.props; const { copied } = this.state; const { address, description, meta = {}, name } = account; const { tags = [] } = meta; @@ -49,14 +49,18 @@ export default class AccountCard extends Component { classes.push(styles.copied); } + const props = onFocus + ? { tabIndex: 0 } + : {}; + return (
diff --git a/js/src/ui/Form/AddressSelect/addressSelect.css b/js/src/ui/Form/AddressSelect/addressSelect.css index 839c025b5..84afbd15a 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.css +++ b/js/src/ui/Form/AddressSelect/addressSelect.css @@ -146,4 +146,8 @@ margin: 1em 0; } + + .account { + margin: 0.5em 0; + } } diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index 2f47484b2..07c133d7c 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -346,6 +346,7 @@ class AddressSelect extends Component { . + +export default from './selectionList'; diff --git a/js/src/modals/VaultAccounts/vaultAccounts.css b/js/src/ui/SelectionList/selectionList.css similarity index 75% rename from js/src/modals/VaultAccounts/vaultAccounts.css rename to js/src/ui/SelectionList/selectionList.css index 1960376f3..eee61dbb5 100644 --- a/js/src/modals/VaultAccounts/vaultAccounts.css +++ b/js/src/ui/SelectionList/selectionList.css @@ -15,16 +15,25 @@ /* along with Parity. If not, see . */ -/* TODO: These overlap with DappPermissions now, make DRY */ -/* (selection component or just styles?) */ -.iconDisabled { - opacity: 0.15; -} - .item { display: flex; flex: 1; + height: 100%; position: relative; + width: 100%; + + &:hover { + box-shadow: inset 0 0 0 2px rgb(255, 255, 255); + } + + .content { + height: 100%; + width: 100%; + + &:hover { + background: rgba(255, 255, 255, 0.25); + } + } .overlay { position: absolute; @@ -33,16 +42,16 @@ } } -.selected, -.unselected { - margin-bottom: 0.25em; - width: 100%; - - &:focus { - outline: none; - } -} - .selected { - background: rgba(255, 255, 255, 0.15) !important; + box-shadow: inset 0 0 0 2px rgb(255, 255, 255); + filter: none; +} + +.unselected { + filter: grayscale(100%); + opacity: 0.5; +} + +.iconDisabled { + opacity: 0.15; } diff --git a/js/src/ui/SelectionList/selectionList.js b/js/src/ui/SelectionList/selectionList.js new file mode 100644 index 000000000..2293cf9c3 --- /dev/null +++ b/js/src/ui/SelectionList/selectionList.js @@ -0,0 +1,93 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; + +import { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons'; +import SectionList from '~/ui/SectionList'; +import { arrayOrObjectProptype } from '~/util/proptypes'; + +import styles from './selectionList.css'; + +export default class SelectionList extends Component { + static propTypes = { + isChecked: PropTypes.func, + items: arrayOrObjectProptype().isRequired, + noStretch: PropTypes.bool, + onDefaultClick: PropTypes.func, + onSelectClick: PropTypes.func.isRequired, + renderItem: PropTypes.func.isRequired + } + + render () { + const { items, noStretch } = this.props; + + return ( + + ); + } + + renderItem = (item, index) => { + const { isChecked, onDefaultClick, onSelectClick, renderItem } = this.props; + const isSelected = isChecked + ? isChecked(item) + : item.checked; + + const makeDefault = () => { + onDefaultClick(item); + return false; + }; + const selectItem = () => { + onSelectClick(item); + return false; + }; + + let defaultIcon = null; + + if (onDefaultClick) { + defaultIcon = isSelected && item.default + ? + : ; + } + + const classes = isSelected + ? [styles.item, styles.selected] + : [styles.item, styles.unselected]; + + return ( +
+
+ { renderItem(item, index) } +
+
+ { defaultIcon } + { + isSelected + ? + : + } +
+
+ ); + } +} diff --git a/js/src/ui/SelectionList/selectionList.spec.js b/js/src/ui/SelectionList/selectionList.spec.js new file mode 100644 index 000000000..4dfaed396 --- /dev/null +++ b/js/src/ui/SelectionList/selectionList.spec.js @@ -0,0 +1,100 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import SelectionList from './'; + +const ITEMS = ['A', 'B', 'C']; + +let component; +let instance; +let renderItem; +let onDefaultClick; +let onSelectClick; + +function render (props = {}) { + renderItem = sinon.stub(); + onDefaultClick = sinon.stub(); + onSelectClick = sinon.stub(); + + component = shallow( + + ); + instance = component.instance(); + + return component; +} + +describe('ui/SelectionList', () => { + beforeEach(() => { + render(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + describe('SectionList', () => { + let section; + + beforeEach(() => { + section = component.find('SectionList'); + }); + + it('renders the SectionList', () => { + expect(section.get(0)).to.be.ok; + }); + + it('passes the items through', () => { + expect(section.props().items).to.deep.equal(ITEMS); + }); + + it('passes internal render method', () => { + expect(section.props().renderItem).to.equal(instance.renderItem); + }); + + it('passes noStretch prop through', () => { + expect(section.props().noStretch).to.equal('testNoStretch'); + }); + }); + + describe('instance methods', () => { + describe('renderItem', () => { + let result; + + beforeEach(() => { + result = instance.renderItem('testItem', 'testIndex'); + }); + + it('renders', () => { + expect(result).to.be.ok; + }); + + it('calls into parent renderItem', () => { + expect(renderItem).to.have.been.calledWith('testItem', 'testIndex'); + }); + }); + }); +}); diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 54d7ba5b8..db640b1a5 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -46,6 +46,7 @@ export ParityBackground from './ParityBackground'; export Portal from './Portal'; export QrCode from './QrCode'; export SectionList from './SectionList'; +export SelectionList from './SelectionList'; export ShortenedHash from './ShortenedHash'; export SignerIcon from './SignerIcon'; export Tags from './Tags'; diff --git a/js/src/views/ParityBar/accountStore.js b/js/src/views/ParityBar/accountStore.js index 4c2736864..f13881df0 100644 --- a/js/src/views/ParityBar/accountStore.js +++ b/js/src/views/ParityBar/accountStore.js @@ -37,7 +37,7 @@ export default class AccountStore { @action setDefaultAccount = (defaultAccount) => { transaction(() => { this.accounts = this.accounts.map((account) => { - account.default = account.address === defaultAccount; + account.checked = account.address === defaultAccount; return account; }); @@ -90,7 +90,7 @@ export default class AccountStore { const account = accounts[address]; account.address = address; - account.default = address === this.defaultAccount; + account.checked = address === this.defaultAccount; return account; }) diff --git a/js/src/views/ParityBar/parityBar.js b/js/src/views/ParityBar/parityBar.js index 7f95cd6d5..3d34f430f 100644 --- a/js/src/views/ParityBar/parityBar.js +++ b/js/src/views/ParityBar/parityBar.js @@ -24,7 +24,7 @@ import { connect } from 'react-redux'; import store from 'store'; import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg'; -import { AccountCard, Badge, Button, ContainerTitle, IdentityIcon, ParityBackground, SectionList } from '~/ui'; +import { AccountCard, Badge, Button, ContainerTitle, IdentityIcon, ParityBackground, SelectionList } from '~/ui'; import { CancelIcon, FingerprintIcon } from '~/ui/Icons'; import DappsStore from '~/views/Dapps/dappsStore'; import { Embedded as Signer } from '~/views/Signer'; @@ -328,10 +328,11 @@ class ParityBar extends Component { { displayType === DISPLAY_ACCOUNTS ? ( - ) @@ -344,31 +345,23 @@ class ParityBar extends Component { ); } + onMakeDefault = (account) => { + this.toggleAccountsDisplay(); + + return this.accountStore + .makeDefaultAccount(account.address) + .then(() => this.accountStore.loadAccounts()); + } + renderAccount = (account) => { const { balances } = this.props; const balance = balances[account.address]; - const makeDefaultAccount = () => { - this.toggleAccountsDisplay(); - return this.accountStore - .makeDefaultAccount(account.address) - .then(() => this.accountStore.loadAccounts()); - }; return ( -
- -
+ ); }