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

View File

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

View File

@ -15,29 +15,40 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
$left: 1.5em; $modalMargin: 1.5em;
$right: $left; $modalBackZ: 2500;
$bottom: $left;
$top: 20vh; /* 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 { .backOverlay {
background-color: rgba(255, 255, 255, 0.25);
opacity: 0;
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
background-color: rgba(255, 255, 255, 0.25);
z-index: -10;
opacity: 0;
transform-origin: 100% 0; transform-origin: 100% 0;
transition-property: opacity, z-index;
transition-duration: 0.25s; transition-duration: 0.25s;
transition-property: opacity, z-index;
transition-timing-function: ease-out; transition-timing-function: ease-out;
z-index: -10;
&.expanded { &.expanded {
opacity: 1; opacity: 1;
z-index: 2500; z-index: $modalBackZ;
} }
} }
@ -52,45 +63,58 @@ $top: 20vh;
} }
.overlay { .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); background-color: rgba(0, 0, 0, 1);
opacity: 0;
z-index: -10;
padding: 1em;
box-sizing: border-box; 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; 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 { &.expanded {
opacity: 1; opacity: 1;
z-index: 3500;
&.popover {
z-index: $popoverZ;
}
&.modal {
z-index: $modalZ;
}
} }
} }
.closeIcon { .closeIcon {
position: absolute;
top: 0.5rem;
right: 1rem;
font-size: 4em; font-size: 4em;
z-index: 100; position: absolute;
right: 1rem;
transition-property: opacity; top: 0.5rem;
transition-duration: 0.25s; transition-duration: 0.25s;
transition-property: opacity;
transition-timing-function: ease-out; transition-timing-function: ease-out;
z-index: 100;
&, * { &, * {
height: 48px !important; 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 = { static propTypes = {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired, open: PropTypes.bool.isRequired,
children: PropTypes.node, children: PropTypes.node,
className: PropTypes.string, className: PropTypes.string,
isChildModal: PropTypes.bool,
onKeyDown: PropTypes.func onKeyDown: PropTypes.func
}; };
@ -54,11 +54,16 @@ export default class Portal extends Component {
} }
render () { render () {
const { children, className, isChildModal } = this.props;
const { expanded } = this.state; const { expanded } = this.state;
const { children, className } = this.props;
const classes = [ styles.overlay, className ];
const backClasses = [ styles.backOverlay ]; const backClasses = [ styles.backOverlay ];
const classes = [
styles.overlay,
isChildModal
? styles.popover
: styles.modal,
className
];
if (expanded) { if (expanded) {
classes.push(styles.expanded); classes.push(styles.expanded);
@ -66,15 +71,20 @@ export default class Portal extends Component {
} }
return ( return (
<ReactPortal isOpened onClose={ this.handleClose }> <ReactPortal
<div className={ backClasses.join(' ') } onClick={ this.handleClose }> isOpened
onClose={ this.handleClose }
>
<div
className={ backClasses.join(' ') }
onClick={ this.handleClose }
>
<div <div
className={ classes.join(' ') } className={ classes.join(' ') }
onClick={ this.stopEvent } onClick={ this.stopEvent }
onKeyDown={ this.handleKeyDown } onKeyDown={ this.handleKeyDown }
> >
<ParityBackground className={ styles.parityBackground } /> <ParityBackground className={ styles.parityBackground } />
{ this.renderCloseIcon() } { this.renderCloseIcon() }
{ children } { children }
</div> </div>
@ -91,7 +101,10 @@ export default class Portal extends Component {
} }
return ( return (
<div className={ styles.closeIcon } onClick={ this.handleClose }> <div
className={ styles.closeIcon }
onClick={ this.handleClose }
>
<CloseIcon /> <CloseIcon />
</div> </div>
); );
@ -107,6 +120,7 @@ export default class Portal extends Component {
} }
handleKeyDown = (event) => { handleKeyDown = (event) => {
const { onKeyDown } = this.props;
const codeName = keycode(event); const codeName = keycode(event);
switch (codeName) { switch (codeName) {
@ -116,12 +130,16 @@ export default class Portal extends Component {
default: default:
event.persist(); event.persist();
return this.props.onKeyDown(event); return onKeyDown
? onKeyDown(event)
: false;
} }
} }
handleDOMAction = (ref, method) => { 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); const element = ReactDOM.findDOMNode(refItem);
if (!element || typeof element[method] !== 'function') { 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 Page from './Page';
import ParityBackground from './ParityBackground'; import ParityBackground from './ParityBackground';
import PasswordStrength from './Form/PasswordStrength'; import PasswordStrength from './Form/PasswordStrength';
import Portal from './Portal';
import QrCode from './QrCode'; import QrCode from './QrCode';
import SectionList from './SectionList'; import SectionList from './SectionList';
import ShortenedHash from './ShortenedHash'; import ShortenedHash from './ShortenedHash';
@ -103,6 +104,7 @@ export {
Page, Page,
ParityBackground, ParityBackground,
PasswordStrength, PasswordStrength,
Portal,
QrCode, QrCode,
RadioButtons, RadioButtons,
Select, Select,