Allow Portal to be used as top-level modal (#4338)

* Portal

* Allow Portal to be used in as both top-level and popover

* modal/popover variable naming

* export Portal in ~/ui

* Properly handle optional onKeyDown

* Add simple Playground Example
This commit is contained in:
Jaco Greeff 2017-01-30 17:08:08 +01:00 committed by Gav Wood
parent 4e7b8652c8
commit 15ffd9a09c
7 changed files with 234 additions and 43 deletions

View File

@ -20,6 +20,7 @@ import React, { Component } from 'react';
import CurrencySymbol from '~/ui/CurrencySymbol/currencySymbol.example';
import QrCode from '~/ui/QrCode/qrCode.example';
import SectionList from '~/ui/SectionList/sectionList.example';
import Portal from '~/ui/Portal/portal.example';
import PlaygroundStore from './store';
import styles from './playground.css';
@ -27,6 +28,7 @@ import styles from './playground.css';
PlaygroundStore.register(<CurrencySymbol />);
PlaygroundStore.register(<QrCode />);
PlaygroundStore.register(<SectionList />);
PlaygroundStore.register(<Portal />);
@observer
export default class Playground extends Component {

View File

@ -176,6 +176,7 @@ class AddressSelect extends Component {
return (
<Portal
className={ styles.inputContainer }
isChildModal
onClose={ this.handleClose }
onKeyDown={ this.handleKeyDown }
open={ expanded }

View File

@ -15,29 +15,40 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
$left: 1.5em;
$right: $left;
$bottom: $left;
$top: 20vh;
$modalMargin: 1.5em;
$modalBackZ: 2500;
/* This should be the default case, the Portal used as a stand-alone modal */
$modalBottom: 15vh;
$modalLeft: $modalMargin;
$modalRight: $modalMargin;
$modalTop: 0;
$modalZ: 3500;
/* This is the case where popped-up over another modal, Portal or otherwise */
$popoverBottom: $modalMargin;
$popoverLeft: $modalMargin;
$popoverRight: $modalMargin;
$popoverTop: 20vh;
$popoverZ: 3600;
.backOverlay {
background-color: rgba(255, 255, 255, 0.25);
opacity: 0;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(255, 255, 255, 0.25);
z-index: -10;
opacity: 0;
transform-origin: 100% 0;
transition-property: opacity, z-index;
transition-duration: 0.25s;
transition-property: opacity, z-index;
transition-timing-function: ease-out;
z-index: -10;
&.expanded {
opacity: 1;
z-index: 2500;
z-index: $modalBackZ;
}
}
@ -52,45 +63,58 @@ $top: 20vh;
}
.overlay {
display: flex;
position: fixed;
top: $top;
left: $left;
width: calc(100vw - $left - $right);
height: calc(100vh - $top - $bottom);
transform-origin: 100% 0;
transition-property: opacity, z-index;
transition-duration: 0.25s;
transition-timing-function: ease-out;
background-color: rgba(0, 0, 0, 1);
opacity: 0;
z-index: -10;
padding: 1em;
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;
}
&.modal {
bottom: $modalBottom;
left: $modalLeft;
right: $modalRight;
top: $modalTop;
}
&.popover {
left: $popoverLeft;
top: $popoverTop;
height: calc(100vh - $popoverTop - $popoverBottom);
width: calc(100vw - $popoverLeft - $popoverRight);
}
&.expanded {
opacity: 1;
z-index: 3500;
&.popover {
z-index: $popoverZ;
}
&.modal {
z-index: $modalZ;
}
}
}
.closeIcon {
position: absolute;
top: 0.5rem;
right: 1rem;
font-size: 4em;
z-index: 100;
transition-property: opacity;
position: absolute;
right: 1rem;
top: 0.5rem;
transition-duration: 0.25s;
transition-property: opacity;
transition-timing-function: ease-out;
z-index: 100;
&, * {
height: 48px !important;

View File

@ -0,0 +1,97 @@
// 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 } from 'react';
import PlaygroundExample from '~/playground/playgroundExample';
import Modal from '../Modal';
import Portal from './portal';
export default class PortalExample extends Component {
state = {
open: []
};
render () {
const { open } = this.state;
return (
<div>
<PlaygroundExample name='Standard Portal'>
<div>
<button onClick={ this.handleOpen(0) }>Open</button>
<Portal
open={ open[0] || false }
onClose={ this.handleClose }
>
<p>This is the first portal</p>
</Portal>
</div>
</PlaygroundExample>
<PlaygroundExample name='Popover Portal'>
<div>
<button onClick={ this.handleOpen(1) }>Open</button>
<Portal
isChildModal
open={ open[1] || false }
onClose={ this.handleClose }
>
<p>This is the second portal</p>
</Portal>
</div>
</PlaygroundExample>
<PlaygroundExample name='Portal in Modal'>
<div>
<button onClick={ this.handleOpen(2) }>Open</button>
<Modal
title='Modal'
visible={ open[2] || false }
>
<button onClick={ this.handleOpen(3) }>Open</button>
<button onClick={ this.handleClose }>Close</button>
</Modal>
<Portal
isChildModal
open={ open[3] || false }
onClose={ this.handleClose }
>
<p>This is the second portal</p>
</Portal>
</div>
</PlaygroundExample>
</div>
);
}
handleOpen = (index) => {
return () => {
const { open } = this.state;
const nextOpen = open.slice();
nextOpen[index] = true;
this.setState({ open: nextOpen });
};
}
handleClose = () => {
this.setState({ open: [] });
}
}

View File

@ -28,9 +28,9 @@ export default class Portal extends Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
children: PropTypes.node,
className: PropTypes.string,
isChildModal: PropTypes.bool,
onKeyDown: PropTypes.func
};
@ -54,11 +54,16 @@ export default class Portal extends Component {
}
render () {
const { children, className, isChildModal } = this.props;
const { expanded } = this.state;
const { children, className } = this.props;
const classes = [ styles.overlay, className ];
const backClasses = [ styles.backOverlay ];
const classes = [
styles.overlay,
isChildModal
? styles.popover
: styles.modal,
className
];
if (expanded) {
classes.push(styles.expanded);
@ -66,15 +71,20 @@ export default class Portal extends Component {
}
return (
<ReactPortal isOpened onClose={ this.handleClose }>
<div className={ backClasses.join(' ') } onClick={ this.handleClose }>
<ReactPortal
isOpened
onClose={ this.handleClose }
>
<div
className={ backClasses.join(' ') }
onClick={ this.handleClose }
>
<div
className={ classes.join(' ') }
onClick={ this.stopEvent }
onKeyDown={ this.handleKeyDown }
>
<ParityBackground className={ styles.parityBackground } />
{ this.renderCloseIcon() }
{ children }
</div>
@ -91,7 +101,10 @@ export default class Portal extends Component {
}
return (
<div className={ styles.closeIcon } onClick={ this.handleClose }>
<div
className={ styles.closeIcon }
onClick={ this.handleClose }
>
<CloseIcon />
</div>
);
@ -107,6 +120,7 @@ export default class Portal extends Component {
}
handleKeyDown = (event) => {
const { onKeyDown } = this.props;
const codeName = keycode(event);
switch (codeName) {
@ -116,12 +130,16 @@ export default class Portal extends Component {
default:
event.persist();
return this.props.onKeyDown(event);
return onKeyDown
? onKeyDown(event)
: false;
}
}
handleDOMAction = (ref, method) => {
const refItem = typeof ref === 'string' ? this.refs[ref] : ref;
const refItem = typeof ref === 'string'
? this.refs[ref]
: ref;
const element = ReactDOM.findDOMNode(refItem);
if (!element || typeof element[method] !== 'function') {

View File

@ -0,0 +1,47 @@
// 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 Portal from './';
let component;
let onClose;
function render (props = {}) {
onClose = sinon.stub();
component = shallow(
<Portal
onClose={ onClose }
open
{ ...props }
/>
);
return component;
}
describe('ui/Portal', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
});

View File

@ -47,6 +47,7 @@ import muiTheme from './Theme';
import Page from './Page';
import ParityBackground from './ParityBackground';
import PasswordStrength from './Form/PasswordStrength';
import Portal from './Portal';
import QrCode from './QrCode';
import SectionList from './SectionList';
import ShortenedHash from './ShortenedHash';
@ -103,6 +104,7 @@ export {
Page,
ParityBackground,
PasswordStrength,
Portal,
QrCode,
RadioButtons,
Select,