From c7f5ee481da237d18a32bb486de2f28c2b57ee53 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 3 Feb 2017 22:44:43 +0100 Subject: [PATCH] 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 --- js/src/modals/AddDapps/addDapps.css | 1 - js/src/modals/AddDapps/addDapps.js | 16 ++- .../DappPermissions/dappPermissions.css | 6 -- .../modals/DappPermissions/dappPermissions.js | 39 ++++---- js/src/ui/Container/Title/title.css | 28 +++--- js/src/ui/Container/Title/title.js | 54 ++++++---- .../ui/Form/AddressSelect/addressSelect.css | 11 ++- js/src/ui/Form/AddressSelect/addressSelect.js | 58 ++++++----- js/src/ui/Modal/modal.css | 14 --- js/src/ui/Modal/modal.js | 7 +- js/src/ui/Portal/portal.css | 61 +++++++++--- js/src/ui/Portal/portal.example.js | 24 +++++ js/src/ui/Portal/portal.js | 98 ++++++++++++++++--- js/src/ui/Portal/portal.spec.js | 17 ++++ js/src/ui/{Modal => }/Title/index.js | 0 js/src/ui/Title/title.css | 26 +++++ js/src/ui/{Modal => }/Title/title.js | 42 +++++--- js/src/ui/index.js | 2 + 18 files changed, 345 insertions(+), 159 deletions(-) rename js/src/ui/{Modal => }/Title/index.js (100%) create mode 100644 js/src/ui/Title/title.css rename js/src/ui/{Modal => }/Title/title.js (65%) diff --git a/js/src/modals/AddDapps/addDapps.css b/js/src/modals/AddDapps/addDapps.css index 8de8f7f80..b26ffa623 100644 --- a/js/src/modals/AddDapps/addDapps.css +++ b/js/src/modals/AddDapps/addDapps.css @@ -20,7 +20,6 @@ } .container { - margin-top: 1.5em; overflow-y: auto; } diff --git a/js/src/modals/AddDapps/addDapps.js b/js/src/modals/AddDapps/addDapps.js index 0a4d19af9..9b1fcc760 100644 --- a/js/src/modals/AddDapps/addDapps.js +++ b/js/src/modals/AddDapps/addDapps.js @@ -18,7 +18,7 @@ import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { ContainerTitle, DappCard, Portal, SectionList } from '~/ui'; +import { DappCard, Portal, SectionList } from '~/ui'; import { CheckIcon } from '~/ui/Icons'; import styles from './addDapps.css'; @@ -41,15 +41,13 @@ export default class AddDapps extends Component { className={ styles.modal } onClose={ store.closeModal } open + title={ + + } > - - } - />
{ diff --git a/js/src/modals/DappPermissions/dappPermissions.css b/js/src/modals/DappPermissions/dappPermissions.css index 8b7c03a6a..22df5d24b 100644 --- a/js/src/modals/DappPermissions/dappPermissions.css +++ b/js/src/modals/DappPermissions/dappPermissions.css @@ -15,12 +15,7 @@ /* along with Parity. If not, see . */ -.modal { - flex-direction: column; -} - .container { - margin-top: 1.5em; overflow-y: auto; } @@ -65,7 +60,6 @@ .legend { opacity: 0.75; - margin-top: 1em; span { line-height: 24px; diff --git a/js/src/modals/DappPermissions/dappPermissions.js b/js/src/modals/DappPermissions/dappPermissions.js index dd6318368..4cd7cc837 100644 --- a/js/src/modals/DappPermissions/dappPermissions.js +++ b/js/src/modals/DappPermissions/dappPermissions.js @@ -18,7 +18,7 @@ import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { AccountCard, ContainerTitle, Portal, SectionList } from '~/ui'; +import { AccountCard, Portal, SectionList } from '~/ui'; import { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons'; import styles from './dappPermissions.css'; @@ -38,18 +38,27 @@ export default class DappPermissions extends Component { return ( + , + defaultIcon: + } } + /> +
+ } onClose={ store.closeModal } open + title={ + + } > - - } - />
-
- , - defaultIcon: - } } - /> -
); } diff --git a/js/src/ui/Container/Title/title.css b/js/src/ui/Container/Title/title.css index 0dec03905..b597aea3e 100644 --- a/js/src/ui/Container/Title/title.css +++ b/js/src/ui/Container/Title/title.css @@ -14,30 +14,36 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ -.byline, .description { + +$bylineColor: #aaa; +$bylineLineHeight: 1.2rem; +$bylineMaxHeight: 2.4rem; +$titleLineHeight: 2rem; +$smallFontSize: 0.75rem; + +.byline, +.description { + color: $bylineColor; + display: -webkit-box; + line-height: $bylineLineHeight; + max-height: $bylineMaxHeight; overflow: hidden; position: relative; - line-height: 1.2em; - max-height: 2.4em; - - display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; - color: #aaa; - * { - color: #aaa !important; + color: $bylineColor !important; } } .description { - font-size: 0.75em; + font-size: $smallFontSize; margin: 0.5em 0 0; } .title { - text-transform: uppercase; + line-height: $titleLineHeight; margin: 0; - line-height: 34px; + text-transform: uppercase; } diff --git a/js/src/ui/Container/Title/title.js b/js/src/ui/Container/Title/title.js index 1eb264201..217ad13bf 100644 --- a/js/src/ui/Container/Title/title.js +++ b/js/src/ui/Container/Title/title.js @@ -29,29 +29,41 @@ export default class Title extends Component { } render () { - const { byline, className, title } = this.props; - - const byLine = typeof byline === 'string' - ? ( - - { byline } - - ) - : byline; + const { className, title } = this.props; return (

{ title }

-
- { byLine } -
+ { this.renderByline() } { this.renderDescription() }
); } + renderByline () { + const { byline } = this.props; + + if (!byline) { + return null; + } + + return ( +
+ { + typeof byline === 'string' + ? ( + + { byline } + + ) + : byline + } +
+ ); + } + renderDescription () { const { description } = this.props; @@ -59,17 +71,17 @@ export default class Title extends Component { return null; } - const desc = typeof description === 'string' - ? ( - - { description } - - ) - : description; - return (
- { desc } + { + typeof description === 'string' + ? ( + + { description } + + ) + : description + }
); } diff --git a/js/src/ui/Form/AddressSelect/addressSelect.css b/js/src/ui/Form/AddressSelect/addressSelect.css index 0502a271f..43d6a7075 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.css +++ b/js/src/ui/Form/AddressSelect/addressSelect.css @@ -73,6 +73,12 @@ } } +.title { + display: flex; + flex-direction: column; + position: relative; +} + .label { margin: 1rem 0.5rem 0.25em; color: rgba(255, 255, 255, 0.498039); @@ -102,14 +108,11 @@ } .categories { - flex: 1; - display: flex; + flex: 1; flex-direction: row; justify-content: flex-start; - margin: 2rem 0 0; - > * { flex: 1; } diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index ce3aad2d3..d3c7462c3 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -180,34 +180,38 @@ class AddressSelect extends Component { onClose={ this.handleClose } onKeyDown={ this.handleKeyDown } open={ expanded } + title={ +
+ +
+ + { this.renderLoader() } +
+ +
+ +
+ + { this.renderCurrentInput() } + { this.renderRegistryValues() } +
+ } > - -
- - { this.renderLoader() } -
- -
- -
- - { this.renderCurrentInput() } - { this.renderRegistryValues() } { this.renderAccounts() } ); diff --git a/js/src/ui/Modal/modal.css b/js/src/ui/Modal/modal.css index 2b8172f2f..5eb419a76 100644 --- a/js/src/ui/Modal/modal.css +++ b/js/src/ui/Modal/modal.css @@ -47,20 +47,6 @@ .title { background: rgba(0, 0, 0, 0.25) !important; padding: 1em; - margin-bottom: 0; - - h3 { - margin: 0; - text-transform: uppercase; - } - - .steps { - margin-bottom: -1em; - } -} - -.waiting { - margin: 1em -1em -1em -1em; } .overlay { diff --git a/js/src/ui/Modal/modal.js b/js/src/ui/Modal/modal.js index 7dde32f45..7c7e19883 100644 --- a/js/src/ui/Modal/modal.js +++ b/js/src/ui/Modal/modal.js @@ -22,7 +22,7 @@ import { connect } from 'react-redux'; import { nodeOrStringProptype } from '~/util/proptypes'; import Container from '../Container'; -import Title from './Title'; +import Title from '../Title'; const ACTIONS_STYLE = { borderStyle: 'none' }; const TITLE_STYLE = { borderStyle: 'none' }; @@ -63,11 +63,12 @@ class Modal extends Component { const contentStyle = muiTheme.parity.getBackgroundStyle(null, settings.backgroundSeed); const header = ( ); const classes = `${styles.dialog} ${className}`; diff --git a/js/src/ui/Portal/portal.css b/js/src/ui/Portal/portal.css index daa646ba9..d88150bfe 100644 --- a/js/src/ui/Portal/portal.css +++ b/js/src/ui/Portal/portal.css @@ -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; } } diff --git a/js/src/ui/Portal/portal.example.js b/js/src/ui/Portal/portal.example.js index e8c51008e..7305a27b0 100644 --- a/js/src/ui/Portal/portal.example.js +++ b/js/src/ui/Portal/portal.example.js @@ -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> ); } diff --git a/js/src/ui/Portal/portal.js b/js/src/ui/Portal/portal.js index f334405ad..03b8b1f18 100644 --- a/js/src/ui/Portal/portal.js +++ b/js/src/ui/Portal/portal.js @@ -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; + } + } } diff --git a/js/src/ui/Portal/portal.spec.js b/js/src/ui/Portal/portal.spec.js index 6d2f5d5f3..fdc1ab4a7 100644 --- a/js/src/ui/Portal/portal.spec.js +++ b/js/src/ui/Portal/portal.spec.js @@ -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); + }); + }); }); diff --git a/js/src/ui/Modal/Title/index.js b/js/src/ui/Title/index.js similarity index 100% rename from js/src/ui/Modal/Title/index.js rename to js/src/ui/Title/index.js diff --git a/js/src/ui/Title/title.css b/js/src/ui/Title/title.css new file mode 100644 index 000000000..c211b0586 --- /dev/null +++ b/js/src/ui/Title/title.css @@ -0,0 +1,26 @@ +/* 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/>. +*/ + +.title { + .steps { + margin: -0.5em 0 -1em 0; + } + + .waiting { + margin: 1em -1em -1em -1em; + } +} diff --git a/js/src/ui/Modal/Title/title.js b/js/src/ui/Title/title.js similarity index 65% rename from js/src/ui/Modal/Title/title.js rename to js/src/ui/Title/title.js index d39bbb772..6f1319bd0 100644 --- a/js/src/ui/Modal/Title/title.js +++ b/js/src/ui/Title/title.js @@ -14,35 +14,49 @@ // 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, PropTypes } from 'react'; import { LinearProgress } from 'material-ui'; import { Step, Stepper, StepLabel } from 'material-ui/Stepper'; +import React, { Component, PropTypes } from 'react'; +// TODO: It would make sense (going forward) to replace all uses of +// ContainerTitle with this component. In that case the styles for the +// h3 (title) can be pulled from there. (As it stands the duplication +// between the 2 has been removed, but as a short-term DRY only) +import { Title as ContainerTitle } from '~/ui/Container'; import { nodeOrStringProptype } from '~/util/proptypes'; -import styles from '../modal.css'; +import styles from './title.css'; export default class Title extends Component { static propTypes = { + activeStep: PropTypes.number, busy: PropTypes.bool, - current: PropTypes.number, + busySteps: PropTypes.array, + className: PropTypes.string, steps: PropTypes.array, - waiting: PropTypes.array, title: nodeOrStringProptype() } render () { - const { current, steps, title } = this.props; + const { activeStep, className, steps, title } = this.props; + + if (!title && !steps) { + return null; + } return ( - <div className={ styles.title }> - <h3> - { + <div + className={ + [styles.title, className].join(' ') + } + > + <ContainerTitle + title={ steps - ? steps[current] + ? steps[activeStep || 0] : title } - </h3> + /> { this.renderSteps() } { this.renderWaiting() } </div> @@ -50,7 +64,7 @@ export default class Title extends Component { } renderSteps () { - const { current, steps } = this.props; + const { activeStep, steps } = this.props; if (!steps) { return; @@ -58,7 +72,7 @@ export default class Title extends Component { return ( <div className={ styles.steps }> - <Stepper activeStep={ current }> + <Stepper activeStep={ activeStep }> { this.renderTimeline() } </Stepper> </div> @@ -80,8 +94,8 @@ export default class Title extends Component { } renderWaiting () { - const { current, busy, waiting } = this.props; - const isWaiting = busy || (waiting || []).includes(current); + const { activeStep, busy, busySteps } = this.props; + const isWaiting = busy || (busySteps || []).includes(activeStep); if (!isWaiting) { return null; diff --git a/js/src/ui/index.js b/js/src/ui/index.js index eed56d369..967465165 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -55,6 +55,7 @@ import SectionList from './SectionList'; import ShortenedHash from './ShortenedHash'; import SignerIcon from './SignerIcon'; import Tags from './Tags'; +import Title from './Title'; import Tooltips, { Tooltip } from './Tooltips'; import TxHash from './TxHash'; import TxList from './TxList'; @@ -119,6 +120,7 @@ export { SectionList, SignerIcon, Tags, + Title, Tooltip, Tooltips, TxHash,