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/>.
|
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
.info {
|
display: flex;
|
||||||
display: inline-block;
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.address {
|
.overlay {
|
||||||
opacity: 0.75;
|
position: absolute;
|
||||||
}
|
right: 0.5em;
|
||||||
|
top: 0.5em;
|
||||||
.description {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected, .unselected {
|
.selected, .unselected {
|
||||||
margin-bottom: 0.25em;
|
margin-bottom: 0.25em;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselected {
|
||||||
|
background: rgba(0, 0, 0, 0.4) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.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 {
|
.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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// 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 { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Button, IdentityIcon, Modal } from '~/ui';
|
import { AccountCard, ContainerTitle, Portal, SectionList } from '~/ui';
|
||||||
import { DoneIcon } from '~/ui/Icons';
|
import { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons';
|
||||||
|
|
||||||
import styles from './dappPermissions.css';
|
import styles from './dappPermissions.css';
|
||||||
|
|
||||||
@ -39,79 +37,81 @@ export default class DappPermissions extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Portal
|
||||||
actions={ [
|
className={ styles.modal }
|
||||||
<Button
|
onClose={ store.closeModal }
|
||||||
icon={ <DoneIcon /> }
|
open
|
||||||
key='done'
|
>
|
||||||
label={
|
<ContainerTitle
|
||||||
<FormattedMessage
|
|
||||||
id='dapps.permissions.button.done'
|
|
||||||
defaultMessage='Done'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={ store.closeModal }
|
|
||||||
/>
|
|
||||||
] }
|
|
||||||
compact
|
|
||||||
title={
|
title={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='dapps.permissions.label'
|
id='dapps.permissions.label'
|
||||||
defaultMessage='visible dapp accounts'
|
defaultMessage='visible dapp accounts'
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
visible
|
/>
|
||||||
>
|
<div className={ styles.container }>
|
||||||
<List>
|
<SectionList
|
||||||
{ this.renderListItems() }
|
items={ store.accounts }
|
||||||
</List>
|
noStretch
|
||||||
</Modal>
|
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;
|
const { store } = this.props;
|
||||||
|
|
||||||
return store.accounts.map((account) => {
|
const onMakeDefault = () => {
|
||||||
const onCheck = () => {
|
store.setDefaultAccount(account.address);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = () => {
|
||||||
store.selectAccount(account.address);
|
store.selectAccount(account.address);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Once new modal & account selection is in, this should be updated
|
let className;
|
||||||
// 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.
|
if (account.checked) {
|
||||||
|
className = account.default
|
||||||
|
? `${styles.selected} ${styles.default}`
|
||||||
|
: styles.selected;
|
||||||
|
} else {
|
||||||
|
className = styles.unselected;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
|
||||||
className={
|
|
||||||
account.checked
|
|
||||||
? styles.selected
|
|
||||||
: styles.unselected
|
|
||||||
}
|
|
||||||
key={ account.address }
|
|
||||||
leftCheckbox={
|
|
||||||
<Checkbox
|
|
||||||
checked={ account.checked }
|
|
||||||
onCheck={ onCheck }
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
primaryText={
|
|
||||||
<div className={ styles.item }>
|
<div className={ styles.item }>
|
||||||
<IdentityIcon address={ account.address } />
|
<AccountCard
|
||||||
<div className={ styles.info }>
|
account={ account }
|
||||||
<h3 className={ styles.name }>
|
className={ className }
|
||||||
{ account.name }
|
onClick={ onSelect }
|
||||||
</h3>
|
|
||||||
<div className={ styles.address }>
|
|
||||||
{ account.address }
|
|
||||||
</div>
|
|
||||||
<div className={ styles.description }>
|
|
||||||
{ account.description }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<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', () => {
|
it('does not render the modal with modalOpen = false', () => {
|
||||||
expect(
|
expect(
|
||||||
renderShallow({ modalOpen: false }).find('Connect(Modal)')
|
renderShallow({ modalOpen: false }).find('Portal')
|
||||||
).to.have.length(0);
|
).to.have.length(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does render the modal with modalOpen = true', () => {
|
it('does render the modal with modalOpen = true', () => {
|
||||||
expect(
|
expect(
|
||||||
renderShallow({ modalOpen: true, accounts: [] }).find('Connect(Modal)')
|
renderShallow({ modalOpen: true, accounts: [] }).find('Portal')
|
||||||
).to.have.length(1);
|
).to.have.length(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -29,12 +29,17 @@ export default class Store {
|
|||||||
|
|
||||||
@action closeModal = () => {
|
@action closeModal = () => {
|
||||||
transaction(() => {
|
transaction(() => {
|
||||||
const accounts = this.accounts
|
let addresses = null;
|
||||||
.filter((account) => account.checked)
|
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);
|
.map((account) => account.address);
|
||||||
|
}
|
||||||
|
|
||||||
this.modalOpen = false;
|
this.modalOpen = false;
|
||||||
this.updateWhitelist(accounts.length === this.accounts.length ? null : accounts);
|
this.updateWhitelist(addresses);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,12 +47,15 @@ export default class Store {
|
|||||||
transaction(() => {
|
transaction(() => {
|
||||||
this.accounts = Object
|
this.accounts = Object
|
||||||
.values(accounts)
|
.values(accounts)
|
||||||
.map((account) => {
|
.map((account, index) => {
|
||||||
return {
|
return {
|
||||||
address: account.address,
|
address: account.address,
|
||||||
checked: this.whitelist
|
checked: this.whitelist
|
||||||
? this.whitelist.includes(account.address)
|
? this.whitelist.includes(account.address)
|
||||||
: true,
|
: true,
|
||||||
|
default: this.whitelist
|
||||||
|
? this.whitelist[0] === account.address
|
||||||
|
: index === 0,
|
||||||
description: account.meta.description,
|
description: account.meta.description,
|
||||||
name: account.name
|
name: account.name
|
||||||
};
|
};
|
||||||
@ -57,9 +65,31 @@ export default class Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action selectAccount = (address) => {
|
@action selectAccount = (address) => {
|
||||||
|
transaction(() => {
|
||||||
this.accounts = this.accounts.map((account) => {
|
this.accounts = this.accounts.map((account) => {
|
||||||
if (account.address === address) {
|
if (account.address === address) {
|
||||||
account.checked = !account.checked;
|
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 = true;
|
||||||
|
account.default = true;
|
||||||
|
} else if (account.default) {
|
||||||
|
account.default = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
|
@ -23,13 +23,12 @@ const ACCOUNTS = {
|
|||||||
'456': { address: '456', name: '456', meta: { description: '456' } },
|
'456': { address: '456', name: '456', meta: { description: '456' } },
|
||||||
'789': { address: '789', name: '789', meta: { description: '789' } }
|
'789': { address: '789', name: '789', meta: { description: '789' } }
|
||||||
};
|
};
|
||||||
const WHITELIST = ['123', '456'];
|
const WHITELIST = ['456', '789'];
|
||||||
|
|
||||||
describe('modals/DappPermissions/store', () => {
|
|
||||||
let api;
|
let api;
|
||||||
let store;
|
let store;
|
||||||
|
|
||||||
beforeEach(() => {
|
function create () {
|
||||||
api = {
|
api = {
|
||||||
parity: {
|
parity: {
|
||||||
getNewDappsWhitelist: sinon.stub().resolves(WHITELIST),
|
getNewDappsWhitelist: sinon.stub().resolves(WHITELIST),
|
||||||
@ -38,6 +37,11 @@ describe('modals/DappPermissions/store', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
store = new Store(api);
|
store = new Store(api);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/DappPermissions/store', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
create();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('constructor', () => {
|
describe('constructor', () => {
|
||||||
@ -51,49 +55,71 @@ describe('modals/DappPermissions/store', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('@actions', () => {
|
describe('@actions', () => {
|
||||||
describe('openModal', () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store.openModal(ACCOUNTS);
|
store.openModal(ACCOUNTS);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('openModal', () => {
|
||||||
it('sets the modalOpen status', () => {
|
it('sets the modalOpen status', () => {
|
||||||
expect(store.modalOpen).to.be.true;
|
expect(store.modalOpen).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets accounts with checked interfaces', () => {
|
it('sets accounts with checked interfaces', () => {
|
||||||
expect(store.accounts.peek()).to.deep.equal([
|
expect(store.accounts.peek()).to.deep.equal([
|
||||||
{ address: '123', name: '123', description: '123', checked: true },
|
{ address: '123', name: '123', description: '123', default: false, checked: false },
|
||||||
{ address: '456', name: '456', description: '456', checked: true },
|
{ address: '456', name: '456', description: '456', default: true, checked: true },
|
||||||
{ address: '789', name: '789', description: '789', checked: false }
|
{ address: '789', name: '789', description: '789', default: false, checked: true }
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('closeModal', () => {
|
describe('closeModal', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store.openModal(ACCOUNTS);
|
store.setDefaultAccount('789');
|
||||||
store.selectAccount('789');
|
|
||||||
store.closeModal();
|
store.closeModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls setNewDappsWhitelist', () => {
|
it('calls setNewDappsWhitelist', () => {
|
||||||
expect(api.parity.setNewDappsWhitelist).to.have.been.calledOnce;
|
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', () => {
|
describe('selectAccount', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store.openModal(ACCOUNTS);
|
|
||||||
store.selectAccount('123');
|
store.selectAccount('123');
|
||||||
store.selectAccount('789');
|
store.selectAccount('789');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unselects previous selected accounts', () => {
|
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', () => {
|
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 SaveIcon from 'material-ui/svg-icons/content/save';
|
||||||
import SendIcon from 'material-ui/svg-icons/content/send';
|
import SendIcon from 'material-ui/svg-icons/content/send';
|
||||||
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
|
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 VerifyIcon from 'material-ui/svg-icons/action/verified-user';
|
||||||
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
|
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
|
||||||
import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
|
import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
|
||||||
@ -67,6 +70,9 @@ export {
|
|||||||
SaveIcon,
|
SaveIcon,
|
||||||
SendIcon,
|
SendIcon,
|
||||||
SnoozeIcon,
|
SnoozeIcon,
|
||||||
|
StarIcon,
|
||||||
|
StarCircleIcon,
|
||||||
|
StarOutlineIcon,
|
||||||
VerifyIcon,
|
VerifyIcon,
|
||||||
VisibleIcon,
|
VisibleIcon,
|
||||||
VpnIcon
|
VpnIcon
|
||||||
|
@ -33,24 +33,14 @@ $popoverTop: 20vh;
|
|||||||
$popoverZ: 3600;
|
$popoverZ: 3600;
|
||||||
|
|
||||||
.backOverlay {
|
.backOverlay {
|
||||||
background-color: rgba(255, 255, 255, 0.25);
|
background-color: rgba(255, 255, 255, 0.35);
|
||||||
opacity: 0;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 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 {
|
.parityBackground {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -59,21 +49,14 @@ $popoverZ: 3600;
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
z-index: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
background-color: rgba(0, 0, 0, 1);
|
background-color: rgba(0, 0, 0, 1);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
opacity: 0;
|
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
position: fixed;
|
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;
|
min-width: 0;
|
||||||
@ -84,6 +67,7 @@ $popoverZ: 3600;
|
|||||||
left: $modalLeft;
|
left: $modalLeft;
|
||||||
right: $modalRight;
|
right: $modalRight;
|
||||||
top: $modalTop;
|
top: $modalTop;
|
||||||
|
z-index: $modalZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.popover {
|
&.popover {
|
||||||
@ -91,19 +75,8 @@ $popoverZ: 3600;
|
|||||||
top: $popoverTop;
|
top: $popoverTop;
|
||||||
height: calc(100vh - $popoverTop - $popoverBottom);
|
height: calc(100vh - $popoverTop - $popoverBottom);
|
||||||
width: calc(100vw - $popoverLeft - $popoverRight);
|
width: calc(100vw - $popoverLeft - $popoverRight);
|
||||||
}
|
|
||||||
|
|
||||||
&.expanded {
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
&.popover {
|
|
||||||
z-index: $popoverZ;
|
z-index: $popoverZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.modal {
|
|
||||||
z-index: $modalZ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.closeIcon {
|
.closeIcon {
|
||||||
@ -111,9 +84,6 @@ $popoverZ: 3600;
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
transition-duration: 0.25s;
|
|
||||||
transition-property: opacity;
|
|
||||||
transition-timing-function: ease-out;
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
&, * {
|
&, * {
|
||||||
|
@ -35,40 +35,11 @@ export default class Portal extends Component {
|
|||||||
onKeyDown: PropTypes.func
|
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 () {
|
render () {
|
||||||
const { children, className, isChildModal } = this.props;
|
const { children, className, isChildModal, open } = this.props;
|
||||||
const { expanded } = this.state;
|
|
||||||
const backClasses = [ styles.backOverlay ];
|
|
||||||
const classes = [
|
|
||||||
styles.overlay,
|
|
||||||
isChildModal
|
|
||||||
? styles.popover
|
|
||||||
: styles.modal,
|
|
||||||
className
|
|
||||||
];
|
|
||||||
|
|
||||||
if (expanded) {
|
if (!open) {
|
||||||
classes.push(styles.expanded);
|
return null;
|
||||||
backClasses.push(styles.expanded);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -77,53 +48,37 @@ export default class Portal extends Component {
|
|||||||
onClose={ this.handleClose }
|
onClose={ this.handleClose }
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={ backClasses.join(' ') }
|
className={ styles.backOverlay }
|
||||||
onClick={ this.handleClose }
|
onClick={ this.handleClose }
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={ classes.join(' ') }
|
className={
|
||||||
|
[
|
||||||
|
styles.overlay,
|
||||||
|
isChildModal
|
||||||
|
? styles.popover
|
||||||
|
: styles.modal,
|
||||||
|
className
|
||||||
|
].join(' ')
|
||||||
|
}
|
||||||
onClick={ this.stopEvent }
|
onClick={ this.stopEvent }
|
||||||
onKeyDown={ this.handleKeyDown }
|
onKeyDown={ this.handleKeyDown }
|
||||||
>
|
>
|
||||||
<ParityBackground className={ styles.parityBackground } />
|
|
||||||
{ this.renderBindings() }
|
|
||||||
{ this.renderCloseIcon() }
|
|
||||||
{ children }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ReactPortal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBindings () {
|
|
||||||
const { expanded } = this.state;
|
|
||||||
|
|
||||||
if (!expanded) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EventListener
|
<EventListener
|
||||||
target='window'
|
target='window'
|
||||||
onKeyUp={ this.handleKeyUp }
|
onKeyUp={ this.handleKeyUp }
|
||||||
/>
|
/>
|
||||||
);
|
<ParityBackground className={ styles.parityBackground } />
|
||||||
}
|
|
||||||
|
|
||||||
renderCloseIcon () {
|
|
||||||
const { expanded } = this.state;
|
|
||||||
|
|
||||||
if (!expanded) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
className={ styles.closeIcon }
|
className={ styles.closeIcon }
|
||||||
onClick={ this.handleClose }
|
onClick={ this.handleClose }
|
||||||
>
|
>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</div>
|
</div>
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ReactPortal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,8 +39,8 @@
|
|||||||
.item {
|
.item {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
flex: 0 1 33.33%;
|
flex: 0 1 33.33%;
|
||||||
height: 100%;
|
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
transition: all 0.75s cubic-bezier(0.23, 1, 0.32, 1);
|
transition: all 0.75s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
@ -56,7 +56,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
flex: 0 0 50%;
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
@ -67,6 +66,13 @@
|
|||||||
& [data-hover="show"] {
|
& [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,
|
className: PropTypes.string,
|
||||||
items: arrayOrObjectProptype().isRequired,
|
items: arrayOrObjectProptype().isRequired,
|
||||||
renderItem: PropTypes.func.isRequired,
|
renderItem: PropTypes.func.isRequired,
|
||||||
|
noStretch: PropTypes.bool,
|
||||||
overlay: nodeOrStringProptype()
|
overlay: nodeOrStringProptype()
|
||||||
}
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
noStretch: false
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { className, items } = this.props;
|
const { className, items } = this.props;
|
||||||
@ -75,7 +80,7 @@ export default class SectionList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderItem = (item, index) => {
|
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)
|
// 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
|
// 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.
|
// CSS-only solution to let the browser do all the work via selectors.
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ styles.item }
|
className={ [
|
||||||
|
styles.item,
|
||||||
|
styles[`stretch-${noStretch ? 'off' : 'on'}`]
|
||||||
|
].join(' ') }
|
||||||
key={ `item_${index}` }
|
key={ `item_${index}` }
|
||||||
>
|
>
|
||||||
{ renderItem(item, index) }
|
{ renderItem(item, index) }
|
||||||
|
Loading…
Reference in New Issue
Block a user