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:
parent
4e7b8652c8
commit
15ffd9a09c
@ -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 {
|
||||||
|
@ -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 }
|
||||||
|
@ -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;
|
||||||
|
97
js/src/ui/Portal/portal.example.js
Normal file
97
js/src/ui/Portal/portal.example.js
Normal 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: [] });
|
||||||
|
}
|
||||||
|
}
|
@ -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') {
|
||||||
|
47
js/src/ui/Portal/portal.spec.js
Normal file
47
js/src/ui/Portal/portal.spec.js
Normal 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;
|
||||||
|
});
|
||||||
|
});
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user