From bde29cdb823d155236e50aece7afb704602a20b6 Mon Sep 17 00:00:00 2001 From: Juan Cortes Ross <juan@bohem.io> Date: Mon, 14 Jan 2019 22:13:44 +0100 Subject: [PATCH 1/8] Initial version --- src/components/CurrentAddress/index.js | 3 + src/components/GenuineCheckModal.js | 13 +- src/components/IsUnlocked.js | 4 +- src/components/ManagerPage/AppsList.js | 197 +++++----- src/components/SettingsPage/CleanButton.js | 3 +- .../SettingsPage/DisablePasswordModal.js | 67 ++-- .../SettingsPage/PasswordAutoLockSelect.js | 8 +- src/components/SettingsPage/PasswordModal.js | 50 +-- .../SettingsPage/RepairDeviceButton.js | 2 +- src/components/SettingsPage/ResetButton.js | 3 +- .../SettingsPage/ResetFallbackModal.js | 2 +- .../SettingsPage/sections/Display.js | 4 +- .../SettingsPage/sections/Export.js | 53 +-- src/components/base/Modal/ConfirmModal.js | 59 +-- src/components/base/Modal/ModalBody.js | 143 ++++---- src/components/base/Modal/ModalContent.js | 59 +++ src/components/base/Modal/ModalFooter.js | 18 + src/components/base/Modal/ModalHeader.js | 93 +++++ src/components/base/Modal/ModalTitle.js | 75 ---- src/components/base/Modal/RepairModal.js | 95 ++--- src/components/base/Modal/index.js | 345 +++++++++--------- src/components/base/Modal/stories.js | 75 ++-- src/components/base/Stepper/index.js | 44 ++- .../modals/AccountSettingRenderBody.js | 64 ++-- src/components/modals/AddAccounts/index.js | 2 + src/components/modals/Debug.js | 32 +- src/components/modals/Disclaimer.js | 49 ++- src/components/modals/OperationDetails.js | 215 ++++++----- src/components/modals/Receive/index.js | 5 +- .../modals/ReleaseNotes/ReleaseNotesBody.js | 37 +- src/components/modals/ReleaseNotes/index.js | 1 + src/components/modals/Send/index.js | 1 + src/components/modals/SettingsAccount.js | 5 +- src/components/modals/ShareAnalytics.js | 62 ++-- src/components/modals/TechnicalData.js | 60 ++- .../modals/UpdateFirmware/Disclaimer.js | 78 ++-- src/components/modals/UpdateFirmware/index.js | 1 + src/index.ejs | 1 + src/reducers/modals.js | 1 - 39 files changed, 1114 insertions(+), 915 deletions(-) create mode 100644 src/components/base/Modal/ModalContent.js create mode 100644 src/components/base/Modal/ModalFooter.js create mode 100644 src/components/base/Modal/ModalHeader.js delete mode 100644 src/components/base/Modal/ModalTitle.js diff --git a/src/components/CurrentAddress/index.js b/src/components/CurrentAddress/index.js index 9b7c6dd6..fc8d4de3 100644 --- a/src/components/CurrentAddress/index.js +++ b/src/components/CurrentAddress/index.js @@ -145,8 +145,11 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> { componentWillUnmount() { if (this._timeout) clearTimeout(this._timeout) + this._isUnmounted = true } + _isUnmounted = false + renderCopy = copy => { const { t } = this.props return ( diff --git a/src/components/GenuineCheckModal.js b/src/components/GenuineCheckModal.js index e70d17ec..d9f4f7a1 100644 --- a/src/components/GenuineCheckModal.js +++ b/src/components/GenuineCheckModal.js @@ -5,7 +5,7 @@ import { translate } from 'react-i18next' import type { T } from 'types/common' -import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal' +import Modal, { ModalBody } from 'components/base/Modal' import GenuineCheck from 'components/GenuineCheck' type Props = { @@ -19,12 +19,13 @@ class GenuineCheckModal extends PureComponent<Props> { renderBody = ({ onClose }) => { const { t, onSuccess, onFail, onUnavailable } = this.props return ( - <ModalBody onClose={onClose}> - <ModalTitle>{t('genuinecheck.modal.title')}</ModalTitle> - <ModalContent> + <ModalBody + onClose={onClose} + title={t('genuinecheck.modal.title')} + render={() => ( <GenuineCheck onSuccess={onSuccess} onFail={onFail} onUnavailable={onUnavailable} /> - </ModalContent> - </ModalBody> + )} + /> ) } diff --git a/src/components/IsUnlocked.js b/src/components/IsUnlocked.js index 997ee69f..35dfbdca 100644 --- a/src/components/IsUnlocked.js +++ b/src/components/IsUnlocked.js @@ -23,8 +23,8 @@ import Box from 'components/base/Box' import InputPassword from 'components/base/InputPassword' import LedgerLiveLogo from 'components/base/LedgerLiveLogo' import IconArrowRight from 'icons/ArrowRight' -import Button from './base/Button/index' -import ConfirmModal from './base/Modal/ConfirmModal' +import Button from 'components/base/Button/index' +import ConfirmModal from 'components/base/Modal/ConfirmModal' type InputValue = { password: string, diff --git a/src/components/ManagerPage/AppsList.js b/src/components/ManagerPage/AppsList.js index 115c8904..390da5c6 100644 --- a/src/components/ManagerPage/AppsList.js +++ b/src/components/ManagerPage/AppsList.js @@ -16,7 +16,7 @@ import installApp from 'commands/installApp' import uninstallApp from 'commands/uninstallApp' import Box from 'components/base/Box' import Space from 'components/base/Space' -import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal' +import Modal from 'components/base/Modal' import Tooltip from 'components/base/Tooltip' import Text from 'components/base/Text' import ProgressBar from 'components/ProgressBar' @@ -32,6 +32,7 @@ import CheckCircle from 'icons/CheckCircle' import { FreezeDeviceChangeEvents } from './HookDeviceChange' import ManagerApp, { Container as FakeManagerAppContainer } from './ManagerApp' import AppSearchBar from './AppSearchBar' +import ModalBody from '../base/Modal/ModalBody' const mapStateToProps = state => ({ isDevMode: developerModeSelector(state), @@ -150,108 +151,104 @@ class AppsList extends PureComponent<Props, State> { handleCloseModal = () => this.setState({ status: 'idle', mode: 'home' }) - renderModal = () => { + renderBody = () => { const { t } = this.props const { app, status, error, mode, progress } = this.state - return ( - <Modal - isOpened={status !== 'idle' && status !== 'loading'} - render={() => ( - <ModalBody align="center" justify="center" style={{ height: 300 }}> - <FreezeDeviceChangeEvents /> - {status === 'busy' || status === 'idle' ? ( - <Fragment> - <ModalTitle> - {mode === 'installing' ? ( - <Box color="grey"> - <Update size={30} /> - </Box> - ) : ( - <Box color="grey"> - <Trash size={30} /> - </Box> - )} - </ModalTitle> - <ModalContent> - <Text ff="Museo Sans|Regular" fontSize={6} color="dark"> - {t(`manager.apps.${mode}`, { app })} - </Text> - <Box mt={6}> - <ProgressBar width={150} progress={progress} /> - </Box> - </ModalContent> - </Fragment> - ) : status === 'error' ? ( - <Fragment> - <TrackPage - category="Manager" - name="Error Modal" - error={error && error.name} - app={app} - /> - <ModalContent grow align="center" justify="center" mt={5}> - <Box color="alertRed"> - <ExclamationCircleThin size={44} /> - </Box> - <Box - color="dark" - mt={4} - fontSize={6} - ff="Museo Sans|Regular" - textAlign="center" - style={{ maxWidth: 350 }} - > - <TranslatedError error={error} field="title" /> - </Box> - <Box - color="graphite" - mt={2} - fontSize={4} - ff="Open Sans" - textAlign="center" - style={{ maxWidth: 350 }} - > - <TranslatedError error={error} field="description" /> - </Box> - </ModalContent> - <ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}> - <Button primary onClick={this.handleCloseModal}> - {t('common.close')} - </Button> - </ModalFooter> - </Fragment> - ) : status === 'success' ? ( - <Fragment> - <ModalContent grow align="center" justify="center" mt={5}> - <Box color="positiveGreen"> - <CheckCircle size={44} /> - </Box> - <Box - color="dark" - mt={4} - fontSize={6} - ff="Museo Sans|Regular" - textAlign="center" - style={{ maxWidth: 350 }} - > - {t( - `manager.apps.${ - mode === 'installing' ? 'installSuccess' : 'uninstallSuccess' - }`, - { app }, - )} - </Box> - </ModalContent> - <ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}> - <Button primary onClick={this.handleCloseModal}> - {t('common.close')} - </Button> - </ModalFooter> - </Fragment> - ) : null} - </ModalBody> + + return ['busy', 'idle'].includes(status) ? ( + <Box grow align="center" justify="center"> + {mode === 'installing' ? ( + <Box color="grey" grow align="center" mb={5}> + <Update size={30} /> + </Box> + ) : ( + <Box color="grey" grow align="center" mb={5}> + <Trash size={30} /> + </Box> )} - /> + <Text ff="Museo Sans|Regular" fontSize={6} color="dark"> + {t(`manager.apps.${mode}`, { app })} + </Text> + <Box mt={6}> + <ProgressBar width={150} progress={progress} /> + </Box> + </Box> + ) : status === 'error' ? ( + <Box> + <TrackPage category="Manager" name="Error Modal" error={error && error.name} app={app} /> + <Box grow align="center" justify="center" mt={5}> + <Box color="alertRed"> + <ExclamationCircleThin size={44} /> + </Box> + <Box + color="dark" + mt={4} + fontSize={6} + ff="Museo Sans|Regular" + textAlign="center" + style={{ maxWidth: 350 }} + > + <TranslatedError error={error} field="title" /> + </Box> + <Box + color="graphite" + mt={2} + fontSize={4} + ff="Open Sans" + textAlign="center" + style={{ maxWidth: 350 }} + > + <TranslatedError error={error} field="description" /> + </Box> + </Box> + </Box> + ) : status === 'success' ? ( + <Box grow align="center" justify="center" mt={5}> + <Box color="positiveGreen"> + <CheckCircle size={44} /> + </Box> + <Box + color="dark" + mt={4} + fontSize={6} + ff="Museo Sans|Regular" + textAlign="center" + style={{ maxWidth: 350 }} + > + {t(`manager.apps.${mode === 'installing' ? 'installSuccess' : 'uninstallSuccess'}`, { + app, + })} + </Box> + </Box> + ) : null + } + + renderFooter = () => { + const { t } = this.props + const { status } = this.state + return ['error', 'success'].includes(status) ? ( + <Box horizontal justifyContent="flex-end" style={{ width: '100%' }}> + <Button primary onClick={this.handleCloseModal}> + {t('common.close')} + </Button> + </Box> + ) : null + } + + renderModal = () => { + const { status } = this.state + return ( + <Modal isOpened={status !== 'idle' && status !== 'loading'}> + <ModalBody + align="center" + justify="center" + title={''} + render={this.renderBody} + renderFooter={this.renderFooter} + > + <FreezeDeviceChangeEvents /> + </ModalBody> + </Modal> ) } diff --git a/src/components/SettingsPage/CleanButton.js b/src/components/SettingsPage/CleanButton.js index 0f72d21f..8a3081ea 100644 --- a/src/components/SettingsPage/CleanButton.js +++ b/src/components/SettingsPage/CleanButton.js @@ -7,7 +7,7 @@ import logger from 'logger' import type { T } from 'types/common' import { cleanAccountsCache } from 'actions/accounts' import Button from 'components/base/Button' -import { ConfirmModal } from 'components/base/Modal' +import ConfirmModal from 'components/base/Modal/ConfirmModal' import { softReset } from 'helpers/reset' import ResetFallbackModal from './ResetFallbackModal' @@ -60,6 +60,7 @@ class CleanButton extends PureComponent<Props, State> { <ConfirmModal analyticsName="CleanCache" + centered isOpened={opened} onClose={this.close} onReject={this.close} diff --git a/src/components/SettingsPage/DisablePasswordModal.js b/src/components/SettingsPage/DisablePasswordModal.js index b9613c42..89d04a01 100644 --- a/src/components/SettingsPage/DisablePasswordModal.js +++ b/src/components/SettingsPage/DisablePasswordModal.js @@ -8,7 +8,8 @@ import Box from 'components/base/Box' import Button from 'components/base/Button' import InputPassword from 'components/base/InputPassword' import Label from 'components/base/Label' -import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from 'components/base/Modal' +import Modal from 'components/base/Modal' +import ModalBody from 'components/base/Modal/ModalBody' import type { T } from 'types/common' @@ -61,37 +62,33 @@ class DisablePasswordModal extends PureComponent<Props, State> { const { t, onClose, ...props } = this.props const { currentPassword, incorrectPassword } = this.state return ( - <Modal - {...props} - onHide={this.handleReset} - onClose={onClose} - render={({ onClose }) => ( - <form onSubmit={this.disablePassword}> - <ModalBody onClose={onClose}> - <ModalTitle data-e2e="disablePassword_modalTitle"> - {t('password.disablePassword.title')} - </ModalTitle> - <ModalContent> - <Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" px={4}> - {t('password.disablePassword.desc')} - <Box px={7} mt={4} flow={3}> - <Box flow={1}> - <Label htmlFor="password"> - {t('password.inputFields.currentPassword.label')} - </Label> - <InputPassword - autoFocus - type="password" - id="password" - onChange={this.handleInputChange('currentPassword')} - value={currentPassword} - error={incorrectPassword} - /> - </Box> + <Modal {...props} centered onHide={this.handleReset} onClose={onClose}> + <form onSubmit={this.disablePassword}> + <ModalBody + onClose={onClose} + title={t('password.disablePassword.title')} + render={() => ( + <Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" px={4}> + {t('password.disablePassword.desc')} + <Box px={7} mt={4} flow={3}> + <Box flow={1}> + <Label htmlFor="password"> + {t('password.inputFields.currentPassword.label')} + </Label> + <InputPassword + autoFocus + type="password" + id="password" + onChange={this.handleInputChange('currentPassword')} + value={currentPassword} + error={incorrectPassword} + /> </Box> </Box> - </ModalContent> - <ModalFooter horizontal align="center" justify="flex-end" flow={2}> + </Box> + )} + renderFooter={() => ( + <Box horizontal align="center" justify="flex-end" flow={2}> <Button small type="button" onClick={onClose}> {t('common.cancel')} </Button> @@ -103,11 +100,11 @@ class DisablePasswordModal extends PureComponent<Props, State> { > {t('common.save')} </Button> - </ModalFooter> - </ModalBody> - </form> - )} - /> + </Box> + )} + /> + </form> + </Modal> ) } } diff --git a/src/components/SettingsPage/PasswordAutoLockSelect.js b/src/components/SettingsPage/PasswordAutoLockSelect.js index 556fa845..8b643caf 100644 --- a/src/components/SettingsPage/PasswordAutoLockSelect.js +++ b/src/components/SettingsPage/PasswordAutoLockSelect.js @@ -30,10 +30,10 @@ class PasswordAutoLockSelect extends PureComponent<Props> { } timeouts = [ - { value: 1, label: `1 ${this.props.t('app:time.minute')}` }, - { value: 10, label: `10 ${this.props.t('app:time.minute')}s` }, - { value: 30, label: `30 ${this.props.t('app:time.minute')}s` }, - { value: 60, label: `1 ${this.props.t('app:time.hour')}` }, + { value: 1, label: `1 ${this.props.t('time.minute')}` }, + { value: 10, label: `10 ${this.props.t('time.minute')}s` }, + { value: 30, label: `30 ${this.props.t('time.minute')}s` }, + { value: 60, label: `1 ${this.props.t('time.hour')}` }, { value: -1, label: this.props.t(`app:common.never`) }, ] diff --git a/src/components/SettingsPage/PasswordModal.js b/src/components/SettingsPage/PasswordModal.js index c55e4056..1b8c3909 100644 --- a/src/components/SettingsPage/PasswordModal.js +++ b/src/components/SettingsPage/PasswordModal.js @@ -1,6 +1,6 @@ // @flow -import React, { PureComponent } from 'react' +import React, { Fragment, PureComponent } from 'react' import type { T } from 'types/common' @@ -8,7 +8,7 @@ import db from 'helpers/db' import { PasswordIncorrectError } from '@ledgerhq/errors' import Box from 'components/base/Box' import Button from 'components/base/Button' -import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from 'components/base/Modal' +import Modal, { ModalBody } from 'components/base/Modal' import PasswordForm from './PasswordForm' @@ -17,6 +17,7 @@ type Props = { onClose: () => void, onChangePassword: (?string) => void, hasPassword: boolean, + isOpened: boolean, } type State = { @@ -36,6 +37,13 @@ const INITIAL_STATE = { class PasswordModal extends PureComponent<Props, State> { state = INITIAL_STATE + componentWillReceiveProps(nextProps: Props) { + if (!nextProps.isOpened) { + // CLean the state? + this.setState(prevState => ({ ...prevState, ...INITIAL_STATE })) + } + } + handleSave = (e: SyntheticEvent<HTMLFormElement>) => { const { currentPassword, newPassword } = this.state @@ -73,23 +81,17 @@ class PasswordModal extends PureComponent<Props, State> { } render() { - const { t, hasPassword, onClose, ...props } = this.props + const { t, hasPassword, onClose, isOpened, ...props } = this.props const { currentPassword, newPassword, incorrectPassword, confirmPassword } = this.state return ( - <Modal - {...props} - onHide={this.handleReset} - onClose={onClose} - render={({ onClose }) => ( - <ModalBody onClose={onClose}> - {hasPassword ? ( - <ModalTitle>{t('password.changePassword.title')}</ModalTitle> - ) : ( - <ModalTitle data-e2e="enablePassword_modal"> - {t('password.setPassword.title')} - </ModalTitle> - )} - <ModalContent> + <Modal isOpened={isOpened} centered> + <ModalBody + {...props} + title={hasPassword ? t('password.changePassword.title') : t('password.setPassword.title')} + onHide={this.handleReset} + onClose={onClose} + render={() => ( + <Fragment> <Box ff="Museo Sans|Regular" color="dark" @@ -116,8 +118,10 @@ class PasswordModal extends PureComponent<Props, State> { onChange={this.handleInputChange} t={t} /> - </ModalContent> - <ModalFooter horizontal align="center" justify="flex-end" flow={2}> + </Fragment> + )} + renderFooter={() => ( + <Box horizontal align="center" justify="flex-end" flow={2}> <Button small type="button" @@ -134,10 +138,10 @@ class PasswordModal extends PureComponent<Props, State> { > {t('common.save')} </Button> - </ModalFooter> - </ModalBody> - )} - /> + </Box> + )} + /> + </Modal> ) } } diff --git a/src/components/SettingsPage/RepairDeviceButton.js b/src/components/SettingsPage/RepairDeviceButton.js index 511bed6e..1a26d384 100644 --- a/src/components/SettingsPage/RepairDeviceButton.js +++ b/src/components/SettingsPage/RepairDeviceButton.js @@ -10,7 +10,7 @@ import { push } from 'react-router-redux' import type { T } from 'types/common' import firmwareRepair from 'commands/firmwareRepair' import Button from 'components/base/Button' -import { RepairModal } from 'components/base/Modal' +import RepairModal from 'components/base/Modal/RepairModal' type Props = { t: T, diff --git a/src/components/SettingsPage/ResetButton.js b/src/components/SettingsPage/ResetButton.js index ba6745b5..2708c75e 100644 --- a/src/components/SettingsPage/ResetButton.js +++ b/src/components/SettingsPage/ResetButton.js @@ -9,7 +9,7 @@ import type { T } from 'types/common' import { hardReset } from 'helpers/reset' import Box from 'components/base/Box' import Button from 'components/base/Button' -import { ConfirmModal } from 'components/base/Modal' +import ConfirmModal from 'components/base/Modal/ConfirmModal' import IconTriangleWarning from 'icons/TriangleWarning' import ResetFallbackModal from './ResetFallbackModal' @@ -58,6 +58,7 @@ class ResetButton extends PureComponent<Props, State> { <ConfirmModal analyticsName="HardReset" isDanger + centered isLoading={pending} isOpened={opened} onClose={this.close} diff --git a/src/components/SettingsPage/ResetFallbackModal.js b/src/components/SettingsPage/ResetFallbackModal.js index fece2e66..7ada15a9 100644 --- a/src/components/SettingsPage/ResetFallbackModal.js +++ b/src/components/SettingsPage/ResetFallbackModal.js @@ -3,7 +3,7 @@ import React, { PureComponent } from 'react' import { translate } from 'react-i18next' -import { ConfirmModal } from 'components/base/Modal' +import ConfirmModal from 'components/base/Modal/ConfirmModal' import { openUserDataFolderAndQuit } from 'helpers/reset' type Props = { diff --git a/src/components/SettingsPage/sections/Display.js b/src/components/SettingsPage/sections/Display.js index 0335c741..176cb629 100644 --- a/src/components/SettingsPage/sections/Display.js +++ b/src/components/SettingsPage/sections/Display.js @@ -91,8 +91,8 @@ class TabGeneral extends PureComponent<Props> { </Row> {hasPassword ? ( <Row - title={t('app:settings.profile.passwordAutoLock')} - desc={t('app:settings.profile.passwordAutoLockDesc')} + title={t('settings.profile.passwordAutoLock')} + desc={t('settings.profile.passwordAutoLockDesc')} > <PasswordAutoLockSelect /> </Row> diff --git a/src/components/SettingsPage/sections/Export.js b/src/components/SettingsPage/sections/Export.js index 1231b31a..7d7bc88b 100644 --- a/src/components/SettingsPage/sections/Export.js +++ b/src/components/SettingsPage/sections/Export.js @@ -10,7 +10,8 @@ import styled from 'styled-components' import { SettingsSection as Section, SettingsSectionHeader as Header } from '../SettingsSection' import IconShare from '../../../icons/Share' import Button from '../../base/Button' -import Modal, { ModalBody, ModalContent, ModalFooter, ModalTitle } from '../../base/Modal' +import Modal from '../../base/Modal' +import ModalBody from '../../base/Modal/ModalBody' import Box from '../../base/Box' import QRCodeExporter from '../../QRCodeExporter' import { BulletRow } from '../../Onboarding/helperComponents' @@ -97,27 +98,32 @@ class SectionExport extends PureComponent<Props, State> { ] return ( - <ModalBody onClose={onClose}> - <ModalTitle>{t('settings.export.modal.title')}</ModalTitle> - <ModalContent flow={2} justify="center" align="center"> - <Box flow={2}> - <QRCodeExporter size={330} /> - </Box> - <Box shrink style={{ width: 330, fontSize: 13, marginTop: 20 }}> - <Text ff="Open Sans|SemiBold" color="dark"> - {t('settings.export.modal.listTitle')} - </Text> + <ModalBody + onClose={onClose} + title={t('settings.export.modal.title')} + render={() => ( + <Box justify="center" align="center"> + <Box flow={2}> + <QRCodeExporter size={330} /> + </Box> + <Box shrink style={{ width: 330, fontSize: 13, marginTop: 20 }}> + <Text ff="Open Sans|SemiBold" color="dark"> + {t('settings.export.modal.listTitle')} + </Text> + </Box> + <Box style={{ width: 330 }}> + {stepsImportMobile.map(step => <BulletRow key={step.key} step={step} />)} + </Box> </Box> - <Box style={{ width: 330 }}> - {stepsImportMobile.map(step => <BulletRow key={step.key} step={step} />)} + )} + renderFooter={() => ( + <Box> + <Button small onClick={onClose} primary> + {t('settings.export.modal.button')} + </Button> </Box> - </ModalContent> - <ModalFooter horizontal align="center" justify="flex-end" flow={2}> - <Button small onClick={onClose} primary> - {t('settings.export.modal.button')} - </Button> - </ModalFooter> - </ModalBody> + )} + /> ) } @@ -139,7 +145,12 @@ class SectionExport extends PureComponent<Props, State> { </Button> } /> - <Modal isOpened={isModalOpened} onClose={this.onModalClose} render={this.renderModal} /> + <Modal + isOpened={isModalOpened} + centered + onClose={this.onModalClose} + render={this.renderModal} + /> </Section> ) } diff --git a/src/components/base/Modal/ConfirmModal.js b/src/components/base/Modal/ConfirmModal.js index 0512a981..416eb5a7 100644 --- a/src/components/base/Modal/ConfirmModal.js +++ b/src/components/base/Modal/ConfirmModal.js @@ -9,7 +9,8 @@ import TrackPage from 'analytics/TrackPage' import Button from 'components/base/Button' import Box from 'components/base/Box' -import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from './index' +import Modal from './index' +import ModalBody from './ModalBody' type Props = { isOpened: boolean, @@ -21,11 +22,13 @@ type Props = { confirmText?: string, cancelText?: string, onReject: Function, + onClose?: Function, onConfirm: Function, t: T, isLoading?: boolean, analyticsName: string, cancellable?: boolean, + centered?: boolean, } class ConfirmModal extends PureComponent<Props> { @@ -43,23 +46,38 @@ class ConfirmModal extends PureComponent<Props> { onConfirm, isLoading, renderIcon, + onClose, t, analyticsName, + centered, ...props } = this.props const realConfirmText = confirmText || t('common.confirm') const realCancelText = cancelText || t('common.cancel') return ( - <Modal - isOpened={isOpened} - preventBackdropClick={isLoading} - {...props} - render={({ onClose }) => ( - <ModalBody onClose={!cancellable && isLoading ? undefined : onClose}> - <TrackPage category="Modal" name={analyticsName} /> - <ModalTitle>{title}</ModalTitle> - <ModalContent> + <Modal isOpened={isOpened} centered={centered}> + <ModalBody + preventBackdropClick={isLoading} + {...props} + onClose={!cancellable && isLoading ? undefined : onClose} + title={title} + renderFooter={() => ( + <Box horizontal align="center" justify="flex-end" flow={2}> + {!isLoading && <Button onClick={onReject}>{realCancelText}</Button>} + <Button + onClick={onConfirm} + primary={!isDanger} + danger={isDanger} + isLoading={isLoading} + disabled={isLoading} + > + {realConfirmText} + </Button> + </Box> + )} + render={() => ( + <Box> {subTitle && ( <Box ff="Museo Sans|Regular" color="dark" textAlign="center" mb={2} mt={3}> {subTitle} @@ -73,22 +91,11 @@ class ConfirmModal extends PureComponent<Props> { <Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center"> {desc} </Box> - </ModalContent> - <ModalFooter horizontal align="center" justify="flex-end" flow={2}> - {!isLoading && <Button onClick={onReject}>{realCancelText}</Button>} - <Button - onClick={onConfirm} - primary={!isDanger} - danger={isDanger} - isLoading={isLoading} - disabled={isLoading} - > - {realConfirmText} - </Button> - </ModalFooter> - </ModalBody> - )} - /> + </Box> + )} + /> + <TrackPage category="Modal" name={analyticsName} /> + </Modal> ) } } diff --git a/src/components/base/Modal/ModalBody.js b/src/components/base/Modal/ModalBody.js index c0083590..363f10aa 100644 --- a/src/components/base/Modal/ModalBody.js +++ b/src/components/base/Modal/ModalBody.js @@ -1,95 +1,102 @@ // @flow -import React, { PureComponent } from 'react' -import styled, { keyframes } from 'styled-components' +import React, { PureComponent, Fragment } from 'react' +import Animated from 'animated/lib/targets/react-dom' +import { findDOMNode } from 'react-dom' -import Box from 'components/base/Box' -import IconCross from 'icons/Cross' +import ModalContent from './ModalContent' +import ModalHeader from './ModalHeader' +import ModalFooter from './ModalFooter' -export const Container = styled(Box).attrs({ - px: 5, - pb: 5, -})`` +import type { RenderProps } from './index' type Props = { - deferHeight?: number, - onClose?: Function, - children: any, + title: string, + onBack?: void => void, + onClose?: void => void, + render?: (?RenderProps) => any, + renderFooter?: (?RenderProps) => any, + renderProps?: RenderProps, + noScroll?: boolean, + refocusWhenChange?: any, } type State = { - isHidden: boolean, + animGradient: Animated.Value, } class ModalBody extends PureComponent<Props, State> { - static defaultProps = { - onClose: undefined, + state = { + animGradient: new Animated.Value(0), } - state = { - isHidden: true, + componentDidUpdate(prevProps: Props) { + const shouldFocus = prevProps.refocusWhenChange !== this.props.refocusWhenChange + if (shouldFocus) { + if (this._content) { + const node = findDOMNode(this._content) // eslint-disable-line react/no-find-dom-node + if (node) { + // $FlowFixMe + node.focus() + } + } + } } - componentDidMount() { - setTimeout(() => { - window.requestAnimationFrame(() => { - this.setState({ isHidden: false }) - }) - }, 150) + _content = null + + animateGradient = (isScrollable: boolean) => { + const anim = { + duration: 150, + toValue: isScrollable ? 1 : 0, + } + Animated.timing(this.state.animGradient, anim).start() } render() { - const { children, onClose, deferHeight, ...props } = this.props - const { isHidden } = this.state + const { onBack, onClose, title, render, renderFooter, renderProps, noScroll } = this.props + const { animGradient } = this.state + + const gradientStyle = { + ...GRADIENT_STYLE, + opacity: animGradient, + } + return ( - <Body - style={{ height: isHidden && deferHeight ? deferHeight : undefined }} - data-e2e="modalBody" - > - {onClose && ( - <CloseContainer onClick={onClose}> - <IconCross size={16} /> - </CloseContainer> - )} - {(!isHidden || !deferHeight) && <Inner {...props}>{children}</Inner>} - </Body> + <Fragment> + <ModalHeader onBack={onBack} onClose={onClose}> + {title} + </ModalHeader> + <ModalContent + tabIndex={0} + ref={n => (this._content = n)} + onIsScrollableChange={this.animateGradient} + noScroll={noScroll} + > + {render && render(renderProps)} + </ModalContent> + <div style={GRADIENT_WRAPPER_STYLE}> + <Animated.div style={gradientStyle} /> + </div> + {renderFooter && <ModalFooter>{renderFooter(renderProps)}</ModalFooter>} + </Fragment> ) } } -const CloseContainer = styled(Box).attrs({ - p: 4, - color: 'fog', -})` - position: absolute; - top: 0; - right: 0; - z-index: 1; - - &:hover { - color: ${p => p.theme.colors.grey}; - } - - &:active { - color: ${p => p.theme.colors.dark}; - } -` - -const Body = styled(Box).attrs({ - bg: p => p.theme.colors.white, - relative: true, - borderRadius: 1, -})` - box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2); -` - -const appear = keyframes` - from { opacity: 0; } - to { opacity: 1; } -` +const GRADIENT_STYLE = { + background: 'linear-gradient(rgba(255, 255, 255, 0), #ffffff)', + height: 40, + position: 'absolute', + bottom: 0, + left: 0, + right: 6, +} -const Inner = styled(Box)` - animation: ${appear} 80ms linear; -` +const GRADIENT_WRAPPER_STYLE = { + height: 0, + position: 'relative', + pointerEvents: 'none', +} export default ModalBody diff --git a/src/components/base/Modal/ModalContent.js b/src/components/base/Modal/ModalContent.js new file mode 100644 index 00000000..7b74ddd1 --- /dev/null +++ b/src/components/base/Modal/ModalContent.js @@ -0,0 +1,59 @@ +// @flow + +/* eslint-disable jsx-a11y/no-noninteractive-tabindex */ + +import React, { PureComponent } from 'react' + +class ModalContent extends PureComponent<{ + children: any, + onIsScrollableChange: boolean => void, + noScroll?: boolean, +}> { + componentDidMount() { + window.requestAnimationFrame(() => { + if (this._isUnmounted) return + this.showHideGradient() + if (this._outer) { + const ro = new ResizeObserver(this.showHideGradient) + ro.observe(this._outer) + } + }) + } + + componentWillUnmount() { + this._isUnmounted = true + } + + _outer = null + _isUnmounted = false + + showHideGradient = () => { + if (!this._outer) return + const { onIsScrollableChange } = this.props + const isScrollable = this._outer.scrollHeight > this._outer.clientHeight + onIsScrollableChange(isScrollable) + } + + render() { + const { children, noScroll } = this.props + + const contentStyle = { + ...CONTENT_STYLE, + overflow: noScroll ? 'visible' : 'auto', + } + + return ( + <div style={contentStyle} ref={n => (this._outer = n)} tabIndex={0}> + {children} + </div> + ) + } +} + +const CONTENT_STYLE = { + flexShrink: 1, + padding: 20, + paddingBottom: 40, +} + +export default ModalContent diff --git a/src/components/base/Modal/ModalFooter.js b/src/components/base/Modal/ModalFooter.js new file mode 100644 index 00000000..97f7c083 --- /dev/null +++ b/src/components/base/Modal/ModalFooter.js @@ -0,0 +1,18 @@ +// @flow + +import React from 'react' + +import { colors } from 'styles/theme' + +const MODAL_FOOTER_STYLE = { + display: 'flex', + justifyContent: 'flex-end', + borderTop: `2px solid ${colors.lightGrey}`, + padding: 20, +} + +const ModalFooter = ({ children }: { children: any }) => ( + <div style={MODAL_FOOTER_STYLE}>{children}</div> +) + +export default ModalFooter diff --git a/src/components/base/Modal/ModalHeader.js b/src/components/base/Modal/ModalHeader.js new file mode 100644 index 00000000..e3078e8d --- /dev/null +++ b/src/components/base/Modal/ModalHeader.js @@ -0,0 +1,93 @@ +// @flow + +import React from 'react' +import styled from 'styled-components' +import { translate } from 'react-i18next' + +import type { T } from 'types/common' + +import Box from 'components/base/Box' + +import IconAngleLeft from 'icons/AngleLeft' +import IconCross from 'icons/Cross' + +const MODAL_HEADER_STYLE = { + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: 20, +} + +const ModalTitle = styled(Box).attrs({ + color: 'dark', + ff: 'Museo Sans|Regular', + fontSize: 6, + grow: true, + shrink: true, +})` + text-align: center; + line-height: 1; +` + +const iconAngleLeft = <IconAngleLeft size={16} /> +const iconCross = <IconCross size={16} /> + +const ModalHeaderAction = styled(Box).attrs({ + horizontal: true, + align: 'center', + fontSize: 3, + p: 4, + color: 'grey', +})` + position: absolute; + top: 0; + left: ${p => (p.right ? 'auto' : 0)}; + right: ${p => (p.right ? 0 : 'auto')}; + line-height: 0; + cursor: pointer; + + &:hover { + color: ${p => p.theme.colors.graphite}; + } + + &:active { + color: ${p => p.theme.colors.dark}; + } + + span { + border-bottom: 1px dashed transparent; + } + &:focus span { + border-bottom-color: inherit; + } +` + +const ModalHeader = ({ + children, + onBack, + onClose, + t, +}: { + children: any, + onBack: void => void, + onClose: void => void, + t: T, +}) => ( + <div style={MODAL_HEADER_STYLE}> + {onBack && ( + <ModalHeaderAction onClick={onBack}> + {iconAngleLeft} + <span>{t('common.back')}</span> + </ModalHeaderAction> + )} + <ModalTitle>{children}</ModalTitle> + {onClose && ( + <ModalHeaderAction right color="fog" onClick={onClose}> + {iconCross} + </ModalHeaderAction> + )} + </div> +) + +export default translate()(ModalHeader) diff --git a/src/components/base/Modal/ModalTitle.js b/src/components/base/Modal/ModalTitle.js deleted file mode 100644 index 55cf2fb4..00000000 --- a/src/components/base/Modal/ModalTitle.js +++ /dev/null @@ -1,75 +0,0 @@ -// @flow - -import React from 'react' -import styled from 'styled-components' -import { translate } from 'react-i18next' - -import type { T } from 'types/common' - -import Box from 'components/base/Box' -import IconAngleLeft from 'icons/AngleLeft' - -const Container = styled(Box).attrs({ - alignItems: 'center', - color: 'dark', - ff: 'Museo Sans|Regular', - fontSize: 6, - justifyContent: 'center', - p: 5, - relative: true, -})`` - -const Back = styled(Box).attrs({ - unstyled: true, - horizontal: true, - align: 'center', - color: 'grey', - ff: 'Open Sans', - fontSize: 3, - p: 4, -})` - position: absolute; - line-height: 1; - top: 0; - left: 0; - - &:hover { - color: ${p => p.theme.colors.graphite}; - } - - &:active { - color: ${p => p.theme.colors.dark}; - } - - span { - border-bottom: 1px dashed transparent; - } - &:focus span { - border-bottom-color: inherit; - } -` - -function ModalTitle({ - t, - onBack, - children, - ...props -}: { - t: T, - onBack: any => void, - children: any, -}) { - return ( - <Container {...props} data-e2e="modal_title"> - {onBack && ( - <Back onClick={onBack}> - <IconAngleLeft size={16} /> - <span>{t('common.back')}</span> - </Back> - )} - {children} - </Container> - ) -} - -export default translate()(ModalTitle) diff --git a/src/components/base/Modal/RepairModal.js b/src/components/base/Modal/RepairModal.js index a69d839c..bf01337e 100644 --- a/src/components/base/Modal/RepairModal.js +++ b/src/components/base/Modal/RepairModal.js @@ -17,7 +17,8 @@ import ProgressCircle from 'components/ProgressCircle' import TranslatedError from 'components/TranslatedError' import ExclamationCircleThin from 'icons/ExclamationCircleThin' -import { Modal, ModalContent, ModalBody, ModalTitle, ModalFooter } from './index' +import Modal from './index' +import ModalBody from './ModalBody' const Container = styled(Box).attrs({ alignItems: 'center', @@ -39,18 +40,18 @@ const Separator = styled(Box).attrs({ ` const DisclaimerStep = ({ desc }: { desc?: string }) => ( - <ModalContent> + <Box> {desc ? ( <Box ff="Open Sans" color="smoke" fontSize={4} textAlign="center" mb={2}> {desc} </Box> ) : null} - </ModalContent> + </Box> ) const FlashStep = ({ progress, t }: { progress: number, t: * }) => progress === 0 ? ( - <ModalContent> + <Box> <Box mx={7}> <Text ff="Open Sans|Regular" align="center" color="smoke"> <Bullet>{'1.'}</Bullet> @@ -74,9 +75,9 @@ const FlashStep = ({ progress, t }: { progress: number, t: * }) => alt={t('manager.modal.mcuFirst')} /> </Box> - </ModalContent> + </Box> ) : ( - <ModalContent> + <Box> <Box mx={7} align="center"> <ProgressCircle size={64} progress={progress} /> </Box> @@ -88,11 +89,11 @@ const FlashStep = ({ progress, t }: { progress: number, t: * }) => {t('manager.modal.mcuPin')} </Text> </Box> - </ModalContent> + </Box> ) const ErrorStep = ({ error }: { error: Error }) => ( - <ModalContent> + <Box> <Container> <Box color="alertRed"> <ExclamationCircleThin size={44} /> @@ -118,7 +119,7 @@ const ErrorStep = ({ error }: { error: Error }) => ( <TranslatedError error={error} field="description" /> </Box> </Container> - </ModalContent> + </Box> ) type Props = { @@ -177,38 +178,44 @@ class RepairModal extends PureComponent<Props, *> { return ( <Modal isOpened={isOpened} + centered preventBackdropClick={isLoading} + onClose={!cancellable && isLoading ? undefined : onReject} {...props} - render={({ onClose }) => ( - <ModalBody onClose={!cancellable && isLoading ? undefined : onClose}> - <TrackPage category="Modal" name={analyticsName} /> - <ModalTitle>{title}</ModalTitle> - {error ? ( - <ErrorStep error={error} /> - ) : isLoading ? ( - <FlashStep t={t} progress={progress} /> - ) : ( - <DisclaimerStep desc={desc} /> - )} - - {!isLoading && !error ? ( - <Box py={2} px={5}> - <Select - isSearchable={false} - isClearable={false} - value={selectedOption} - onChange={this.onChange} - autoFocus - options={forceRepairChoices} - renderOption={this.renderOption} - renderValue={this.renderValue} - /> - </Box> - ) : null} - - {!isLoading ? ( - <ModalFooter horizontal align="center" justify="flex-end" flow={2}> - {error ? <Button onClick={onReject}>{t(`common.close`)}</Button> : null} + > + <TrackPage category="Modal" name={analyticsName} /> + <ModalBody + title={title} + render={() => ( + <Box> + {error ? ( + <ErrorStep error={error} /> + ) : isLoading ? ( + <FlashStep t={t} progress={progress} /> + ) : ( + <DisclaimerStep desc={desc} /> + )} + + {!isLoading && !error ? ( + <Box py={2} px={5}> + <Select + isSearchable={false} + isClearable={false} + value={selectedOption} + onChange={this.onChange} + autoFocus + options={forceRepairChoices} + renderOption={this.renderOption} + renderValue={this.renderValue} + /> + </Box> + ) : null} + </Box> + )} + renderFooter={() => + !isLoading ? ( + <Box horizontal align="center" justify="flex-end" flow={2}> + <Button onClick={onReject}>{t(`common.${error ? 'close' : 'cancel'}`)}</Button> {error ? null : ( <> <Button @@ -222,11 +229,11 @@ class RepairModal extends PureComponent<Props, *> { </Button> </> )} - </ModalFooter> - ) : null} - </ModalBody> - )} - /> + </Box> + ) : null + } + /> + </Modal> ) } } diff --git a/src/components/base/Modal/index.js b/src/components/base/Modal/index.js index cad43fda..ff0ae518 100644 --- a/src/components/base/Modal/index.js +++ b/src/components/base/Modal/index.js @@ -1,54 +1,28 @@ // @flow -/* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable react/no-multi-comp */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ -import React, { Component } from 'react' -import { findDOMNode } from 'react-dom' +import React, { PureComponent, Fragment } from 'react' +import { createPortal } from 'react-dom' import { connect } from 'react-redux' -import Mortal from 'react-mortal' -import styled from 'styled-components' import noop from 'lodash/noop' -import { EXPERIMENTAL_CENTER_MODAL } from 'config/constants' - -import { rgba } from 'styles/helpers' -import { radii } from 'styles/theme' +import Animated from 'animated/lib/targets/react-dom' +import Easing from 'animated/lib/Easing' import { closeModal, isModalOpened, getModalData } from 'reducers/modals' - -import Box from 'components/base/Box' -import GrowScroll from 'components/base/GrowScroll' +import { colors } from 'styles/theme' export { default as ModalBody } from './ModalBody' -export { default as ConfirmModal } from './ConfirmModal' -export { default as RepairModal } from './RepairModal' -export { default as ModalTitle } from './ModalTitle' -const springConfig = { - stiffness: 320, +const animShowHide = { + duration: 200, + easing: Easing.bezier(0.3, 1.0, 0.5, 0.8), } -type OwnProps = { - name?: string, // eslint-disable-line - isOpened?: boolean, - onBeforeOpen?: ({ data: * }) => *, // eslint-disable-line - onClose?: () => void, - onHide?: () => void, - preventBackdropClick?: boolean, - render: Function, - refocusWhenChange?: string, - width?: string, -} +const domNode = process.env.STORYBOOK_ENV ? document.body : document.getElementById('modals') -type Props = OwnProps & { - isOpened?: boolean, - data?: any, -} & { - onClose?: () => void, -} - -const mapStateToProps = (state, { name, isOpened, onBeforeOpen }: OwnProps): * => { +const mapStateToProps = (state, { name, isOpened, onBeforeOpen }: Props): * => { const data = getModalData(state, name || '') const modalOpened = isOpened || (name && isModalOpened(state, name)) @@ -62,7 +36,7 @@ const mapStateToProps = (state, { name, isOpened, onBeforeOpen }: OwnProps): * = } } -const mapDispatchToProps = (dispatch: *, { name, onClose = noop }: OwnProps): * => ({ +const mapDispatchToProps = (dispatch: *, { name, onClose = noop }: Props): * => ({ onClose: name ? () => { dispatch(closeModal(name)) @@ -71,178 +45,187 @@ const mapDispatchToProps = (dispatch: *, { name, onClose = noop }: OwnProps): * : onClose, }) -const Container = styled(Box).attrs({ - color: 'grey', - sticky: true, - style: p => ({ - pointerEvents: p.isVisible ? 'auto' : 'none', - }), -})` - position: fixed; - z-index: 30; -` - -const Backdrop = styled(Box).attrs({ - bg: p => rgba(p.theme.colors.black, 0.4), - sticky: true, - style: p => ({ - opacity: p.op, - }), -})` - position: fixed; -` - -const NonClickableHeadArea = styled.div` - position: fixed; - height: 48px; - width: 100%; - top: 0; - left: 0; - z-index: 1; -` - -const Wrapper = styled(Box).attrs({ - bg: 'transparent', - flow: 4, - style: p => ({ - opacity: p.op, - transform: `scale3d(${p.scale}, ${p.scale}, ${p.scale})`, - }), -})` - outline: none; - width: ${p => (p.width ? p.width : '500px')}; - z-index: 2; -` - -class Pure extends Component<any> { - shouldComponentUpdate(nextProps) { - if (nextProps.isAnimated) { - return false - } - - return true - } +export type RenderProps = { + onClose?: void => void, + data: any, +} - render() { - const { data, onClose, render } = this.props +type Props = { + isOpened?: boolean, + children?: any, + centered?: boolean, + onClose?: void => void, + onHide?: void => void, + render?: RenderProps => any, + data?: any, + preventBackdropClick?: boolean, - return render({ data, onClose }) - } + name?: string, // eslint-disable-line + onBeforeOpen?: ({ data: * }) => *, // eslint-disable-line } -function stopPropagation(e) { - e.stopPropagation() +type State = { + animShowHide: Animated.Value, + isInDOM: boolean, } -const wrap = EXPERIMENTAL_CENTER_MODAL - ? children => ( - <Box alignItems="center" justifyContent="center" grow> - {children} - </Box> - ) - : children => ( - <GrowScroll alignItems="center" full pt={8}> - {children} - </GrowScroll> - ) +class Modal extends PureComponent<Props, State> { + state = { + animShowHide: new Animated.Value(0), + isInDOM: this.props.isOpened === true, + } -export class Modal extends Component<Props> { - static defaultProps = { - isOpened: false, - onHide: noop, - preventBackdropClick: false, + static getDerivedStateFromProps(nextProps: Props) { + const patch = {} + if (nextProps.isOpened) { + patch.isInDOM = true + } + return patch } - shouldComponentUpdate(nextProps: Props) { - if (this.props.isOpened || nextProps.isOpened) { - return true + componentDidMount() { + if (this.props.isOpened) { + this.animateEnter() } - return false + this.state.animShowHide.addListener(({ value }) => { + if (value === 0) { + const { onHide } = this.props + this.setState({ isInDOM: false }) + if (onHide) { + onHide() + } + } + if (value === 1) this.setState({ isInDOM: true }) + }) + + document.addEventListener('keyup', this.handleKeyup) } componentDidUpdate(prevProps: Props) { - const didOpened = this.props.isOpened && !prevProps.isOpened - const didClose = !this.props.isOpened && prevProps.isOpened - const shouldFocus = didOpened || this.props.refocusWhenChange !== prevProps.refocusWhenChange + const didOpened = !prevProps.isOpened && this.props.isOpened + const didClosed = prevProps.isOpened && !this.props.isOpened + if (didOpened) { - // Store a reference to the last active element, to restore it after - // modal close - this._lastFocusedElement = document.activeElement - } - if (shouldFocus) { - this.focusWrapper() + this.animateEnter() } - if (didClose) { - if (this._lastFocusedElement) { - this._lastFocusedElement.focus() - } + if (didClosed) { + this.animateLeave() } } - _wrapper = null - _lastFocusedElement = null + componentWillUnmount() { + document.removeEventListener('keyup', this.handleKeyup) + } + + handleKeyup = (e: KeyboardEvent) => { + const { onClose, preventBackdropClick } = this.props + if (e.which === 27 && onClose && !preventBackdropClick) { + onClose() + } + } - focusWrapper = () => { - // Forced to use findDOMNode here, because innerRef is giving a proxied component - const domWrapper = findDOMNode(this._wrapper) // eslint-disable-line react/no-find-dom-node - if (domWrapper instanceof HTMLDivElement && !domWrapper.contains(this._lastFocusedElement)) { - domWrapper.focus() + handleClickOnBackdrop = () => { + const { preventBackdropClick, onClose } = this.props + if (!preventBackdropClick && onClose) { + onClose() } } + swallowClick = e => { + e.preventDefault() + e.stopPropagation() + } + + animateEnter = () => + Animated.timing(this.state.animShowHide, { ...animShowHide, toValue: 1 }).start() + + animateLeave = () => + Animated.timing(this.state.animShowHide, { ...animShowHide, toValue: 0 }).start() + render() { - const { preventBackdropClick, isOpened, onHide, render, data, onClose, width } = this.props - - return ( - <Mortal - isOpened={isOpened} - onClose={onClose} - onHide={onHide} - closeOnEsc={!preventBackdropClick} - motionStyle={(spring, isVisible) => ({ - opacity: spring(isVisible ? 1 : 0, springConfig), - scale: spring(isVisible ? 1 : 0.95, springConfig), - })} - > - {(m, isVisible, isAnimated) => ( - <Container isVisible={isVisible} onClick={preventBackdropClick ? undefined : onClose}> - <Backdrop op={m.opacity} /> - <NonClickableHeadArea onClick={stopPropagation} /> - {wrap( - <Wrapper - tabIndex={-1} - op={m.opacity} - scale={m.scale} - innerRef={n => (this._wrapper = n)} - onClick={stopPropagation} - width={width} - > - <Pure isAnimated={isAnimated} render={render} data={data} onClose={onClose} /> - </Wrapper>, - )} - </Container> - )} - </Mortal> + const { animShowHide, isInDOM } = this.state + const { children, render, centered, onClose, data, isOpened } = this.props + + if (!isInDOM) { + return null + } + + const backdropStyle = { + ...BACKDROP_STYLE, + opacity: animShowHide, + } + + const containerStyle = { + ...CONTAINER_STYLE, + justifyContent: centered ? 'center' : 'flex-start', + pointerEvents: isOpened ? 'auto' : 'none', + } + + const scale = animShowHide.interpolate({ + inputRange: [0, 1], + outputRange: [1.1, 1], + clamp: true, + }) + + const bodyWrapperStyle = { + ...BODY_WRAPPER_STYLE, + opacity: animShowHide, + transform: [{ scale }], + } + + const renderProps = { + onClose, + data, + } + + const modal = ( + <Fragment> + <Animated.div style={backdropStyle} /> + <div style={containerStyle} onClick={this.handleClickOnBackdrop}> + <Animated.div style={bodyWrapperStyle} onClick={this.swallowClick}> + {render && render(renderProps)} + {children} + </Animated.div> + </div> + </Fragment> ) + + return domNode ? createPortal(modal, domNode) : null } } -export const ModalFooter = styled(Box).attrs({ - px: 5, - py: 3, -})` - border-top: 2px solid ${p => p.theme.colors.lightGrey}; - border-bottom-left-radius: ${radii[1]}px; - border-bottom-right-radius: ${radii[1]}px; -` - -export const ModalContent = styled(Box).attrs({ - px: 5, - pb: 5, - selectable: true, -})`` +const BACKDROP_STYLE = { + pointerEvents: 'none', + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'rgba(0, 0, 0, 0.4)', + zIndex: 100, +} + +const CONTAINER_STYLE = { + ...BACKDROP_STYLE, + background: 'transparent', + padding: '60px 0 60px 0', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', +} + +const BODY_WRAPPER_STYLE = { + background: 'white', + width: 500, + borderRadius: 3, + boxShadow: 'box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2)', + color: colors.smoke, + flexShrink: 1, + display: 'flex', + flexDirection: 'column', +} export default connect( mapStateToProps, diff --git a/src/components/base/Modal/stories.js b/src/components/base/Modal/stories.js index 9e946800..2eb46bb4 100644 --- a/src/components/base/Modal/stories.js +++ b/src/components/base/Modal/stories.js @@ -2,50 +2,57 @@ import React from 'react' import { storiesOf } from '@storybook/react' -import { action } from '@storybook/addon-actions' import { boolean, text } from '@storybook/addon-knobs' +import { action } from '@storybook/addon-actions' -import { - Modal, - ModalBody, - ModalTitle, - ModalContent, - ModalFooter, - ConfirmModal, -} from 'components/base/Modal' +import Modal from 'components/base/Modal' +import ModalBody from 'components/base/Modal/ModalBody' +import Input from 'components/base/Input' +import Label from 'components/base/Label' import Box from 'components/base/Box' -import Button from 'components/base/Button' const stories = storiesOf('Components/base', module) stories.add('Modal', () => ( <Modal isOpened={boolean('isOpened', true)} + centered={boolean('centered', true)} + onClose={action('onClose')} render={({ onClose }) => ( - <ModalBody onClose={onClose}> - <ModalTitle>{'modal title'}</ModalTitle> - <ModalContent>{'this is the modal content'}</ModalContent> - <ModalFooter horizontal align="center"> - <Box grow>{'modal footer'}</Box> - <Button primary>{'Next'}</Button> - </ModalFooter> - </ModalBody> - )} - /> -)) - -stories.add('ConfirmModal', () => ( - <ConfirmModal - categoryName="" - isOpened - isDanger={boolean('isDanger', false)} - title={text('title', 'Hard reset')} - subTitle={text('subTitle', 'Are you sure houston?')} - desc={text( - 'desc', - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh diam. In eget ipsum arcu donec finibus', + <ModalBody + onClose={onClose} + onBack={action('onBack')} + title={text('title', 'Send funds')} + render={() => ( + <Box flow={4}> + <Box flow={2}> + <Label>{'first field'}</Label> + <Input autoFocus /> + </Box> + <Box horizontal flow={4}> + <Box flow={2} flex={1}> + <Label>{'second field'}</Label> + <Input /> + </Box> + <Box flow={2} flex={1}> + <Label>{'third field'}</Label> + <Input /> + </Box> + </Box> + <Box horizontal flow={4}> + <Box flow={2} flex={1}> + <Label>{'second field'}</Label> + <Input /> + </Box> + <Box flow={2} flex={1}> + <Label>{'third field'}</Label> + <Input /> + </Box> + </Box> + </Box> + )} + renderFooter={() => 'footer'} + /> )} - onConfirm={action('onConfirm')} - onReject={action('onReject')} /> )) diff --git a/src/components/base/Stepper/index.js b/src/components/base/Stepper/index.js index 8daf4acb..73511549 100644 --- a/src/components/base/Stepper/index.js +++ b/src/components/base/Stepper/index.js @@ -1,12 +1,12 @@ // @flow -import React, { PureComponent } from 'react' +import React, { PureComponent, Fragment } from 'react' import invariant from 'invariant' import { translate } from 'react-i18next' import type { T } from 'types/common' -import { ModalContent, ModalTitle, ModalFooter, ModalBody } from 'components/base/Modal' +import { ModalBody } from 'components/base/Modal' import Breadcrumb from 'components/Breadcrumb' type Props = { @@ -29,6 +29,7 @@ export type Step = { shouldRenderFooter?: StepProps => boolean, shouldPreventClose?: boolean | (StepProps => boolean), onBack?: StepProps => void, + noScroll?: boolean, } type State = { @@ -72,6 +73,7 @@ class Stepper extends PureComponent<Props, State> { onBack, shouldPreventClose, shouldRenderFooter, + noScroll, } = step const stepProps: StepProps = { @@ -89,25 +91,27 @@ class Stepper extends PureComponent<Props, State> { : !!shouldPreventClose return ( - <ModalBody onClose={preventClose ? undefined : onClose}> - <ModalTitle onBack={onBack ? () => onBack(stepProps) : undefined}>{title}</ModalTitle> - <ModalContent> - <Breadcrumb - mb={6} - currentStep={stepIndex} - items={steps} - stepsDisabled={disabledSteps} - stepsErrors={errorSteps} - /> - <StepComponent {...stepProps} /> - {children} - </ModalContent> - {renderFooter && ( - <ModalFooter horizontal align="center" justify="flex-end"> - <StepFooter {...stepProps} /> - </ModalFooter> + <ModalBody + refocusWhenChange={stepId} + onClose={preventClose ? undefined : onClose} + onBack={onBack ? () => onBack(stepProps) : undefined} + title={title} + noScroll={noScroll} + render={() => ( + <Fragment> + <Breadcrumb + mb={6} + currentStep={stepIndex} + items={steps} + stepsDisabled={disabledSteps} + stepsErrors={errorSteps} + /> + <StepComponent {...stepProps} /> + {children} + </Fragment> )} - </ModalBody> + renderFooter={renderFooter ? () => <StepFooter {...stepProps} /> : undefined} + /> ) } } diff --git a/src/components/modals/AccountSettingRenderBody.js b/src/components/modals/AccountSettingRenderBody.js index eb1e1d0d..552470bd 100644 --- a/src/components/modals/AccountSettingRenderBody.js +++ b/src/components/modals/AccountSettingRenderBody.js @@ -1,6 +1,6 @@ // @flow -import React, { PureComponent } from 'react' +import React, { PureComponent, Fragment } from 'react' import styled from 'styled-components' import { connect } from 'react-redux' import { compose } from 'redux' @@ -28,13 +28,8 @@ import Input from 'components/base/Input' import Select from 'components/base/Select' import SyncAgo from 'components/SyncAgo' -import { - ModalBody, - ModalTitle, - ModalFooter, - ModalContent, - ConfirmModal, -} from 'components/base/Modal' +import ConfirmModal from 'components/base/Modal/ConfirmModal' +import ModalBody from 'components/base/Modal/ModalBody' type State = { accountName: ?string, @@ -74,7 +69,7 @@ const defaultState = { isRemoveAccountModalOpen: false, } -class HelperComp extends PureComponent<Props, State> { +class AccountSettingRenderBody extends PureComponent<Props, State> { state = { ...defaultState, } @@ -84,7 +79,6 @@ class HelperComp extends PureComponent<Props, State> { } getAccount(data: Object): Account { - // FIXME this should be a selector const { accountName } = this.state const account = get(data, 'account', {}) @@ -132,7 +126,6 @@ class HelperComp extends PureComponent<Props, State> { e: SyntheticEvent<HTMLFormElement>, ) => { e.preventDefault() - const { updateAccount, setDataModal } = this.props const { accountName, accountUnit, endpointConfig, endpointConfigError } = this.state @@ -194,10 +187,10 @@ class HelperComp extends PureComponent<Props, State> { endpointConfigError, } = this.state const { t, onClose, data } = this.props + if (!data) return null const account = this.getAccount(data) const bridge = getBridgeForCurrency(account.currency) - const usefulData = { xpub: account.xpub || undefined, index: account.index, @@ -207,11 +200,12 @@ class HelperComp extends PureComponent<Props, State> { } return ( - <ModalBody onClose={onClose}> - <form onSubmit={this.handleSubmit(account, onClose)}> - <TrackPage category="Modal" name="AccountSettings" /> - <ModalTitle>{t('account.settings.title')}</ModalTitle> - <ModalContent mb={3}> + <ModalBody + onClose={onClose} + title={t('account.settings.title')} + render={() => ( + <form onSubmit={this.handleSubmit(account, onClose)}> + <TrackPage category="Modal" name="AccountSettings" /> <Container> <Box> <OptionRowTitle>{t('account.settings.accountName.title')}</OptionRowTitle> @@ -284,8 +278,21 @@ class HelperComp extends PureComponent<Props, State> { value={JSON.stringify(usefulData, null, 2)} /> </Spoiler> - </ModalContent> - <ModalFooter horizontal> + <ConfirmModal + analyticsName="RemoveAccount" + isDanger + isOpened={isRemoveAccountModalOpen} + onClose={this.handleCloseRemoveAccountModal} + onReject={this.handleCloseRemoveAccountModal} + onConfirm={() => this.handleRemoveAccount(account)} + title={t('settings.removeAccountModal.title')} + subTitle={t('common.areYouSure')} + desc={t('settings.removeAccountModal.desc')} + /> + </form> + )} + renderFooter={() => ( + <Fragment> <Button event="OpenAccountDelete" danger @@ -297,20 +304,9 @@ class HelperComp extends PureComponent<Props, State> { <Button event="DoneEditingAccount" ml="auto" type="submit" primary> {t('common.apply')} </Button> - </ModalFooter> - </form> - <ConfirmModal - analyticsName="RemoveAccount" - isDanger - isOpened={isRemoveAccountModalOpen} - onClose={this.handleCloseRemoveAccountModal} - onReject={this.handleCloseRemoveAccountModal} - onConfirm={() => this.handleRemoveAccount(account)} - title={t('settings.removeAccountModal.title')} - subTitle={t('common.areYouSure')} - desc={t('settings.removeAccountModal.desc')} - /> - </ModalBody> + </Fragment> + )} + /> ) } } @@ -321,7 +317,7 @@ export default compose( mapDispatchToProps, ), translate(), -)(HelperComp) +)(AccountSettingRenderBody) export function InputLeft({ currency }: { currency: Currency }) { return ( diff --git a/src/components/modals/AddAccounts/index.js b/src/components/modals/AddAccounts/index.js index e6edd600..0bd842cf 100644 --- a/src/components/modals/AddAccounts/index.js +++ b/src/components/modals/AddAccounts/index.js @@ -43,6 +43,7 @@ const createSteps = () => { footer: StepChooseCurrencyFooter, onBack: null, hideFooter: false, + noScroll: true, }, { id: 'connectDevice', @@ -241,6 +242,7 @@ class AddAccounts extends PureComponent<Props, State> { return ( <Modal + centered name={MODAL_ADD_ACCOUNTS} refocusWhenChange={stepId} onHide={() => this.setState({ ...INITIAL_STATE })} diff --git a/src/components/modals/Debug.js b/src/components/modals/Debug.js index 8ed42391..66362152 100644 --- a/src/components/modals/Debug.js +++ b/src/components/modals/Debug.js @@ -1,11 +1,12 @@ // @flow /* eslint-disable react/jsx-no-literals */ -import React, { Component } from 'react' +import React, { Component, Fragment } from 'react' import { connect } from 'react-redux' import { createStructuredSelector } from 'reselect' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/currencies' import { getDerivationScheme, runDerivationScheme } from '@ledgerhq/live-common/lib/derivation' -import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal' +import Modal from 'components/base/Modal' +import ModalBody from 'components/base/Modal/ModalBody' import { getCurrentDevice } from 'reducers/devices' import Button from 'components/base/Button' import Box from 'components/base/Box' @@ -127,14 +128,12 @@ class Debug extends Component<*, *> { const { device } = this.props const { logs } = this.state return ( - <Modal - name="MODAL_DEBUG" - onHide={this.onHide} - render={({ onClose }: *) => ( - <ModalBody onClose={onClose}> - <SyncSkipUnderPriority priority={99999999} /> - <ModalTitle>developer internal tools</ModalTitle> - <ModalContent> + <Modal name="MODAL_DEBUG" centered onHide={this.onHide}> + <ModalBody + title="developer internal tools" + render={() => ( + <Box> + <SyncSkipUnderPriority priority={99999999} /> <Box style={{ height: 60, overflow: 'auto' }}> {device && ( <Box horizontal style={{ padding: 10 }}> @@ -191,6 +190,7 @@ class Debug extends Component<*, *> { > {logs.map(log => ( <Box + key={log.txt} style={{ userSelect: 'all', color: log.type === 'error' ? '#c22' : '#888', @@ -200,6 +200,10 @@ class Debug extends Component<*, *> { </Box> ))} </Box> + </Box> + )} + renderFooter={() => ( + <Fragment> <Button style={{ position: 'absolute', right: 30, bottom: 28 }} onClick={() => { @@ -208,10 +212,10 @@ class Debug extends Component<*, *> { > Clear </Button> - </ModalContent> - </ModalBody> - )} - /> + </Fragment> + )} + /> + </Modal> ) } } diff --git a/src/components/modals/Disclaimer.js b/src/components/modals/Disclaimer.js index ce340847..7e73dc78 100644 --- a/src/components/modals/Disclaimer.js +++ b/src/components/modals/Disclaimer.js @@ -7,13 +7,21 @@ import type { T } from 'types/common' import { MODAL_DISCLAIMER } from 'config/constants' -import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' +import Modal from 'components/base/Modal' +import ModalBody from 'components/base/Modal/ModalBody' import Button from 'components/base/Button' import Box from 'components/base/Box' import { HandShield } from 'components/WarnBox' +import { compose } from 'redux' +import connect from 'react-redux/es/connect/connect' +import { closeModal } from '../../reducers/modals' type Props = { t: T, + closeModal: string => void, +} +const mapDispatchToProps = { + closeModal, } class DisclaimerModal extends PureComponent<Props> { @@ -21,28 +29,39 @@ class DisclaimerModal extends PureComponent<Props> { const { t } = this.props return ( - <Modal - name={MODAL_DISCLAIMER} - render={({ onClose }) => ( - <ModalBody onClose={onClose}> - <ModalTitle>{t('disclaimerModal.title')}</ModalTitle> - <ModalContent flow={4} ff="Open Sans|Regular" fontSize={4} color="smoke"> + <Modal name={MODAL_DISCLAIMER} centered> + <ModalBody + title={t('disclaimerModal.title')} + render={() => ( + <Box flow={4} ff="Open Sans|Regular" fontSize={4} color="smoke"> <Box align="center" mt={4} pb={4}> <HandShield size={55} /> </Box> <p>{t('disclaimerModal.desc_1')}</p> <p>{t('disclaimerModal.desc_2')}</p> - </ModalContent> - <ModalFooter horizontal justifyContent="flex-end"> - <Button data-e2e="continue_button" onClick={onClose} primary> + </Box> + )} + renderFooter={() => ( + <Box horizontal justifyContent="flex-end"> + <Button + data-e2e="continue_button" + onClick={() => this.props.closeModal(MODAL_DISCLAIMER)} + primary + > {t('disclaimerModal.cta')} </Button> - </ModalFooter> - </ModalBody> - )} - /> + </Box> + )} + /> + </Modal> ) } } -export default translate()(DisclaimerModal) +export default compose( + connect( + null, + mapDispatchToProps, + ), + translate(), +)(DisclaimerModal) diff --git a/src/components/modals/OperationDetails.js b/src/components/modals/OperationDetails.js index bb77163f..b9f55c04 100644 --- a/src/components/modals/OperationDetails.js +++ b/src/components/modals/OperationDetails.js @@ -18,12 +18,10 @@ import { MODAL_OPERATION_DETAILS } from 'config/constants' import { getMarketColor } from 'styles/helpers' import Box from 'components/base/Box' -import GradientBox from 'components/GradientBox' -import GrowScroll from 'components/base/GrowScroll' import Button from 'components/base/Button' import Bar from 'components/base/Bar' import FormattedVal from 'components/base/FormattedVal' -import Modal, { ModalBody, ModalTitle, ModalFooter, ModalContent } from 'components/base/Modal' +import Modal, { ModalBody } from 'components/base/Modal' import Text from 'components/base/Text' import CopyWithFeedback from 'components/base/CopyWithFeedback' @@ -130,122 +128,120 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => { const uniqueSenders = uniq(senders) return ( - <ModalBody onClose={onClose}> - <TrackPage category="Modal" name="OperationDetails" /> - <ModalTitle>{t('operationDetails.title')}</ModalTitle> - <ModalContent relative style={{ height: 500 }} px={0} pb={0}> - <GrowScroll px={5} pt={1} pb={8}> - <Box flow={3}> - <Box alignItems="center" mt={1}> - <ConfirmationCheck - marketColor={marketColor} - isConfirmed={isConfirmed} - style={{ - transform: 'scale(1.5)', - }} - t={t} - type={type} - withTooltip={false} - /> - <Box my={4} alignItems="center"> - <Box> - <FormattedVal - color={amount.isNegative() ? 'smoke' : undefined} - unit={unit} - alwaysShowSign - showCode - val={amount} - fontSize={7} - disableRounding - /> - </Box> - <Box mt={1}> - <CounterValue - color="grey" - fontSize={5} - date={date} - currency={currency} - value={amount} - /> - </Box> - </Box> - </Box> - <Box horizontal flow={2}> - <Box flex={1}> - <OpDetailsTitle>{t('operationDetails.account')}</OpDetailsTitle> - <OpDetailsData>{name}</OpDetailsData> + <ModalBody + title={t('operationDetails.title')} + onClose={onClose} + render={() => ( + <Box flow={3}> + <Box alignItems="center" mt={1}> + <ConfirmationCheck + marketColor={marketColor} + isConfirmed={isConfirmed} + style={{ + transform: 'scale(1.5)', + }} + t={t} + type={type} + withTooltip={false} + /> + <Box my={4} alignItems="center"> + <Box> + <FormattedVal + color={amount.isNegative() ? 'smoke' : undefined} + unit={unit} + alwaysShowSign + showCode + val={amount} + fontSize={7} + disableRounding + /> </Box> - <Box flex={1}> - <OpDetailsTitle>{t('operationDetails.date')}</OpDetailsTitle> - <OpDetailsData>{moment(date).format('LLL')}</OpDetailsData> + <Box mt={1}> + <CounterValue + color="grey" + fontSize={5} + date={date} + currency={currency} + value={amount} + /> </Box> </Box> - <B /> - <Box horizontal flow={2}> - <Box flex={1}> - <OpDetailsTitle>{t('operationDetails.fees')}</OpDetailsTitle> - {fee ? ( - <Fragment> - <OpDetailsData> - <FormattedVal unit={unit} showCode val={fee} color="smoke" /> - </OpDetailsData> - </Fragment> - ) : ( - <OpDetailsData>{t('operationDetails.noFees')}</OpDetailsData> - )} - </Box> - <Box flex={1}> - <OpDetailsTitle>{t('operationDetails.status')}</OpDetailsTitle> - <OpDetailsData color={isConfirmed ? 'positiveGreen' : null} horizontal flow={1}> - <Box> - {isConfirmed - ? t('operationDetails.confirmed') - : t('operationDetails.notConfirmed')} - </Box> - <Box>{`(${confirmations})`}</Box> - </OpDetailsData> - </Box> + </Box> + <Box horizontal flow={2}> + <Box flex={1}> + <OpDetailsTitle>{t('operationDetails.account')}</OpDetailsTitle> + <OpDetailsData>{name}</OpDetailsData> </Box> - <B /> - <Box> - <OpDetailsTitle>{t('operationDetails.identifier')}</OpDetailsTitle> - <OpDetailsData> - <Ellipsis canSelect>{hash}</Ellipsis> - <GradientHover> - <CopyWithFeedback text={hash} /> - </GradientHover> - </OpDetailsData> + <Box flex={1}> + <OpDetailsTitle>{t('operationDetails.date')}</OpDetailsTitle> + <OpDetailsData>{moment(date).format('LLL')}</OpDetailsData> </Box> - <B /> - <Box> - <OpDetailsTitle>{t('operationDetails.from')}</OpDetailsTitle> - <DataList lines={uniqueSenders} t={t} /> + </Box> + <B /> + <Box horizontal flow={2}> + <Box flex={1}> + <OpDetailsTitle>{t('operationDetails.fees')}</OpDetailsTitle> + {fee ? ( + <Fragment> + <OpDetailsData> + <FormattedVal unit={unit} showCode val={fee} color="smoke" /> + </OpDetailsData> + </Fragment> + ) : ( + <OpDetailsData>{t('operationDetails.noFees')}</OpDetailsData> + )} </Box> - <B /> - <Box> - <OpDetailsTitle>{t('operationDetails.to')}</OpDetailsTitle> - <DataList lines={recipients} t={t} /> + <Box flex={1}> + <OpDetailsTitle>{t('operationDetails.status')}</OpDetailsTitle> + <OpDetailsData color={isConfirmed ? 'positiveGreen' : null} horizontal flow={1}> + <Box> + {isConfirmed + ? t('operationDetails.confirmed') + : t('operationDetails.notConfirmed')} + </Box> + <Box>{`(${confirmations})`}</Box> + </OpDetailsData> </Box> - {Object.entries(extra).map(([key, value]) => ( - <Box key={key}> - <OpDetailsTitle> - <Trans i18nKey={`operationDetails.extra.${key}`} defaults={key} /> - </OpDetailsTitle> - <OpDetailsData>{value}</OpDetailsData> - </Box> - ))} </Box> - </GrowScroll> - <GradientBox /> - </ModalContent> - - {url && ( - <ModalFooter horizontal justify="flex-end" flow={2}> + <B /> + <Box> + <OpDetailsTitle>{t('operationDetails.identifier')}</OpDetailsTitle> + <OpDetailsData> + <Ellipsis canSelect>{hash}</Ellipsis> + <GradientHover> + <CopyWithFeedback text={hash} /> + </GradientHover> + </OpDetailsData> + </Box> + <B /> + <Box> + <OpDetailsTitle>{t('operationDetails.from')}</OpDetailsTitle> + <DataList lines={uniqueSenders} t={t} /> + </Box> + <B /> + <Box> + <OpDetailsTitle>{t('operationDetails.to')}</OpDetailsTitle> + <DataList lines={recipients} t={t} /> + </Box> + {Object.entries(extra).map(([key, value]) => ( + <Box key={key}> + <OpDetailsTitle> + <Trans i18nKey={`operationDetails.extra.${key}`} defaults={key} /> + </OpDetailsTitle> + <OpDetailsData>{value}</OpDetailsData> + </Box> + ))} + </Box> + )} + renderFooter={() => + url && ( <Button primary onClick={() => openURL(url)}> {t('operationDetails.viewOperation')} </Button> - </ModalFooter> - )} + ) + } + > + <TrackPage category="Modal" name="OperationDetails" /> </ModalBody> ) }) @@ -255,12 +251,13 @@ type ModalRenderProps = { account: string, operation: string, }, - onClose: Function, + onClose?: Function, } const OperationDetailsWrapper = ({ t }: { t: T }) => ( <Modal name={MODAL_OPERATION_DETAILS} + centered render={(props: ModalRenderProps) => { const { data, onClose } = props return <OperationDetails t={t} {...data} onClose={onClose} /> diff --git a/src/components/modals/Receive/index.js b/src/components/modals/Receive/index.js index c0beab10..5d73ec95 100644 --- a/src/components/modals/Receive/index.js +++ b/src/components/modals/Receive/index.js @@ -193,16 +193,17 @@ class ReceiveModal extends PureComponent<Props, State> { return ( <Modal name={MODAL_RECEIVE} + centered refocusWhenChange={stepId} onHide={this.handleReset} preventBackdropClick={isModalLocked} onBeforeOpen={this.handleBeforeOpenModal} - render={({ onClose }) => ( + render={() => ( <Stepper title={t('receive.title')} initialStepId={stepId} onStepChange={this.handleStepChange} - onClose={onClose} + onClose={addtionnalProps.closeModal} steps={this.STEPS} disabledSteps={disabledSteps} errorSteps={errorSteps} diff --git a/src/components/modals/ReleaseNotes/ReleaseNotesBody.js b/src/components/modals/ReleaseNotes/ReleaseNotesBody.js index 1549727f..67162fab 100644 --- a/src/components/modals/ReleaseNotes/ReleaseNotesBody.js +++ b/src/components/modals/ReleaseNotes/ReleaseNotesBody.js @@ -14,7 +14,7 @@ import GradientBox from 'components/GradientBox' import TranslatedError from 'components/TranslatedError' import TrackPage from 'analytics/TrackPage' import Markdown, { Notes } from 'components/base/Markdown' -import { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' +import ModalBody from 'components/base/Modal/ModalBody' import type { T } from 'types/common' @@ -115,21 +115,26 @@ class ReleaseNotesBody extends PureComponent<Props, State> { const { onClose, t } = this.props return ( - <ModalBody onClose={onClose}> - <TrackPage category="Modal" name="ReleaseNotes" /> - <ModalTitle>{t('releaseNotes.title')}</ModalTitle> - <ModalContent relative style={{ height: 500 }} px={0} pb={0}> - <GrowScroll px={5} pb={8}> - {this.renderContent()} - </GrowScroll> - <GradientBox /> - </ModalContent> - <ModalFooter horizontal justifyContent="flex-end"> - <Button onClick={onClose} primary> - {t('common.continue')} - </Button> - </ModalFooter> - </ModalBody> + <ModalBody + onClose={onClose} + title={t('releaseNotes.title')} + render={() => ( + <Box relative style={{ height: 500 }} px={0} pb={0}> + <TrackPage category="Modal" name="ReleaseNotes" /> + <GrowScroll px={5} pb={8}> + {this.renderContent()} + </GrowScroll> + <GradientBox /> + </Box> + )} + renderFooter={() => ( + <Box horizontal justifyContent="flex-end"> + <Button onClick={onClose} primary> + {t('common.continue')} + </Button> + </Box> + )} + /> ) } } diff --git a/src/components/modals/ReleaseNotes/index.js b/src/components/modals/ReleaseNotes/index.js index cf4a4b35..3cd2192d 100644 --- a/src/components/modals/ReleaseNotes/index.js +++ b/src/components/modals/ReleaseNotes/index.js @@ -9,6 +9,7 @@ import ReleaseNotesBody from './ReleaseNotesBody' const ReleaseNotesModal = () => ( <Modal name={MODAL_RELEASES_NOTES} + centered render={({ data, onClose }) => <ReleaseNotesBody version={data} onClose={onClose} />} /> ) diff --git a/src/components/modals/Send/index.js b/src/components/modals/Send/index.js index 090ca4b0..50c56351 100644 --- a/src/components/modals/Send/index.js +++ b/src/components/modals/Send/index.js @@ -273,6 +273,7 @@ class SendModal extends PureComponent<Props, State<*>> { return ( <Modal name={MODAL_SEND} + centered refocusWhenChange={stepId} onHide={this.handleReset} preventBackdropClick={isModalLocked} diff --git a/src/components/modals/SettingsAccount.js b/src/components/modals/SettingsAccount.js index af1d2f3a..108e7a54 100644 --- a/src/components/modals/SettingsAccount.js +++ b/src/components/modals/SettingsAccount.js @@ -11,7 +11,10 @@ export default class SettingsAccount extends PureComponent<*, *> { return ( <Modal name={MODAL_SETTINGS_ACCOUNT} - render={({ data, onClose }) => <AccountSettingRenderBody data={data} onClose={onClose} />} + centered + render={({ data, onClose }) => ( + <AccountSettingRenderBody {...this.props} data={data} onClose={onClose} /> + )} /> ) } diff --git a/src/components/modals/ShareAnalytics.js b/src/components/modals/ShareAnalytics.js index 467b060d..c5f315fc 100644 --- a/src/components/modals/ShareAnalytics.js +++ b/src/components/modals/ShareAnalytics.js @@ -1,19 +1,28 @@ // @flow -import React, { PureComponent } from 'react' +import React, { Fragment, PureComponent } from 'react' import { translate } from 'react-i18next' import styled from 'styled-components' import { MODAL_SHARE_ANALYTICS } from 'config/constants' -import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' +import Modal from 'components/base/Modal' import Button from 'components/base/Button' import Box from 'components/base/Box' - +import { connect } from 'react-redux' +import { compose } from 'redux' import type { T } from 'types/common' +import { closeModal } from '../../reducers/modals' +import ModalBody from '../base/Modal/ModalBody' + type Props = { t: T, + closeModal: string => void, } +const mapDispatchToProps = { + closeModal, +} + class ShareAnalytics extends PureComponent<Props, *> { render() { const { t } = this.props @@ -56,30 +65,41 @@ class ShareAnalytics extends PureComponent<Props, *> { }, ] return ( - <Modal - name={MODAL_SHARE_ANALYTICS} - render={({ onClose }) => ( - <ModalBody onClose={onClose}> - <ModalTitle data-e2e="modal_title_shareAnalytics"> - {t('onboarding.analytics.shareAnalytics.title')} - </ModalTitle> - <InlineDesc>{t('onboarding.analytics.shareAnalytics.desc')}</InlineDesc> - <ModalContent mx={5}> - <Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul> - </ModalContent> - <ModalFooter horizontal justifyContent="flex-end"> - <Button onClick={onClose} primary data-e2e="modal_buttonClose_shareAnalytics"> + <Modal name={MODAL_SHARE_ANALYTICS} centered> + <ModalBody + title={t('onboarding.analytics.shareAnalytics.title')} + render={() => ( + <Fragment> + <InlineDesc>{t('onboarding.analytics.shareAnalytics.desc')}</InlineDesc> + <Box mx={5}> + <Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul> + </Box> + </Fragment> + )} + renderFooter={() => ( + <Fragment> + <Button + onClick={() => this.props.closeModal(MODAL_SHARE_ANALYTICS)} + primary + data-e2e="modal_buttonClose_shareAnalytics" + > {t('common.close')} </Button> - </ModalFooter> - </ModalBody> - )} - /> + </Fragment> + )} + /> + </Modal> ) } } -export default translate()(ShareAnalytics) +export default compose( + connect( + null, + mapDispatchToProps, + ), + translate(), +)(ShareAnalytics) export const Ul = styled.ul.attrs({ ff: 'Open Sans|Regular', diff --git a/src/components/modals/TechnicalData.js b/src/components/modals/TechnicalData.js index 395cdf46..93f2c3ef 100644 --- a/src/components/modals/TechnicalData.js +++ b/src/components/modals/TechnicalData.js @@ -1,16 +1,25 @@ // @flow -import React, { PureComponent } from 'react' +import React, { Fragment, PureComponent } from 'react' import { translate } from 'react-i18next' import { MODAL_TECHNICAL_DATA } from 'config/constants' -import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' +import Modal, { ModalBody } from 'components/base/Modal' import Button from 'components/base/Button' import type { T } from 'types/common' +import { connect } from 'react-redux' +import { compose } from 'redux' +import Box from 'components/base/Box' import { Ul, InlineDesc } from './ShareAnalytics' +import { closeModal } from '../../reducers/modals' type Props = { t: T, + closeModal: string => void, +} + +const mapDispatchToProps = { + closeModal, } class TechnicalData extends PureComponent<Props, *> { @@ -33,27 +42,38 @@ class TechnicalData extends PureComponent<Props, *> { ] return ( - <Modal - name={MODAL_TECHNICAL_DATA} - render={({ onClose }) => ( - <ModalBody onClose={onClose}> - <ModalTitle data-e2e="modal_title_TechData"> - {t('onboarding.analytics.technicalData.mandatoryContextual.title')} - </ModalTitle> - <InlineDesc>{t('onboarding.analytics.technicalData.desc')}</InlineDesc> - <ModalContent mx={5}> - <Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul> - </ModalContent> - <ModalFooter horizontal justifyContent="flex-end"> - <Button onClick={onClose} primary data-e2e="modal_buttonClose_techData"> + <Modal name={MODAL_TECHNICAL_DATA} centered> + <ModalBody + title={t('onboarding.analytics.technicalData.mandatoryContextual.title')} + render={() => ( + <Fragment> + <InlineDesc>{t('onboarding.analytics.technicalData.desc')}</InlineDesc> + <Box mx={5}> + <Ul>{items.map(item => <li key={item.key}>{item.desc}</li>)}</Ul> + </Box> + </Fragment> + )} + renderFooter={() => ( + <Fragment> + <Button + onClick={() => this.props.closeModal(MODAL_TECHNICAL_DATA)} + primary + data-e2e="modal_buttonClose_techData" + > {t('common.close')} </Button> - </ModalFooter> - </ModalBody> - )} - /> + </Fragment> + )} + /> + </Modal> ) } } -export default translate()(TechnicalData) +export default compose( + connect( + null, + mapDispatchToProps, + ), + translate(), +)(TechnicalData) diff --git a/src/components/modals/UpdateFirmware/Disclaimer.js b/src/components/modals/UpdateFirmware/Disclaimer.js index 76d16fae..f18019e5 100644 --- a/src/components/modals/UpdateFirmware/Disclaimer.js +++ b/src/components/modals/UpdateFirmware/Disclaimer.js @@ -1,12 +1,13 @@ // @flow /* eslint react/jsx-no-literals: 0 */ -import React, { PureComponent, Fragment } from 'react' +import React, { PureComponent } from 'react' import { translate, Trans } from 'react-i18next' import type { OsuFirmware, FinalFirmware } from '@ledgerhq/live-common/lib/types/manager' import type { T } from 'types/common' -import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal' +import Modal from 'components/base/Modal' +import ModalBody from 'components/base/Modal/ModalBody' import Text from 'components/base/Text' import Button from 'components/base/Button' import GrowScroll from 'components/base/GrowScroll' @@ -17,6 +18,7 @@ import TrackPage from 'analytics/TrackPage' import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate' import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate' +import Box from '../../base/Box/Box' type Props = { t: T, @@ -35,49 +37,51 @@ class DisclaimerModal extends PureComponent<Props, State> { render(): React$Node { const { status, firmware, onClose, t, goToNextStep } = this.props return ( - <Modal - isOpened={status === 'disclaimer'} - onClose={onClose} - render={({ onClose }) => ( - <ModalBody onClose={onClose} grow align="center" justify="center" mt={3}> - <TrackPage category="Manager" name="DisclaimerModal" /> - <Fragment> - <ModalTitle>{t('manager.firmware.update')}</ModalTitle> - <ModalContent> - <Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center"> - <Trans i18nKey="manager.firmware.disclaimerTitle"> - You are about to install - <Text ff="Open Sans|SemiBold" color="dark"> - {`firmware version ${ - firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : '' - }`} - </Text> - </Trans> - </Text> - <Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center"> - {t('manager.firmware.disclaimerAppDelete')} - {t('manager.firmware.disclaimerAppReinstall')} - </Text> - </ModalContent> + <Modal isOpened={status === 'disclaimer'} onClose={onClose}> + <TrackPage category="Manager" name="DisclaimerModal" /> + <ModalBody + grow + align="center" + justify="center" + mt={3} + title={t('manager.firmware.update')} + render={() => ( + <Box> + <Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center"> + <Trans i18nKey="manager.firmware.disclaimerTitle"> + You are about to install + <Text ff="Open Sans|SemiBold" color="dark"> + {`firmware version ${ + firmware && firmware.osu ? getCleanVersion(firmware.osu.name) : '' + }`} + </Text> + </Trans> + </Text> + <Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center"> + {t('manager.firmware.disclaimerAppDelete')} + {t('manager.firmware.disclaimerAppReinstall')} + </Text> {firmware && firmware.osu ? ( - <ModalContent relative pb={0} style={{ height: 250, width: '100%' }}> + <Box relative pb={0} style={{ height: 250, width: '100%' }}> <GrowScroll pb={5}> <Notes> <Markdown>{firmware.osu.notes}</Markdown> </Notes> </GrowScroll> <GradientBox /> - </ModalContent> + </Box> ) : null} - <ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}> - <Button primary onClick={goToNextStep}> - {t('common.continue')} - </Button> - </ModalFooter> - </Fragment> - </ModalBody> - )} - /> + </Box> + )} + renderFooter={() => ( + <Box horizontal justifyContent="flex-end"> + <Button primary onClick={goToNextStep}> + {t('common.continue')} + </Button> + </Box> + )} + /> + </Modal> ) } } diff --git a/src/components/modals/UpdateFirmware/index.js b/src/components/modals/UpdateFirmware/index.js index 371909de..9d2ed400 100644 --- a/src/components/modals/UpdateFirmware/index.js +++ b/src/components/modals/UpdateFirmware/index.js @@ -103,6 +103,7 @@ class UpdateModal extends PureComponent<Props, State> { return ( <Modal onClose={onClose} + centered onHide={this.handleReset} isOpened={status === 'install'} refocusWhenChange={stepId} diff --git a/src/index.ejs b/src/index.ejs index cc3c0c35..9426e338 100644 --- a/src/index.ejs +++ b/src/index.ejs @@ -75,6 +75,7 @@ <img class="logo" src="<%= __DEV__ ? '.' : '../static' %>/images/ledgerlive-logo.svg" alt="" /> </div> <div id="app"></div> + <div id="modals"></div> <script> const { remote } = require('electron') const { name } = remote.getCurrentWindow() diff --git a/src/reducers/modals.js b/src/reducers/modals.js index 26c6ca80..d4b6d644 100644 --- a/src/reducers/modals.js +++ b/src/reducers/modals.js @@ -44,7 +44,6 @@ const handlers = { ...state, [name]: { isOpened: false, - data: undefined, }, } }, From b8cfb933167bce99ab21aedb991e36f411e686e0 Mon Sep 17 00:00:00 2001 From: meriadec <meriadec.pillet@gmail.com> Date: Tue, 15 Jan 2019 13:38:52 +0100 Subject: [PATCH 2/8] Fix submit of account settings --- src/components/modals/AccountSettingRenderBody.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/modals/AccountSettingRenderBody.js b/src/components/modals/AccountSettingRenderBody.js index 552470bd..ec5cc8d4 100644 --- a/src/components/modals/AccountSettingRenderBody.js +++ b/src/components/modals/AccountSettingRenderBody.js @@ -123,7 +123,7 @@ class AccountSettingRenderBody extends PureComponent<Props, State> { }) handleSubmit = (account: Account, onClose: () => void) => ( - e: SyntheticEvent<HTMLFormElement>, + e: SyntheticEvent<HTMLFormElement | HTMLInputElement>, ) => { e.preventDefault() const { updateAccount, setDataModal } = this.props @@ -199,12 +199,14 @@ class AccountSettingRenderBody extends PureComponent<Props, State> { blockHeight: account.blockHeight, } + const onSubmit = this.handleSubmit(account, onClose) + return ( <ModalBody onClose={onClose} title={t('account.settings.title')} render={() => ( - <form onSubmit={this.handleSubmit(account, onClose)}> + <Fragment> <TrackPage category="Modal" name="AccountSettings" /> <Container> <Box> @@ -218,6 +220,7 @@ class AccountSettingRenderBody extends PureComponent<Props, State> { value={account.name} maxLength={MAX_ACCOUNT_NAME_SIZE} onChange={this.handleChangeName} + onEnter={onSubmit} onFocus={e => this.handleFocus(e, 'accountName')} error={accountNameError} /> @@ -289,7 +292,7 @@ class AccountSettingRenderBody extends PureComponent<Props, State> { subTitle={t('common.areYouSure')} desc={t('settings.removeAccountModal.desc')} /> - </form> + </Fragment> )} renderFooter={() => ( <Fragment> @@ -301,7 +304,7 @@ class AccountSettingRenderBody extends PureComponent<Props, State> { > {t('common.delete')} </Button> - <Button event="DoneEditingAccount" ml="auto" type="submit" primary> + <Button event="DoneEditingAccount" ml="auto" onClick={onSubmit} primary> {t('common.apply')} </Button> </Fragment> From dea7bfeb36d8a78e4b29e82e3e3b9dd255d24e77 Mon Sep 17 00:00:00 2001 From: meriadec <meriadec.pillet@gmail.com> Date: Tue, 15 Jan 2019 13:39:07 +0100 Subject: [PATCH 3/8] Fix display issues in account settings modal - disabled scroll because the unit select menu was not correctly displayed (caused by the parent `overflow: scroll`) - full size advanced account infos (dunno why it's called "advanced logs", lol) --- src/components/modals/AccountSettingRenderBody.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/modals/AccountSettingRenderBody.js b/src/components/modals/AccountSettingRenderBody.js index ec5cc8d4..3b494e55 100644 --- a/src/components/modals/AccountSettingRenderBody.js +++ b/src/components/modals/AccountSettingRenderBody.js @@ -203,6 +203,7 @@ class AccountSettingRenderBody extends PureComponent<Props, State> { return ( <ModalBody + noScroll onClose={onClose} title={t('account.settings.title')} render={() => ( @@ -277,6 +278,7 @@ class AccountSettingRenderBody extends PureComponent<Props, State> { height: 200, outline: 'none', padding: '20px', + width: '100%', }} value={JSON.stringify(usefulData, null, 2)} /> From b93f48064a588eaa859a4ff3abcd5e3b3179c9c4 Mon Sep 17 00:00:00 2001 From: Juan Cortes Ross <juan@bohem.io> Date: Tue, 15 Jan 2019 17:56:54 +0100 Subject: [PATCH 4/8] Allow components inside modal body to overflow (ie dropdowns) --- src/components/SettingsPage/PasswordModal.js | 3 +-- src/components/base/Modal/ModalContent.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/SettingsPage/PasswordModal.js b/src/components/SettingsPage/PasswordModal.js index 1b8c3909..5acaf9cf 100644 --- a/src/components/SettingsPage/PasswordModal.js +++ b/src/components/SettingsPage/PasswordModal.js @@ -39,8 +39,7 @@ class PasswordModal extends PureComponent<Props, State> { componentWillReceiveProps(nextProps: Props) { if (!nextProps.isOpened) { - // CLean the state? - this.setState(prevState => ({ ...prevState, ...INITIAL_STATE })) + this.setState(INITIAL_STATE) } } diff --git a/src/components/base/Modal/ModalContent.js b/src/components/base/Modal/ModalContent.js index 7b74ddd1..84986fde 100644 --- a/src/components/base/Modal/ModalContent.js +++ b/src/components/base/Modal/ModalContent.js @@ -39,7 +39,7 @@ class ModalContent extends PureComponent<{ const contentStyle = { ...CONTENT_STYLE, - overflow: noScroll ? 'visible' : 'auto', + overflow: noScroll ? 'visible' : 'inherit', } return ( From 14498f1c9e929aff6e85aab38324cca09ed457ae Mon Sep 17 00:00:00 2001 From: Juan Cortes Ross <juan@bohem.io> Date: Thu, 17 Jan 2019 18:48:19 +0100 Subject: [PATCH 5/8] Removes footer if no content visible on install/uninstall; Fixes inner scroll for operationdetails; --- src/components/ManagerPage/AppsList.js | 10 +++++----- src/components/base/Modal/ModalContent.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ManagerPage/AppsList.js b/src/components/ManagerPage/AppsList.js index 390da5c6..b97fe08f 100644 --- a/src/components/ManagerPage/AppsList.js +++ b/src/components/ManagerPage/AppsList.js @@ -225,26 +225,25 @@ class AppsList extends PureComponent<Props, State> { renderFooter = () => { const { t } = this.props - const { status } = this.state - return ['error', 'success'].includes(status) ? ( + return ( <Box horizontal justifyContent="flex-end" style={{ width: '100%' }}> <Button primary onClick={this.handleCloseModal}> {t('common.close')} </Button> </Box> - ) : null + ) } renderModal = () => { const { status } = this.state return ( - <Modal isOpened={status !== 'idle' && status !== 'loading'}> + <Modal isOpened={status !== 'idle' && status !== 'loading'} centered> <ModalBody align="center" justify="center" title={''} render={this.renderBody} - renderFooter={this.renderFooter} + renderFooter={['error', 'success'].includes(status) ? this.renderFooter : undefined} > <FreezeDeviceChangeEvents /> </ModalBody> @@ -295,6 +294,7 @@ class AppsList extends PureComponent<Props, State> { render() { const { t } = this.props + return ( <Box flow={6}> <Box> diff --git a/src/components/base/Modal/ModalContent.js b/src/components/base/Modal/ModalContent.js index 84986fde..7b74ddd1 100644 --- a/src/components/base/Modal/ModalContent.js +++ b/src/components/base/Modal/ModalContent.js @@ -39,7 +39,7 @@ class ModalContent extends PureComponent<{ const contentStyle = { ...CONTENT_STYLE, - overflow: noScroll ? 'visible' : 'inherit', + overflow: noScroll ? 'visible' : 'auto', } return ( From 0570dfacf8dd011d5bce19c5dc2e1379ab629a47 Mon Sep 17 00:00:00 2001 From: meriadec <meriadec.pillet@gmail.com> Date: Mon, 4 Feb 2019 14:58:50 +0100 Subject: [PATCH 6/8] Various polishes on Modal refactoring - use overlayScrollbar from electron blink features, to prevent scrollbar taking space. as a side effect we can now scroll by holding scrollbar with mouse left button. this is a native behavior that we should not decide to remove for users - update bottom white gradient to prevent overlapping scrollbar - make account settings modal scrollable, but increase it's default height so no weird Select triggering scroll when opening - remove GrowScroll from release notes modal so we don't have two nested scrolls (was producing really weird behaviour, scrollbar thumb not indicating scroll % properly) --- src/components/base/Modal/ModalBody.js | 2 +- src/components/modals/AccountSettingRenderBody.js | 15 +++++++++------ .../modals/ReleaseNotes/ReleaseNotesBody.js | 5 ++--- src/main/app.js | 1 + 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/base/Modal/ModalBody.js b/src/components/base/Modal/ModalBody.js index 363f10aa..aa6642f1 100644 --- a/src/components/base/Modal/ModalBody.js +++ b/src/components/base/Modal/ModalBody.js @@ -90,7 +90,7 @@ const GRADIENT_STYLE = { position: 'absolute', bottom: 0, left: 0, - right: 6, + right: 20, } const GRADIENT_WRAPPER_STYLE = { diff --git a/src/components/modals/AccountSettingRenderBody.js b/src/components/modals/AccountSettingRenderBody.js index 3b494e55..71cba156 100644 --- a/src/components/modals/AccountSettingRenderBody.js +++ b/src/components/modals/AccountSettingRenderBody.js @@ -23,6 +23,7 @@ import TrackPage from 'analytics/TrackPage' import Spoiler from 'components/base/Spoiler' import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon' import Box from 'components/base/Box' +import Space from 'components/base/Space' import Button from 'components/base/Button' import Input from 'components/base/Input' import Select from 'components/base/Select' @@ -203,7 +204,6 @@ class AccountSettingRenderBody extends PureComponent<Props, State> { return ( <ModalBody - noScroll onClose={onClose} title={t('account.settings.title')} render={() => ( @@ -266,8 +266,7 @@ class AccountSettingRenderBody extends PureComponent<Props, State> { ) : null} <Spoiler textTransform title={t('account.settings.advancedLogs')}> <SyncAgo date={account.lastSyncDate} /> - <textarea - readOnly + <div style={{ userSelect: 'text', border: '1px dashed #f9f9f9', @@ -275,13 +274,16 @@ class AccountSettingRenderBody extends PureComponent<Props, State> { color: '#000', fontFamily: 'monospace', fontSize: '10px', - height: 200, outline: 'none', padding: '20px', width: '100%', + whiteSpace: 'pre-wrap', + wordWrap: 'break-word', + overflow: 'auto', }} - value={JSON.stringify(usefulData, null, 2)} - /> + > + {JSON.stringify(usefulData, null, 2)} + </div> </Spoiler> <ConfirmModal analyticsName="RemoveAccount" @@ -294,6 +296,7 @@ class AccountSettingRenderBody extends PureComponent<Props, State> { subTitle={t('common.areYouSure')} desc={t('settings.removeAccountModal.desc')} /> + <Space of={20} /> </Fragment> )} renderFooter={() => ( diff --git a/src/components/modals/ReleaseNotes/ReleaseNotesBody.js b/src/components/modals/ReleaseNotes/ReleaseNotesBody.js index 67162fab..61ff613d 100644 --- a/src/components/modals/ReleaseNotes/ReleaseNotesBody.js +++ b/src/components/modals/ReleaseNotes/ReleaseNotesBody.js @@ -7,7 +7,6 @@ import network from 'api/network' import Button from 'components/base/Button' import Box from 'components/base/Box' -import GrowScroll from 'components/base/GrowScroll' import Text from 'components/base/Text' import Spinner from 'components/base/Spinner' import GradientBox from 'components/GradientBox' @@ -121,9 +120,9 @@ class ReleaseNotesBody extends PureComponent<Props, State> { render={() => ( <Box relative style={{ height: 500 }} px={0} pb={0}> <TrackPage category="Modal" name="ReleaseNotes" /> - <GrowScroll px={5} pb={8}> + <Box px={5} pb={8}> {this.renderContent()} - </GrowScroll> + </Box> <GradientBox /> </Box> )} diff --git a/src/main/app.js b/src/main/app.js index 53d3ad9a..08385a54 100644 --- a/src/main/app.js +++ b/src/main/app.js @@ -84,6 +84,7 @@ const defaultWindowOptions = { backgroundColor: '#fff', webPreferences: { + blinkFeatures: 'OverlayScrollbars', devTools, // Enable, among other things, the ResizeObserver experimentalFeatures: true, From 802492adaf1892ca39aaaefebd75639fd4ac9ec8 Mon Sep 17 00:00:00 2001 From: Juan Cortes Ross <juan@bohem.io> Date: Thu, 7 Feb 2019 14:40:03 +0100 Subject: [PATCH 7/8] -Added enter key trigger for password modal -Added close buttons to technical data, share analytics, trade safely modals --- src/components/ManagerPage/AppsList.js | 3 +-- src/components/SettingsPage/PasswordForm.js | 1 + src/components/modals/Disclaimer.js | 8 +++----- src/components/modals/ShareAnalytics.js | 8 +++----- src/components/modals/TechnicalData.js | 9 ++++----- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/components/ManagerPage/AppsList.js b/src/components/ManagerPage/AppsList.js index 8d34ab3a..d2935f46 100644 --- a/src/components/ManagerPage/AppsList.js +++ b/src/components/ManagerPage/AppsList.js @@ -1,7 +1,7 @@ // @flow /* eslint-disable react/jsx-no-literals */ // FIXME -import React, { PureComponent, Fragment } from 'react' +import React, { PureComponent } from 'react' import styled from 'styled-components' import { translate } from 'react-i18next' import { connect } from 'react-redux' @@ -15,7 +15,6 @@ import { developerModeSelector } from 'reducers/settings' import installApp from 'commands/installApp' import uninstallApp from 'commands/uninstallApp' import Box from 'components/base/Box' -import Space from 'components/base/Space' import Modal from 'components/base/Modal' import Tooltip from 'components/base/Tooltip' import Text from 'components/base/Text' diff --git a/src/components/SettingsPage/PasswordForm.js b/src/components/SettingsPage/PasswordForm.js index d0587fec..ff6a4a03 100644 --- a/src/components/SettingsPage/PasswordForm.js +++ b/src/components/SettingsPage/PasswordForm.js @@ -70,6 +70,7 @@ class PasswordForm extends PureComponent<Props> { </Label> <InputPassword style={{ width: 240 }} + onEnter={onSubmit} id="confirmPassword" onChange={onChange('confirmPassword')} value={confirmPassword} diff --git a/src/components/modals/Disclaimer.js b/src/components/modals/Disclaimer.js index 7e73dc78..a92dba1b 100644 --- a/src/components/modals/Disclaimer.js +++ b/src/components/modals/Disclaimer.js @@ -25,12 +25,14 @@ const mapDispatchToProps = { } class DisclaimerModal extends PureComponent<Props> { + onClose = () => this.props.closeModal(MODAL_DISCLAIMER) render() { const { t } = this.props return ( <Modal name={MODAL_DISCLAIMER} centered> <ModalBody + onClose={this.onClose} title={t('disclaimerModal.title')} render={() => ( <Box flow={4} ff="Open Sans|Regular" fontSize={4} color="smoke"> @@ -43,11 +45,7 @@ class DisclaimerModal extends PureComponent<Props> { )} renderFooter={() => ( <Box horizontal justifyContent="flex-end"> - <Button - data-e2e="continue_button" - onClick={() => this.props.closeModal(MODAL_DISCLAIMER)} - primary - > + <Button data-e2e="continue_button" onClick={this.onClose} primary> {t('disclaimerModal.cta')} </Button> </Box> diff --git a/src/components/modals/ShareAnalytics.js b/src/components/modals/ShareAnalytics.js index c5f315fc..425e0711 100644 --- a/src/components/modals/ShareAnalytics.js +++ b/src/components/modals/ShareAnalytics.js @@ -24,6 +24,7 @@ const mapDispatchToProps = { } class ShareAnalytics extends PureComponent<Props, *> { + onClose = () => this.props.closeModal(MODAL_SHARE_ANALYTICS) render() { const { t } = this.props const items = [ @@ -67,6 +68,7 @@ class ShareAnalytics extends PureComponent<Props, *> { return ( <Modal name={MODAL_SHARE_ANALYTICS} centered> <ModalBody + onClose={this.onClose} title={t('onboarding.analytics.shareAnalytics.title')} render={() => ( <Fragment> @@ -78,11 +80,7 @@ class ShareAnalytics extends PureComponent<Props, *> { )} renderFooter={() => ( <Fragment> - <Button - onClick={() => this.props.closeModal(MODAL_SHARE_ANALYTICS)} - primary - data-e2e="modal_buttonClose_shareAnalytics" - > + <Button onClick={this.onClose} primary data-e2e="modal_buttonClose_shareAnalytics"> {t('common.close')} </Button> </Fragment> diff --git a/src/components/modals/TechnicalData.js b/src/components/modals/TechnicalData.js index 93f2c3ef..b975f6da 100644 --- a/src/components/modals/TechnicalData.js +++ b/src/components/modals/TechnicalData.js @@ -23,6 +23,8 @@ const mapDispatchToProps = { } class TechnicalData extends PureComponent<Props, *> { + onClose = () => this.props.closeModal(MODAL_TECHNICAL_DATA) + render() { const { t } = this.props @@ -44,6 +46,7 @@ class TechnicalData extends PureComponent<Props, *> { return ( <Modal name={MODAL_TECHNICAL_DATA} centered> <ModalBody + onClose={this.onClose} title={t('onboarding.analytics.technicalData.mandatoryContextual.title')} render={() => ( <Fragment> @@ -55,11 +58,7 @@ class TechnicalData extends PureComponent<Props, *> { )} renderFooter={() => ( <Fragment> - <Button - onClick={() => this.props.closeModal(MODAL_TECHNICAL_DATA)} - primary - data-e2e="modal_buttonClose_techData" - > + <Button onClick={this.onClose} primary data-e2e="modal_buttonClose_techData"> {t('common.close')} </Button> </Fragment> From bcae62f5f7d0e0c2127341570ed48d9ae92b08a9 Mon Sep 17 00:00:00 2001 From: Juan Cortes Ross <juan@bohem.io> Date: Thu, 7 Feb 2019 15:36:13 +0100 Subject: [PATCH 8/8] Reduced horizontal margin on shareanalytics/technicaldata modals --- src/components/modals/ShareAnalytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modals/ShareAnalytics.js b/src/components/modals/ShareAnalytics.js index 425e0711..76808988 100644 --- a/src/components/modals/ShareAnalytics.js +++ b/src/components/modals/ShareAnalytics.js @@ -111,5 +111,5 @@ export const InlineDesc = styled(Box).attrs({ ff: 'Open Sans|SemiBold', fontSize: 4, color: 'dark', - mx: '45px', + mx: '15px', })``