Dapp Account Selection & Defaults (#4355)
* Manage default accounts * Portal * Portal * Allow Portal to be used in as both top-level and popover * modal/popover variable naming * Move to Portal * export Portal in ~/ui * WIP * Tags handle empty values * Export AccountCard in ~/ui * Allow ETH-only & zero display * Use ui/Balance for balance display * Add tests for Balance & Tags component availability * WIP * Default overlay display to block (not flex) * Revert block * WIP * Add className, optional handlers only * WIP * Properly handle optional onKeyDown * Selection updated * Align margins * Remove old code * Remove debug logging * TransitionGroup for animations * No anim * Cleanups * Revert addons removal * Fix tests * Pr gumbles
This commit is contained in:
parent
12aadc3e2a
commit
a935a04449
@ -15,33 +15,60 @@
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.modal {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 1.5em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
.info {
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.address {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 0.5em;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 0.5em 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
right: 0.5em;
|
||||
top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.selected, .unselected {
|
||||
margin-bottom: 0.25em;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.unselected {
|
||||
background: rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
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;
|
||||
margin-top: 1em;
|
||||
|
||||
span {
|
||||
line-height: 24px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
@ -14,14 +14,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Checkbox } from 'material-ui';
|
||||
import { List, ListItem } from 'material-ui/List';
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Button, IdentityIcon, Modal } from '~/ui';
|
||||
import { DoneIcon } from '~/ui/Icons';
|
||||
import { AccountCard, ContainerTitle, Portal, SectionList } from '~/ui';
|
||||
import { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons';
|
||||
|
||||
import styles from './dappPermissions.css';
|
||||
|
||||
@ -39,79 +37,81 @@ export default class DappPermissions extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
actions={ [
|
||||
<Button
|
||||
icon={ <DoneIcon /> }
|
||||
key='done'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='dapps.permissions.button.done'
|
||||
defaultMessage='Done'
|
||||
/>
|
||||
}
|
||||
onClick={ store.closeModal }
|
||||
/>
|
||||
] }
|
||||
compact
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='dapps.permissions.label'
|
||||
defaultMessage='visible dapp accounts'
|
||||
/>
|
||||
}
|
||||
visible
|
||||
<Portal
|
||||
className={ styles.modal }
|
||||
onClose={ store.closeModal }
|
||||
open
|
||||
>
|
||||
<List>
|
||||
{ this.renderListItems() }
|
||||
</List>
|
||||
</Modal>
|
||||
<ContainerTitle
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='dapps.permissions.label'
|
||||
defaultMessage='visible dapp accounts'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className={ styles.container }>
|
||||
<SectionList
|
||||
items={ store.accounts }
|
||||
noStretch
|
||||
renderItem={ this.renderAccount }
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.legend }>
|
||||
<FormattedMessage
|
||||
id='dapps.permissions.description'
|
||||
defaultMessage='{activeIcon} account is available to application, {defaultIcon} account is the default account'
|
||||
values={ {
|
||||
activeIcon: <CheckIcon />,
|
||||
defaultIcon: <StarIcon />
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
renderListItems () {
|
||||
renderAccount = (account) => {
|
||||
const { store } = this.props;
|
||||
|
||||
return store.accounts.map((account) => {
|
||||
const onCheck = () => {
|
||||
store.selectAccount(account.address);
|
||||
};
|
||||
const onMakeDefault = () => {
|
||||
store.setDefaultAccount(account.address);
|
||||
};
|
||||
|
||||
// TODO: Once new modal & account selection is in, this should be updated
|
||||
// to conform to the new (as of this code WIP) look & feel for selection.
|
||||
// For now in the current/old style, not as pretty but consistent.
|
||||
return (
|
||||
<ListItem
|
||||
className={
|
||||
account.checked
|
||||
? styles.selected
|
||||
: styles.unselected
|
||||
}
|
||||
key={ account.address }
|
||||
leftCheckbox={
|
||||
<Checkbox
|
||||
checked={ account.checked }
|
||||
onCheck={ onCheck }
|
||||
/>
|
||||
}
|
||||
primaryText={
|
||||
<div className={ styles.item }>
|
||||
<IdentityIcon address={ account.address } />
|
||||
<div className={ styles.info }>
|
||||
<h3 className={ styles.name }>
|
||||
{ account.name }
|
||||
</h3>
|
||||
<div className={ styles.address }>
|
||||
{ account.address }
|
||||
</div>
|
||||
<div className={ styles.description }>
|
||||
{ account.description }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
const onSelect = () => {
|
||||
store.selectAccount(account.address);
|
||||
};
|
||||
|
||||
let className;
|
||||
|
||||
if (account.checked) {
|
||||
className = account.default
|
||||
? `${styles.selected} ${styles.default}`
|
||||
: styles.selected;
|
||||
} else {
|
||||
className = styles.unselected;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.item }>
|
||||
<AccountCard
|
||||
account={ account }
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -33,13 +33,13 @@ describe('modals/DappPermissions', () => {
|
||||
|
||||
it('does not render the modal with modalOpen = false', () => {
|
||||
expect(
|
||||
renderShallow({ modalOpen: false }).find('Connect(Modal)')
|
||||
renderShallow({ modalOpen: false }).find('Portal')
|
||||
).to.have.length(0);
|
||||
});
|
||||
|
||||
it('does render the modal with modalOpen = true', () => {
|
||||
expect(
|
||||
renderShallow({ modalOpen: true, accounts: [] }).find('Connect(Modal)')
|
||||
renderShallow({ modalOpen: true, accounts: [] }).find('Portal')
|
||||
).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
@ -29,12 +29,17 @@ export default class Store {
|
||||
|
||||
@action closeModal = () => {
|
||||
transaction(() => {
|
||||
const accounts = this.accounts
|
||||
.filter((account) => account.checked)
|
||||
.map((account) => account.address);
|
||||
let addresses = null;
|
||||
const checkedAccounts = this.accounts.filter((account) => account.checked);
|
||||
|
||||
if (checkedAccounts.length) {
|
||||
addresses = checkedAccounts.filter((account) => account.default)
|
||||
.concat(checkedAccounts.filter((account) => !account.default))
|
||||
.map((account) => account.address);
|
||||
}
|
||||
|
||||
this.modalOpen = false;
|
||||
this.updateWhitelist(accounts.length === this.accounts.length ? null : accounts);
|
||||
this.updateWhitelist(addresses);
|
||||
});
|
||||
}
|
||||
|
||||
@ -42,12 +47,15 @@ export default class Store {
|
||||
transaction(() => {
|
||||
this.accounts = Object
|
||||
.values(accounts)
|
||||
.map((account) => {
|
||||
.map((account, index) => {
|
||||
return {
|
||||
address: account.address,
|
||||
checked: this.whitelist
|
||||
? this.whitelist.includes(account.address)
|
||||
: true,
|
||||
default: this.whitelist
|
||||
? this.whitelist[0] === account.address
|
||||
: index === 0,
|
||||
description: account.meta.description,
|
||||
name: account.name
|
||||
};
|
||||
@ -57,9 +65,31 @@ export default class Store {
|
||||
}
|
||||
|
||||
@action selectAccount = (address) => {
|
||||
transaction(() => {
|
||||
this.accounts = this.accounts.map((account) => {
|
||||
if (account.address === address) {
|
||||
account.checked = !account.checked;
|
||||
account.default = false;
|
||||
}
|
||||
|
||||
return account;
|
||||
});
|
||||
|
||||
this.setDefaultAccount((
|
||||
this.accounts.find((account) => account.default) ||
|
||||
this.accounts.find((account) => account.checked) ||
|
||||
{}
|
||||
).address);
|
||||
});
|
||||
}
|
||||
|
||||
@action setDefaultAccount = (address) => {
|
||||
this.accounts = this.accounts.map((account) => {
|
||||
if (account.address === address) {
|
||||
account.checked = !account.checked;
|
||||
account.checked = true;
|
||||
account.default = true;
|
||||
} else if (account.default) {
|
||||
account.default = false;
|
||||
}
|
||||
|
||||
return account;
|
||||
|
@ -23,21 +23,25 @@ const ACCOUNTS = {
|
||||
'456': { address: '456', name: '456', meta: { description: '456' } },
|
||||
'789': { address: '789', name: '789', meta: { description: '789' } }
|
||||
};
|
||||
const WHITELIST = ['123', '456'];
|
||||
const WHITELIST = ['456', '789'];
|
||||
|
||||
let api;
|
||||
let store;
|
||||
|
||||
function create () {
|
||||
api = {
|
||||
parity: {
|
||||
getNewDappsWhitelist: sinon.stub().resolves(WHITELIST),
|
||||
setNewDappsWhitelist: sinon.stub().resolves(true)
|
||||
}
|
||||
};
|
||||
|
||||
store = new Store(api);
|
||||
}
|
||||
|
||||
describe('modals/DappPermissions/store', () => {
|
||||
let api;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
api = {
|
||||
parity: {
|
||||
getNewDappsWhitelist: sinon.stub().resolves(WHITELIST),
|
||||
setNewDappsWhitelist: sinon.stub().resolves(true)
|
||||
}
|
||||
};
|
||||
|
||||
store = new Store(api);
|
||||
create();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
@ -51,49 +55,71 @@ describe('modals/DappPermissions/store', () => {
|
||||
});
|
||||
|
||||
describe('@actions', () => {
|
||||
describe('openModal', () => {
|
||||
beforeEach(() => {
|
||||
store.openModal(ACCOUNTS);
|
||||
});
|
||||
beforeEach(() => {
|
||||
store.openModal(ACCOUNTS);
|
||||
});
|
||||
|
||||
describe('openModal', () => {
|
||||
it('sets the modalOpen status', () => {
|
||||
expect(store.modalOpen).to.be.true;
|
||||
});
|
||||
|
||||
it('sets accounts with checked interfaces', () => {
|
||||
expect(store.accounts.peek()).to.deep.equal([
|
||||
{ address: '123', name: '123', description: '123', checked: true },
|
||||
{ address: '456', name: '456', description: '456', checked: true },
|
||||
{ address: '789', name: '789', description: '789', checked: false }
|
||||
{ address: '123', name: '123', description: '123', default: false, checked: false },
|
||||
{ address: '456', name: '456', description: '456', default: true, checked: true },
|
||||
{ address: '789', name: '789', description: '789', default: false, checked: true }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeModal', () => {
|
||||
beforeEach(() => {
|
||||
store.openModal(ACCOUNTS);
|
||||
store.selectAccount('789');
|
||||
store.setDefaultAccount('789');
|
||||
store.closeModal();
|
||||
});
|
||||
|
||||
it('calls setNewDappsWhitelist', () => {
|
||||
expect(api.parity.setNewDappsWhitelist).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('has the default account in first position', () => {
|
||||
expect(api.parity.setNewDappsWhitelist).to.have.been.calledWith(['789', '456']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectAccount', () => {
|
||||
beforeEach(() => {
|
||||
store.openModal(ACCOUNTS);
|
||||
store.selectAccount('123');
|
||||
store.selectAccount('789');
|
||||
});
|
||||
|
||||
it('unselects previous selected accounts', () => {
|
||||
expect(store.accounts.find((account) => account.address === '123').checked).to.be.false;
|
||||
expect(store.accounts.find((account) => account.address === '123').checked).to.be.true;
|
||||
});
|
||||
|
||||
it('selects previous unselected accounts', () => {
|
||||
expect(store.accounts.find((account) => account.address === '789').checked).to.be.true;
|
||||
expect(store.accounts.find((account) => account.address === '789').checked).to.be.false;
|
||||
});
|
||||
|
||||
it('sets a new default when default was unselected', () => {
|
||||
store.selectAccount('456');
|
||||
expect(store.accounts.find((account) => account.address === '456').default).to.be.false;
|
||||
expect(store.accounts.find((account) => account.address === '123').default).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('setDefaultAccount', () => {
|
||||
beforeEach(() => {
|
||||
store.setDefaultAccount('789');
|
||||
});
|
||||
|
||||
it('unselects previous default', () => {
|
||||
expect(store.accounts.find((account) => account.address === '456').default).to.be.false;
|
||||
});
|
||||
|
||||
it('selects new default', () => {
|
||||
expect(store.accounts.find((account) => account.address === '789').default).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -38,6 +38,9 @@ import RefreshIcon from 'material-ui/svg-icons/action/autorenew';
|
||||
import SaveIcon from 'material-ui/svg-icons/content/save';
|
||||
import SendIcon from 'material-ui/svg-icons/content/send';
|
||||
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
|
||||
import StarCircleIcon from 'material-ui/svg-icons/action/stars';
|
||||
import StarIcon from 'material-ui/svg-icons/toggle/star';
|
||||
import StarOutlineIcon from 'material-ui/svg-icons/toggle/star-border';
|
||||
import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
|
||||
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
|
||||
import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
|
||||
@ -67,6 +70,9 @@ export {
|
||||
SaveIcon,
|
||||
SendIcon,
|
||||
SnoozeIcon,
|
||||
StarIcon,
|
||||
StarCircleIcon,
|
||||
StarOutlineIcon,
|
||||
VerifyIcon,
|
||||
VisibleIcon,
|
||||
VpnIcon
|
||||
|
@ -33,23 +33,13 @@ $popoverTop: 20vh;
|
||||
$popoverZ: 3600;
|
||||
|
||||
.backOverlay {
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
opacity: 0;
|
||||
background-color: rgba(255, 255, 255, 0.35);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform-origin: 100% 0;
|
||||
transition-duration: 0.25s;
|
||||
transition-property: opacity, z-index;
|
||||
transition-timing-function: ease-out;
|
||||
z-index: -10;
|
||||
|
||||
&.expanded {
|
||||
opacity: 1;
|
||||
z-index: $modalBackZ;
|
||||
}
|
||||
z-index: $modalBackZ;
|
||||
}
|
||||
|
||||
.parityBackground {
|
||||
@ -59,21 +49,14 @@ $popoverZ: 3600;
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0.25;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
padding: 1.5em;
|
||||
position: fixed;
|
||||
transform-origin: 100% 0;
|
||||
transition-duration: 0.25s;
|
||||
transition-property: opacity, z-index;
|
||||
transition-timing-function: ease-out;
|
||||
z-index: -10;
|
||||
|
||||
* {
|
||||
min-width: 0;
|
||||
@ -84,6 +67,7 @@ $popoverZ: 3600;
|
||||
left: $modalLeft;
|
||||
right: $modalRight;
|
||||
top: $modalTop;
|
||||
z-index: $modalZ;
|
||||
}
|
||||
|
||||
&.popover {
|
||||
@ -91,18 +75,7 @@ $popoverZ: 3600;
|
||||
top: $popoverTop;
|
||||
height: calc(100vh - $popoverTop - $popoverBottom);
|
||||
width: calc(100vw - $popoverLeft - $popoverRight);
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
opacity: 1;
|
||||
|
||||
&.popover {
|
||||
z-index: $popoverZ;
|
||||
}
|
||||
|
||||
&.modal {
|
||||
z-index: $modalZ;
|
||||
}
|
||||
z-index: $popoverZ;
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,9 +84,6 @@ $popoverZ: 3600;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 0.5rem;
|
||||
transition-duration: 0.25s;
|
||||
transition-property: opacity;
|
||||
transition-timing-function: ease-out;
|
||||
z-index: 100;
|
||||
|
||||
&, * {
|
||||
|
@ -35,40 +35,11 @@ export default class Portal extends Component {
|
||||
onKeyDown: PropTypes.func
|
||||
};
|
||||
|
||||
state = {
|
||||
expanded: false
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.props.open !== nextProps.open) {
|
||||
const opening = nextProps.open;
|
||||
const closing = !opening;
|
||||
|
||||
if (opening) {
|
||||
return this.setState({ expanded: true });
|
||||
}
|
||||
|
||||
if (closing) {
|
||||
return this.setState({ expanded: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, className, isChildModal } = this.props;
|
||||
const { expanded } = this.state;
|
||||
const backClasses = [ styles.backOverlay ];
|
||||
const classes = [
|
||||
styles.overlay,
|
||||
isChildModal
|
||||
? styles.popover
|
||||
: styles.modal,
|
||||
className
|
||||
];
|
||||
const { children, className, isChildModal, open } = this.props;
|
||||
|
||||
if (expanded) {
|
||||
classes.push(styles.expanded);
|
||||
backClasses.push(styles.expanded);
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -77,17 +48,33 @@ export default class Portal extends Component {
|
||||
onClose={ this.handleClose }
|
||||
>
|
||||
<div
|
||||
className={ backClasses.join(' ') }
|
||||
className={ styles.backOverlay }
|
||||
onClick={ this.handleClose }
|
||||
>
|
||||
<div
|
||||
className={ classes.join(' ') }
|
||||
className={
|
||||
[
|
||||
styles.overlay,
|
||||
isChildModal
|
||||
? styles.popover
|
||||
: styles.modal,
|
||||
className
|
||||
].join(' ')
|
||||
}
|
||||
onClick={ this.stopEvent }
|
||||
onKeyDown={ this.handleKeyDown }
|
||||
>
|
||||
<EventListener
|
||||
target='window'
|
||||
onKeyUp={ this.handleKeyUp }
|
||||
/>
|
||||
<ParityBackground className={ styles.parityBackground } />
|
||||
{ this.renderBindings() }
|
||||
{ this.renderCloseIcon() }
|
||||
<div
|
||||
className={ styles.closeIcon }
|
||||
onClick={ this.handleClose }
|
||||
>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
@ -95,38 +82,6 @@ export default class Portal extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderBindings () {
|
||||
const { expanded } = this.state;
|
||||
|
||||
if (!expanded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EventListener
|
||||
target='window'
|
||||
onKeyUp={ this.handleKeyUp }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderCloseIcon () {
|
||||
const { expanded } = this.state;
|
||||
|
||||
if (!expanded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ styles.closeIcon }
|
||||
onClick={ this.handleClose }
|
||||
>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
stopEvent = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -39,8 +39,8 @@
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 0 1 33.33%;
|
||||
height: 100%;
|
||||
opacity: 0.75;
|
||||
padding: 0.25em;
|
||||
transition: all 0.75s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
@ -56,7 +56,6 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
flex: 0 0 50%;
|
||||
opacity: 1;
|
||||
z-index: 100;
|
||||
|
||||
@ -67,6 +66,13 @@
|
||||
& [data-hover="show"] {
|
||||
}
|
||||
}
|
||||
|
||||
&.stretch-on:hover {
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
|
||||
&.stretch-off:hover {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,8 +31,13 @@ export default class SectionList extends Component {
|
||||
className: PropTypes.string,
|
||||
items: arrayOrObjectProptype().isRequired,
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
noStretch: PropTypes.bool,
|
||||
overlay: nodeOrStringProptype()
|
||||
}
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
noStretch: false
|
||||
};
|
||||
|
||||
render () {
|
||||
const { className, items } = this.props;
|
||||
@ -75,7 +80,7 @@ export default class SectionList extends Component {
|
||||
}
|
||||
|
||||
renderItem = (item, index) => {
|
||||
const { renderItem } = this.props;
|
||||
const { noStretch, renderItem } = this.props;
|
||||
|
||||
// NOTE: Any children that is to be showed or hidden (depending on hover state)
|
||||
// should have the data-hover="show|hide" attributes. For the current implementation
|
||||
@ -85,7 +90,10 @@ export default class SectionList extends Component {
|
||||
// CSS-only solution to let the browser do all the work via selectors.
|
||||
return (
|
||||
<div
|
||||
className={ styles.item }
|
||||
className={ [
|
||||
styles.item,
|
||||
styles[`stretch-${noStretch ? 'off' : 'on'}`]
|
||||
].join(' ') }
|
||||
key={ `item_${index}` }
|
||||
>
|
||||
{ renderItem(item, index) }
|
||||
|
Loading…
Reference in New Issue
Block a user