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
This commit is contained in:
Jaco Greeff 2017-02-24 14:37:56 +01:00 committed by GitHub
parent 5817cfdf41
commit a6ed3dc5dc
17 changed files with 378 additions and 268 deletions

View File

@ -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;
}

View File

@ -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,7 +47,6 @@ export default class AddDapps extends Component {
/>
}
>
<div className={ styles.container }>
<div className={ styles.warning } />
{
this.renderList(store.sortedLocal, store.displayApps,
@ -86,7 +84,6 @@ export default class AddDapps extends Component {
/>
)
}
</div>
</Portal>
);
}
@ -102,9 +99,11 @@ export default class AddDapps extends Component {
<div className={ styles.header }>{ header }</div>
<div className={ styles.byline }>{ byline }</div>
</div>
<SectionList
<SelectionList
isChecked={ this.isVisible }
items={ items }
noStretch
onSelectClick={ this.onSelect }
renderItem={ this.renderApp }
/>
</div>
@ -112,30 +111,27 @@ export default class AddDapps extends Component {
}
renderApp = (app) => {
const { store } = this.props;
const isVisible = store.displayApps[app.id].visible;
return (
<DappCard
app={ app }
key={ app.id }
/>
);
}
const onClick = () => {
if (isVisible) {
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);
}
};
return (
<DappCard
app={ app }
className={
isVisible
? styles.selected
: styles.unselected
}
key={ app.id }
onClick={ onClick }
>
<CheckIcon className={ styles.selectIcon } />
</DappCard>
);
}
}

View File

@ -15,51 +15,6 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.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;

View File

@ -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 {
/>
}
>
<div className={ styles.container }>
<SectionList
<SelectionList
items={ permissionStore.accounts }
noStretch
onDefaultClick={ this.onMakeDefault }
onSelectClick={ this.onSelect }
renderItem={ this.renderAccount }
/>
</div>
</Portal>
);
}
renderAccount = (account) => {
const { balances, permissionStore } = 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;
onMakeDefault = (account) => {
this.props.permissionStore.setDefaultAccount(account.address);
}
onSelect = (account) => {
this.props.permissionStore.selectAccount(account.address);
}
renderAccount = (account) => {
const { balances } = this.props;
const balance = balances[account.address];
return (
<div className={ styles.item }>
<AccountCard
account={ account }
balance={ balance }
className={ className }
onClick={ onSelect }
/>
<div className={ styles.overlay }>
{
account.checked && account.default
? <StarIcon />
: <StarOutlineIcon className={ styles.iconDisabled } onClick={ onMakeDefault } />
}
{
account.checked
? <CheckIcon onClick={ onSelect } />
: <CheckIcon className={ styles.iconDisabled } onClick={ onSelect } />
}
</div>
</div>
);
}
}

View File

@ -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 {
/>
}
>
<SectionList
items={ vaultAccounts }
noStretch
renderItem={ this.renderAccount }
selectedAccounts={ selectedAccounts }
/>
{ this.renderList(vaultAccounts, selectedAccounts) }
</Portal>
);
}
// 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 (
<SelectionList
isChecked={ this.isSelected }
items={ vaultAccounts }
noStretch
onSelectClick={ this.onSelect }
renderItem={ this.renderAccount }
/>
);
}
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 (
<div className={ styles.item }>
<AccountCard
account={ account }
balance={ balance }
className={
isSelected
? styles.selected
: styles.unselected
}
onClick={ onSelect }
/>
<div className={ styles.overlay }>
{
isSelected
? <CheckIcon onClick={ onSelect } />
: <CheckIcon className={ styles.iconDisabled } onClick={ onSelect } />
}
</div>
</div>
);
}
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();
}

View File

@ -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', () => {

View File

@ -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);

View File

@ -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 (
<div
key={ address }
tabIndex={ 0 }
className={ classes.join(' ') }
onClick={ this.onClick }
onFocus={ this.onFocus }
onKeyDown={ this.handleKeyDown }
{ ...props }
>
<div className={ styles.mainContainer }>
<div className={ styles.infoContainer }>

View File

@ -146,4 +146,8 @@
margin: 1em 0;
}
.account {
margin: 0.5em 0;
}
}

View File

@ -346,6 +346,7 @@ class AddressSelect extends Component {
<AccountCard
account={ account }
balance={ balance }
className={ styles.account }
key={ `account_${index}` }
onClick={ this.handleClick }
onFocus={ this.focusItem }

View File

@ -0,0 +1,17 @@
// 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 <http://www.gnu.org/licenses/>.
export default from './selectionList';

View File

@ -15,16 +15,25 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
/* 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;
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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 (
<SectionList
items={ items }
noStretch={ noStretch }
renderItem={ this.renderItem }
/>
);
}
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
? <StarIcon />
: <StarOutlineIcon className={ styles.iconDisabled } onClick={ makeDefault } />;
}
const classes = isSelected
? [styles.item, styles.selected]
: [styles.item, styles.unselected];
return (
<div className={ classes.join(' ') }>
<div
className={ styles.content }
onClick={ selectItem }
>
{ renderItem(item, index) }
</div>
<div className={ styles.overlay }>
{ defaultIcon }
{
isSelected
? <CheckIcon onClick={ selectItem } />
: <CheckIcon className={ styles.iconDisabled } onClick={ selectItem } />
}
</div>
</div>
);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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(
<SelectionList
items={ ITEMS }
noStretch='testNoStretch'
onDefaultClick={ onDefaultClick }
onSelectClick={ onSelectClick }
renderItem={ renderItem }
/>
);
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');
});
});
});
});

View File

@ -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';

View File

@ -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;
})

View File

@ -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
? (
<SectionList
<SelectionList
className={ styles.accountsSection }
items={ this.accountStore.accounts }
noStretch
onSelectClick={ this.onMakeDefault }
renderItem={ this.renderAccount }
/>
)
@ -344,31 +345,23 @@ class ParityBar extends Component {
);
}
renderAccount = (account) => {
const { balances } = this.props;
const balance = balances[account.address];
const makeDefaultAccount = () => {
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];
return (
<div
className={ styles.account }
onClick={ makeDefaultAccount }
>
<AccountCard
account={ account }
balance={ balance }
className={
account.default
? styles.selected
: styles.unselected
}
/>
</div>
);
}