From 15ffd9a09c17412a5e991823b23a9a038c6b767e Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 30 Jan 2017 17:08:08 +0100 Subject: [PATCH] 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 --- js/src/playground/playground.js | 2 + js/src/ui/Form/AddressSelect/addressSelect.js | 1 + js/src/ui/Portal/portal.css | 90 ++++++++++------- js/src/ui/Portal/portal.example.js | 97 +++++++++++++++++++ js/src/ui/Portal/portal.js | 38 ++++++-- js/src/ui/Portal/portal.spec.js | 47 +++++++++ js/src/ui/index.js | 2 + 7 files changed, 234 insertions(+), 43 deletions(-) create mode 100644 js/src/ui/Portal/portal.example.js create mode 100644 js/src/ui/Portal/portal.spec.js diff --git a/js/src/playground/playground.js b/js/src/playground/playground.js index 65535ca41..a1790ed32 100644 --- a/js/src/playground/playground.js +++ b/js/src/playground/playground.js @@ -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(); PlaygroundStore.register(); PlaygroundStore.register(); +PlaygroundStore.register(); @observer export default class Playground extends Component { diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index d49898eba..ce3aad2d3 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -176,6 +176,7 @@ class AddressSelect extends Component { return ( . */ -$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; diff --git a/js/src/ui/Portal/portal.example.js b/js/src/ui/Portal/portal.example.js new file mode 100644 index 000000000..e8c51008e --- /dev/null +++ b/js/src/ui/Portal/portal.example.js @@ -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 . + +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 ( +
+ +
+ + +

This is the first portal

+
+
+
+ + +
+ + +

This is the second portal

+
+
+
+ + +
+ + + + + + + + +

This is the second portal

+
+
+
+
+ ); + } + + handleOpen = (index) => { + return () => { + const { open } = this.state; + const nextOpen = open.slice(); + + nextOpen[index] = true; + this.setState({ open: nextOpen }); + }; + } + + handleClose = () => { + this.setState({ open: [] }); + } +} diff --git a/js/src/ui/Portal/portal.js b/js/src/ui/Portal/portal.js index cf7bc54a0..53c4324c2 100644 --- a/js/src/ui/Portal/portal.js +++ b/js/src/ui/Portal/portal.js @@ -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 ( - -
+ +
- { this.renderCloseIcon() } { children }
@@ -91,7 +101,10 @@ export default class Portal extends Component { } return ( -
+
); @@ -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') { diff --git a/js/src/ui/Portal/portal.spec.js b/js/src/ui/Portal/portal.spec.js new file mode 100644 index 000000000..6d2f5d5f3 --- /dev/null +++ b/js/src/ui/Portal/portal.spec.js @@ -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 . + +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( + + ); + + return component; +} + +describe('ui/Portal', () => { + beforeEach(() => { + render(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); +}); diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 99a6348af..37dc88e9a 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -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,