diff --git a/packages/xod-client-browser/src/containers/App.jsx b/packages/xod-client-browser/src/containers/App.jsx index f40535f48..a1dab9969 100644 --- a/packages/xod-client-browser/src/containers/App.jsx +++ b/packages/xod-client-browser/src/containers/App.jsx @@ -48,11 +48,13 @@ class App extends client.App { this.hideInstallAppPopup = this.hideInstallAppPopup.bind(this); - this.hotkeyHandlers = { - [client.COMMAND.NEW_PROJECT]: this.onCreateProject, - [client.COMMAND.UNDO]: this.props.actions.undoCurrentPatch, - [client.COMMAND.REDO]: this.props.actions.redoCurrentPatch, - }; + this.hotkeyHandlers = R.merge( + { + [client.COMMAND.NEW_PROJECT]: this.onCreateProject, + [client.COMMAND.ADD_PATCH]: this.props.actions.createPatch, + }, + this.defaultHotkeyHandlers + ); this.urlActions = { [client.URL_ACTION_TYPES.OPEN_TUTORIAL]: this.onOpenTutorial, diff --git a/packages/xod-client-electron/src/view/containers/App.jsx b/packages/xod-client-electron/src/view/containers/App.jsx index 9ac246b77..5b5b46117 100644 --- a/packages/xod-client-electron/src/view/containers/App.jsx +++ b/packages/xod-client-electron/src/view/containers/App.jsx @@ -191,10 +191,7 @@ class App extends client.App { this.showError(error); }); - this.hotkeyHandlers = { - [client.COMMAND.UNDO]: this.props.actions.undoCurrentPatch, - [client.COMMAND.REDO]: this.props.actions.redoCurrentPatch, - }; + this.hotkeyHandlers = this.defaultHotkeyHandlers; this.urlActions = { // actionPathName: params => this.props.actions.someAction(params.foo, params.bar), diff --git a/packages/xod-client/src/core/containers/App.jsx b/packages/xod-client/src/core/containers/App.jsx index ea312d662..ce879ba2c 100644 --- a/packages/xod-client/src/core/containers/App.jsx +++ b/packages/xod-client/src/core/containers/App.jsx @@ -37,7 +37,7 @@ import { LIVENESS, } from 'xod-arduino'; -import { isInputTarget } from '../../utils/browser'; +import { isInputTarget, elementHasFocusFunction } from '../../utils/browser'; import { lowercaseKebabMask, patchBasenameMask, @@ -58,6 +58,7 @@ import { import { USERNAME_NEEDED_FOR_LITERAL } from '../../user/messages'; import { PROJECT_NAME_NEEDED_FOR_LITERAL } from '../../project/messages'; import { DO_NOT_USE_TETHERING_INTERNET_IN_BROWSER } from '../../debugger/messages'; +import { COMMAND } from '../../utils/constants'; import formatErrorMessage from '../formatErrorMessage'; @@ -72,6 +73,19 @@ export default class App extends React.Component { ); this.getGlobals = this.getGlobals.bind(this); + this.defaultHotkeyHandlers = { + [COMMAND.UNDO]: this.props.actions.undoCurrentPatch, + [COMMAND.REDO]: this.props.actions.redoCurrentPatch, + [COMMAND.HIDE_HELPBOX]: this.props.actions.hideHelpbox, + [COMMAND.TOGGLE_HELP]: this.props.actions.toggleHelp, + [COMMAND.INSERT_NODE]: event => { + if (isInputTarget(event)) return; + this.props.actions.showSuggester(null); + }, + [COMMAND.PAN_TO_ORIGIN]: this.props.actions.panToOrigin, + [COMMAND.PAN_TO_CENTER]: this.props.actions.panToCenter, + }; + /** * We have to handle some hotkeys, because: * - Browser IDE should prevent default event handling @@ -96,6 +110,33 @@ export default class App extends React.Component { document.addEventListener('copy', this.props.actions.copyEntities); document.addEventListener('paste', this.props.actions.pasteEntities); this.props.actions.fetchGrant(/* startup */ true); + + // TODO: Replace with ref and `React.createRef` + // after updating React >16.3 + const appEl = document.getElementById('App'); + + if (elementHasFocusFunction(appEl)) appEl.focus(); + + /** + * Listen capturing focusout on any element inside body element + * and if element unfocused to the body element — focus on the `App` + * component to make sure main hotkeys will work anytime. + */ + document.body.addEventListener( + 'focusout', + // We need a timeout here to await while focus changed on the some element + () => + setTimeout(() => { + // If focused changed to the body element — focus to the App + if ( + document.activeElement === document.body && + elementHasFocusFunction(appEl) + ) { + appEl.focus(); + } + }, 0), + true + ); } onShowCodeArduino(liveness = LIVENESS.NONE) { @@ -309,7 +350,7 @@ export default class App extends React.Component { } render() { - return
; + return
; } } @@ -353,6 +394,7 @@ App.propTypes = { toggleDebugger: PropTypes.func.isRequired, logDebugger: PropTypes.func.isRequired, clearDebugger: PropTypes.func.isRequired, + showSuggester: PropTypes.func.isRequired, showLibSuggester: PropTypes.func.isRequired, toggleAccountPane: PropTypes.func.isRequired, fetchGrant: PropTypes.func.isRequired, @@ -362,6 +404,9 @@ App.propTypes = { abortSimulation: PropTypes.func.isRequired, generateApiKey: PropTypes.func.isRequired, renewApiToken: PropTypes.func.isRequired, + hideHelpbox: PropTypes.func.isRequired, + panToOrigin: PropTypes.func.isRequired, + panToCenter: PropTypes.func.isRequired, /* eslint-enable react/no-unused-prop-types */ }), }; @@ -394,7 +439,6 @@ App.actions = { startSerialSession: actions.startSerialSession, stopDebuggerSession: actions.stopDebuggerSession, toggleDebugger: actions.toggleDebugger, - showSuggester: actions.showSuggester, logDebugger: actions.addMessagesToDebuggerLog, clearDebugger: actions.clearDebuggerLog, cutEntities: actions.cutEntities, @@ -402,6 +446,7 @@ App.actions = { pasteEntities: actions.pasteEntities, setCurrentPatchOffsetToOrigin: actions.setCurrentPatchOffsetToOrigin, setCurrentPatchOffsetToCenter: actions.setCurrentPatchOffsetToCenter, + showSuggester: actions.showSuggester, showLibSuggester: actions.showLibSuggester, toggleAccountPane: actions.toggleAccountPane, fetchGrant: actions.fetchGrant, @@ -412,4 +457,7 @@ App.actions = { abortSimulation: actions.abortSimulation, generateApiKey: actions.generateApiKey, renewApiToken: actions.renewApiToken, + hideHelpbox: actions.hideHelpbox, + panToOrigin: actions.setCurrentPatchOffsetToOrigin, + panToCenter: actions.setCurrentPatchOffsetToCenter, }; diff --git a/packages/xod-client/src/editor/containers/Editor.jsx b/packages/xod-client/src/editor/containers/Editor.jsx index 0dafbd442..a534a1df0 100644 --- a/packages/xod-client/src/editor/containers/Editor.jsx +++ b/packages/xod-client/src/editor/containers/Editor.jsx @@ -8,7 +8,7 @@ import { DragDropContext } from 'react-dnd'; import HTML5Backend from 'react-dnd-html5-backend'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { HotKeys, FocusTrap } from 'react-hotkeys'; +import { FocusTrap } from 'react-hotkeys'; import * as XP from 'xod-project'; import debounce from 'throttle-debounce/debounce'; @@ -19,7 +19,6 @@ import * as DebuggerSelectors from '../../debugger/selectors'; import * as EditorSelectors from '../selectors'; import { isInputTarget } from '../../utils/browser'; -import { COMMAND } from '../../utils/constants'; import sanctuaryPropType from '../../utils/sanctuaryPropType'; import { FOCUS_AREAS, TAB_TYPES, SIDEBAR_IDS } from '../constants'; @@ -45,7 +44,6 @@ class Editor extends React.Component { constructor(props) { super(props); - this.getHotkeyHandlers = this.getHotkeyHandlers.bind(this); this.toggleHelp = this.toggleHelp.bind(this); this.onAddNode = this.onAddNode.bind(this); this.onInstallLibrary = this.onInstallLibrary.bind(this); @@ -84,19 +82,6 @@ class Editor extends React.Component { this.props.actions.setFocusedArea(FOCUS_AREAS.LIB_SUGGESTER); } - getHotkeyHandlers() { - return { - [COMMAND.HIDE_HELPBOX]: () => this.props.actions.hideHelpbox(), - [COMMAND.TOGGLE_HELP]: this.toggleHelp, - [COMMAND.INSERT_NODE]: event => { - if (isInputTarget(event)) return; - this.showSuggester(null); - }, - [COMMAND.PAN_TO_ORIGIN]: this.props.actions.panToOrigin, - [COMMAND.PAN_TO_CENTER]: this.props.actions.panToCenter, - }; - } - toggleHelp(e) { if (isInputTarget(e)) return; @@ -261,8 +246,7 @@ class Editor extends React.Component { )(this.props.panelsSettings); return ( - - +
); } } @@ -342,8 +326,6 @@ Editor.propTypes = { togglePanelAutohide: PropTypes.func.isRequired, hideHelpbox: PropTypes.func.isRequired, showHelpbox: PropTypes.func.isRequired, - panToOrigin: PropTypes.func.isRequired, - panToCenter: PropTypes.func.isRequired, runTabtest: PropTypes.func.isRequired, }), }; @@ -387,10 +369,8 @@ const mapDispatchToProps = dispatch => ({ minimizePanel: Actions.minimizePanel, movePanel: Actions.movePanel, togglePanelAutohide: Actions.togglePanelAutohide, - hideHelpbox: Actions.hideHelpbox, showHelpbox: Actions.showHelpbox, - panToOrigin: Actions.setCurrentPatchOffsetToOrigin, - panToCenter: Actions.setCurrentPatchOffsetToCenter, + hideHelpbox: Actions.hideHelpbox, runTabtest: Actions.runTabtest, }, dispatch diff --git a/packages/xod-client/src/utils/browser.js b/packages/xod-client/src/utils/browser.js index 50ef52e5d..b105653e0 100644 --- a/packages/xod-client/src/utils/browser.js +++ b/packages/xod-client/src/utils/browser.js @@ -52,3 +52,6 @@ export const isMacOS = () => window.navigator.appVersion.indexOf('Mac') !== -1; export const restoreFocusOnApp = () => { document.getElementById('App').focus(); }; + +export const elementHasFocusFunction = el => + el && typeof el.focus === 'function';