Extend Portal component with title, buttons & steps (as per Modal) (#4392)

* Allow Portal to take title & buttons props

* Fix tests

* Portal consistent in screen center

* Allow hiding of Close (e.g. FirstRun usage)

* Set overflow style on body based on open

* Don't lock scroll for child popups (overlaps)

* Override buttons to be white

* Expose ~/ui/Modal/Title as re-usable component

* Use ~/ui/Title to render the Title

* Update tests

* Added a portal example with buttons and steps

* Address PR comments

* Fix AddressSelect with new container withing container

* Move legend to "buttons"

* AddressSelect extra padding
This commit is contained in:
Jaco Greeff
2017-02-03 22:44:43 +01:00
committed by GitHub
parent a68ca7444e
commit c7f5ee481d
18 changed files with 345 additions and 159 deletions

View File

@@ -16,13 +16,14 @@
*/
$modalMargin: 1.5em;
$modalPadding: 1.5em;
$modalBackZ: 2500;
/* This should be the default case, the Portal used as a stand-alone modal */
$modalBottom: 15vh;
$modalBottom: $modalMargin;
$modalLeft: $modalMargin;
$modalRight: $modalMargin;
$modalTop: 0;
$modalTop: $modalMargin;
$modalZ: 3500;
/* This is the case where popped-up over another modal, Portal or otherwise */
@@ -55,7 +56,9 @@ $popoverZ: 3600;
background-color: rgba(0, 0, 0, 1);
box-sizing: border-box;
display: flex;
padding: 1.5em;
flex-direction: column;
justify-content: space-between;
padding: $modalPadding;
position: fixed;
* {
@@ -77,22 +80,48 @@ $popoverZ: 3600;
width: calc(100vw - $popoverLeft - $popoverRight);
z-index: $popoverZ;
}
}
.closeIcon {
font-size: 4em;
position: absolute;
right: 1rem;
top: 0.5rem;
z-index: 100;
.buttonRow {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
padding: $modalPadding 0 0 0;
&, * {
height: 48px !important;
width: 48px !important;
button:not([disabled]) {
color: white !important;
svg {
fill: white !important;
}
}
}
&:hover {
cursor: pointer;
opacity: 0.5;
.childContainer {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
.closeIcon {
font-size: 4em;
position: absolute;
right: 1rem;
top: 0.5rem;
z-index: 100;
&, * {
height: 48px !important;
width: 48px !important;
}
&:hover {
cursor: pointer;
opacity: 0.5;
}
}
.titleRow {
margin-bottom: $modalPadding;
}
}

View File

@@ -16,6 +16,7 @@
import React, { Component } from 'react';
import { Button } from '~/ui';
import PlaygroundExample from '~/playground/playgroundExample';
import Modal from '../Modal';
@@ -77,6 +78,29 @@ export default class PortalExample extends Component {
</Portal>
</div>
</PlaygroundExample>
<PlaygroundExample name='Portal with Buttons'>
<div>
<button onClick={ this.handleOpen(4) }>Open</button>
<Portal
activeStep={ 0 }
buttons={ [
<Button
key='close'
label='close'
onClick={ this.handleClose }
/>
] }
isChildModal
open={ open[4] || false }
onClose={ this.handleClose }
steps={ [ 'step 1', 'step 2' ] }
title='Portal with button'
>
<p>This is the fourth portal</p>
</Portal>
</div>
</PlaygroundExample>
</div>
);
}

View File

@@ -20,8 +20,10 @@ import ReactDOM from 'react-dom';
import ReactPortal from 'react-portal';
import keycode from 'keycode';
import { nodeOrStringProptype } from '~/util/proptypes';
import { CloseIcon } from '~/ui/Icons';
import ParityBackground from '~/ui/ParityBackground';
import Title from '~/ui/Title';
import styles from './portal.css';
@@ -29,14 +31,35 @@ export default class Portal extends Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
activeStep: PropTypes.number,
busy: PropTypes.bool,
busySteps: PropTypes.array,
buttons: PropTypes.array,
children: PropTypes.node,
className: PropTypes.string,
hideClose: PropTypes.bool,
isChildModal: PropTypes.bool,
onKeyDown: PropTypes.func
onKeyDown: PropTypes.func,
steps: PropTypes.array,
title: nodeOrStringProptype()
};
componentDidMount () {
this.setBodyOverflow(this.props.open);
}
componentWillReceiveProps (nextProps) {
if (nextProps.open !== this.props.open) {
this.setBodyOverflow(nextProps.open);
}
}
componentWillUnmount () {
this.setBodyOverflow(false);
}
render () {
const { children, className, isChildModal, open } = this.props;
const { activeStep, busy, busySteps, children, className, isChildModal, open, steps, title } = this.props;
if (!open) {
return null;
@@ -69,32 +92,72 @@ export default class Portal extends Component {
onKeyUp={ this.handleKeyUp }
/>
<ParityBackground className={ styles.parityBackground } />
<div
className={ styles.closeIcon }
onClick={ this.handleClose }
>
<CloseIcon />
{ this.renderClose() }
<Title
activeStep={ activeStep }
busy={ busy }
busySteps={ busySteps }
className={ styles.titleRow }
steps={ steps }
title={ title }
/>
<div className={ styles.childContainer }>
{ children }
</div>
{ children }
{ this.renderButtons() }
</div>
</div>
</ReactPortal>
);
}
renderButtons () {
const { buttons } = this.props;
if (!buttons) {
return null;
}
return (
<div className={ styles.buttonRow }>
{ buttons }
</div>
);
}
renderClose () {
const { hideClose } = this.props;
if (hideClose) {
return null;
}
return (
<CloseIcon
className={ styles.closeIcon }
onClick={ this.handleClose }
/>
);
}
stopEvent = (event) => {
event.preventDefault();
event.stopPropagation();
}
handleClose = () => {
this.props.onClose();
const { hideClose, onClose } = this.props;
if (!hideClose) {
onClose();
}
}
handleKeyDown = (event) => {
const { onKeyDown } = this.props;
event.persist();
return onKeyDown
? onKeyDown(event)
: false;
@@ -111,10 +174,11 @@ export default class Portal extends Component {
}
handleDOMAction = (ref, method) => {
const refItem = typeof ref === 'string'
? this.refs[ref]
: ref;
const element = ReactDOM.findDOMNode(refItem);
const element = ReactDOM.findDOMNode(
typeof ref === 'string'
? this.refs[ref]
: ref
);
if (!element || typeof element[method] !== 'function') {
console.warn('could not find', ref, 'or method', method);
@@ -123,4 +187,12 @@ export default class Portal extends Component {
return element[method]();
}
setBodyOverflow (open) {
if (!this.props.isChildModal) {
document.body.style.overflow = open
? 'hidden'
: null;
}
}
}

View File

@@ -44,4 +44,21 @@ describe('ui/Portal', () => {
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('title rendering', () => {
const TITLE = 'some test title';
let title;
beforeEach(() => {
title = render({ title: TITLE }).find('Title');
});
it('renders the specified title', () => {
expect(title).to.have.length(1);
});
it('renders the passed title', () => {
expect(title.props().title).to.equal(TITLE);
});
});
});