Forráskód Böngészése

Merge pull request #132 from smiclea/confirm-on-enter

Press Enter to confirm in 'confirmation' modals
Dorin Paslaru 8 éve
szülő
commit
9da5fd1799

+ 1 - 1
src/components/atoms/Button/Button.jsx

@@ -108,7 +108,7 @@ const StyledButton = styled.button`
 
 
 const Button = ({ ...props }) => {
 const Button = ({ ...props }) => {
   return (
   return (
-    <StyledButton {...props} />
+    <StyledButton {...props} onFocus={e => { e.target.blur() }} />
   )
   )
 }
 }
 
 

+ 5 - 1
src/components/molecules/Modal/Modal.jsx

@@ -19,6 +19,7 @@ import Modal from 'react-modal'
 
 
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
+import KeyboardManager from '../../../utils/KeyboardManager'
 
 
 const Title = styled.div`
 const Title = styled.div`
   height: 48px;
   height: 48px;
@@ -56,7 +57,10 @@ class NewModal extends React.Component {
   }
   }
 
 
   componentWillReceiveProps(newProps) {
   componentWillReceiveProps(newProps) {
-    if (this.props.isOpen === true && newProps.isOpen === false) {
+    if (!this.props.isOpen && newProps.isOpen) {
+      KeyboardManager.onKeyDown('modal', null, 1)
+    } else if (this.props.isOpen && !newProps.isOpen) {
+      KeyboardManager.removeKeyDown('modal')
       this.handleModalClose()
       this.handleModalClose()
     }
     }
   }
   }

+ 10 - 0
src/components/organisms/AlertModal/AlertModal.jsx

@@ -19,6 +19,7 @@ import PropTypes from 'prop-types'
 import { Modal, Button, StatusImage } from 'components'
 import { Modal, Button, StatusImage } from 'components'
 
 
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
+import KeyboardManager from '../../../utils/KeyboardManager'
 
 
 import questionImage from './images/question.svg'
 import questionImage from './images/question.svg'
 import errorImage from './images/error.svg'
 import errorImage from './images/error.svg'
@@ -56,6 +57,7 @@ class AlertModal extends React.Component {
     message: PropTypes.string,
     message: PropTypes.string,
     extraMessage: PropTypes.string,
     extraMessage: PropTypes.string,
     type: PropTypes.string,
     type: PropTypes.string,
+    isOpen: PropTypes.bool,
     onRequestClose: PropTypes.func,
     onRequestClose: PropTypes.func,
     onConfirmation: PropTypes.func,
     onConfirmation: PropTypes.func,
   }
   }
@@ -64,6 +66,14 @@ class AlertModal extends React.Component {
     type: 'confirmation',
     type: 'confirmation',
   }
   }
 
 
+  componentWillReceiveProps(newProps) {
+    if (newProps.isOpen && !this.props.isOpen) {
+      KeyboardManager.onEnter('alert', () => { this.props.onConfirmation() }, 2)
+    } else if (!newProps.isOpen && this.props.isOpen) {
+      KeyboardManager.removeKeyDown('alert')
+    }
+  }
+
   renderDismissButton() {
   renderDismissButton() {
     if (this.props.type !== 'error') {
     if (this.props.type !== 'error') {
       return null
       return null

+ 5 - 0
src/components/organisms/Endpoint/Endpoint.jsx

@@ -35,6 +35,7 @@ import ObjectUtils from '../../../utils/ObjectUtils'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import DomUtils from '../../../utils/DomUtils'
 import DomUtils from '../../../utils/DomUtils'
 import { ContentPlugin } from '../../../plugins/endpoint'
 import { ContentPlugin } from '../../../plugins/endpoint'
+import KeyboardManager from '../../../utils/KeyboardManager'
 
 
 const Wrapper = styled.div`
 const Wrapper = styled.div`
   padding: 48px 32px 32px 32px;
   padding: 48px 32px 32px 32px;
@@ -143,6 +144,7 @@ class Endpoint extends React.Component {
 
 
   componentDidMount() {
   componentDidMount() {
     ProviderActions.getConnectionInfoSchema(this.getEndpointType())
     ProviderActions.getConnectionInfoSchema(this.getEndpointType())
+    KeyboardManager.onEnter('endpoint', () => { if (this.isValidateButtonEnabled) this.handleValidateClick() }, 2)
   }
   }
 
 
   componentWillReceiveProps(props) {
   componentWillReceiveProps(props) {
@@ -176,6 +178,7 @@ class Endpoint extends React.Component {
     EndpointActions.clearValidation()
     EndpointActions.clearValidation()
     ProviderActions.clearConnectionInfoSchema()
     ProviderActions.clearConnectionInfoSchema()
     clearTimeout(this.closeTimeout)
     clearTimeout(this.closeTimeout)
+    KeyboardManager.removeKeyDown('endpoint')
   }
   }
 
 
   getEndpointType() {
   getEndpointType() {
@@ -311,6 +314,7 @@ class Endpoint extends React.Component {
   }
   }
 
 
   renderButtons() {
   renderButtons() {
+    this.isValidateButtonEnabled = true
     let actionButton = <Button large onClick={() => this.handleValidateClick()}>Validate and save</Button>
     let actionButton = <Button large onClick={() => this.handleValidateClick()}>Validate and save</Button>
 
 
     let message = 'Validating Endpoint ...'
     let message = 'Validating Endpoint ...'
@@ -319,6 +323,7 @@ class Endpoint extends React.Component {
         message = 'Saving ...'
         message = 'Saving ...'
       }
       }
 
 
+      this.isValidateButtonEnabled = false
       actionButton = <LoadingButton large>{message}</LoadingButton>
       actionButton = <LoadingButton large>{message}</LoadingButton>
     }
     }
 
 

+ 10 - 1
src/components/organisms/ReplicaExecutionOptions/ReplicaExecutionOptions.jsx

@@ -19,10 +19,11 @@ import PropTypes from 'prop-types'
 import { WizardOptionsField, Button } from 'components'
 import { WizardOptionsField, Button } from 'components'
 
 
 import LabelDictionary from '../../../utils/LabelDictionary'
 import LabelDictionary from '../../../utils/LabelDictionary'
+import KeyboardManager from '../../../utils/KeyboardManager'
+import { executionOptions } from '../../../config'
 
 
 import executionImage from './images/execution.svg'
 import executionImage from './images/execution.svg'
 
 
-import { executionOptions } from '../../../config'
 
 
 const Wrapper = styled.div`
 const Wrapper = styled.div`
   display: flex;
   display: flex;
@@ -71,6 +72,14 @@ class ReplicaExecutionOptions extends React.Component {
     }
     }
   }
   }
 
 
+  componentDidMount() {
+    KeyboardManager.onEnter('execution-options', () => { this.props.onExecuteClick(this.state.fields) }, 2)
+  }
+
+  componentWillUnmount() {
+    KeyboardManager.removeKeyDown('execution-options')
+  }
+
   getFieldValue(field) {
   getFieldValue(field) {
     if (!this.props.options || this.props.options[field.name] === null || this.props.options[field.name] === undefined) {
     if (!this.props.options || this.props.options[field.name] === null || this.props.options[field.name] === undefined) {
       return field.value
       return field.value

+ 9 - 0
src/components/organisms/ReplicaMigrationOptions/ReplicaMigrationOptions.jsx

@@ -19,6 +19,7 @@ import PropTypes from 'prop-types'
 import { Button, WizardOptionsField } from 'components'
 import { Button, WizardOptionsField } from 'components'
 
 
 import LabelDictionary from '../../../utils/LabelDictionary'
 import LabelDictionary from '../../../utils/LabelDictionary'
+import KeyboardManager from '../../../utils/KeyboardManager'
 import replicaMigrationImage from './images/replica-migration.svg'
 import replicaMigrationImage from './images/replica-migration.svg'
 
 
 const Wrapper = styled.div`
 const Wrapper = styled.div`
@@ -79,6 +80,14 @@ class ReplicaMigrationOptions extends React.Component {
     }
     }
   }
   }
 
 
+  componentDidMount() {
+    KeyboardManager.onEnter('migration-options', () => { this.props.onMigrateClick(this.state.fields) }, 2)
+  }
+
+  componentWillUnmount() {
+    KeyboardManager.removeKeyDown('migration-options')
+  }
+
   handleValueChange(field, value) {
   handleValueChange(field, value) {
     this.state.fields.find(f => f.name === field.name).value = value
     this.state.fields.find(f => f.name === field.name).value = value
     this.setState({ fields: this.state.fields })
     this.setState({ fields: this.state.fields })

+ 9 - 0
src/components/organisms/WizardPageContent/WizardPageContent.jsx

@@ -108,6 +108,7 @@ class WizardPageContent extends React.Component {
     onAddScheduleClick: PropTypes.func,
     onAddScheduleClick: PropTypes.func,
     onScheduleChange: PropTypes.func,
     onScheduleChange: PropTypes.func,
     onScheduleRemove: PropTypes.func,
     onScheduleRemove: PropTypes.func,
+    onContentRef: PropTypes.func,
   }
   }
 
 
   constructor() {
   constructor() {
@@ -119,6 +120,14 @@ class WizardPageContent extends React.Component {
     }
     }
   }
   }
 
 
+  componentDidMount() {
+    this.props.onContentRef(this)
+  }
+
+  componentWillUnmount() {
+    this.props.onContentRef(null)
+  }
+
   getProvidersType(type) {
   getProvidersType(type) {
     if (this.props.type === 'replica') {
     if (this.props.type === 'replica') {
       if (type === 'source') {
       if (type === 'source') {

+ 15 - 0
src/components/pages/WizardPage/WizardPage.jsx

@@ -42,6 +42,7 @@ import ReplicaActions from '../../../actions/ReplicaActions'
 import ScheduleActions from '../../../actions/ScheduleActions'
 import ScheduleActions from '../../../actions/ScheduleActions'
 import ScheduleStore from '../../../stores/ScheduleStore'
 import ScheduleStore from '../../../stores/ScheduleStore'
 import Wait from '../../../utils/Wait'
 import Wait from '../../../utils/Wait'
+import KeyboardManager from '../../../utils/KeyboardManager'
 import { wizardConfig, executionOptions } from '../../../config'
 import { wizardConfig, executionOptions } from '../../../config'
 
 
 const Wrapper = styled.div``
 const Wrapper = styled.div``
@@ -92,10 +93,23 @@ class WizardPage extends React.Component {
 
 
   componentDidMount() {
   componentDidMount() {
     document.title = 'Coriolis Wizard'
     document.title = 'Coriolis Wizard'
+    KeyboardManager.onEnter('wizard', () => { this.handleEnterKey() })
+    KeyboardManager.onEsc('wizard', () => { this.handleEscKey() })
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
     WizardActions.clearData()
     WizardActions.clearData()
+    KeyboardManager.removeKeyDown('wizard')
+  }
+
+  handleEnterKey() {
+    if (this.contentRef && !this.contentRef.isNextButtonDisabled()) {
+      this.handleNextClick()
+    }
+  }
+
+  handleEscKey() {
+    this.handleBackClick()
   }
   }
 
 
   loadDataForPage(page) {
   loadDataForPage(page) {
@@ -387,6 +401,7 @@ class WizardPage extends React.Component {
             onAddScheduleClick={schedule => { this.handleAddScheduleClick(schedule) }}
             onAddScheduleClick={schedule => { this.handleAddScheduleClick(schedule) }}
             onScheduleChange={(scheduleId, data) => { this.handleScheduleChange(scheduleId, data) }}
             onScheduleChange={(scheduleId, data) => { this.handleScheduleChange(scheduleId, data) }}
             onScheduleRemove={scheduleId => { this.handleScheduleRemove(scheduleId) }}
             onScheduleRemove={scheduleId => { this.handleScheduleRemove(scheduleId) }}
+            onContentRef={ref => { this.contentRef = ref }}
           />}
           />}
         />
         />
         <Modal
         <Modal

+ 3 - 1
src/plugins/endpoint/azure/ContentPlugin.jsx

@@ -19,6 +19,7 @@ import PropTypes from 'prop-types'
 import { TextArea } from 'components'
 import { TextArea } from 'components'
 import Palette from '../../../components/styleUtils/Palette'
 import Palette from '../../../components/styleUtils/Palette'
 import StyleProps from '../../../components/styleUtils/StyleProps'
 import StyleProps from '../../../components/styleUtils/StyleProps'
+import KeyboardManager from '../../../utils/KeyboardManager'
 import { Wrapper, Fields, FieldStyled, Row } from '../default/ContentPlugin'
 import { Wrapper, Fields, FieldStyled, Row } from '../default/ContentPlugin'
 
 
 const RadioGroup = styled.div`
 const RadioGroup = styled.div`
@@ -227,7 +228,8 @@ class ContentPlugin extends React.Component {
         height="96px"
         height="96px"
         placeholder="Use the Azure CLI to get the details of a registered cloud and paste it here"
         placeholder="Use the Azure CLI to get the details of a registered cloud and paste it here"
         value={this.state.jsonConfig}
         value={this.state.jsonConfig}
-        onBlur={() => { this.handleJsonConfigBlur() }}
+        onFocus={() => { KeyboardManager.onKeyDown('json-config', null, 3) }} // disable key down propagation
+        onBlur={() => { KeyboardManager.removeKeyDown('json-config'); this.handleJsonConfigBlur() }}
         onChange={e => { this.setState({ jsonConfig: e.target.value }) }}
         onChange={e => { this.setState({ jsonConfig: e.target.value }) }}
         disabled={this.props.disabled}
         disabled={this.props.disabled}
       />
       />

+ 59 - 0
src/utils/KeyboardManager.js

@@ -0,0 +1,59 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+let eventAdded = false
+let listeners = []
+const keyDownHandler = evt => {
+  let maxPriority = 0
+  listeners.forEach(l => { maxPriority = Math.max(l.priority, maxPriority) })
+  let prioritizedListeners = listeners.filter(l => l.priority === maxPriority)
+  prioritizedListeners.forEach(listener => {
+    if (listener.callback) listener.callback(evt)
+  })
+}
+export default class KeyboardManager {
+  static eventAdded = false
+  static onKeyDown(id, callback, priority) {
+    if (!eventAdded) {
+      eventAdded = true
+      document.addEventListener('keydown', evt => { keyDownHandler(evt) })
+    }
+
+    let listener = listeners.find(l => l.id === id)
+    if (listener) {
+      return
+    }
+    listeners.push({ id, callback, priority: priority || 0 })
+  }
+
+  static onEnter(id, callback, priority) {
+    this.onKeyDown(`${id}-enter`, evt => {
+      if (evt.keyCode === 13) {
+        callback(evt)
+      }
+    }, priority)
+  }
+
+  static onEsc(id, callback, priority) {
+    this.onKeyDown(`${id}-esc`, evt => {
+      if (evt.keyCode === 27) {
+        callback(evt)
+      }
+    }, priority)
+  }
+
+  static removeKeyDown(id) {
+    listeners = listeners.filter(l => l.id !== id && l.id !== `${id}-enter` && l.id !== `${id}-esc`)
+  }
+}