diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 2d58622db8..9317e0b746 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -498,9 +498,6 @@ export default class ContentMessages { this.inprogress.push(upload); dis.dispatch({action: 'upload_started'}); - // Focus the composer view - dis.dispatch({action: 'focus_composer'}); - let error; function onProgress(ev) { diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index cd752fc2ce..e3a99a42ca 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -106,7 +106,7 @@ const LoggedInView = React.createClass({ CallMediaHandler.loadDevices(); - document.addEventListener('keydown', this._onKeyDown); + document.addEventListener('keydown', this._onNativeKeyDown, false); this._sessionStore = sessionStore; this._sessionStoreToken = this._sessionStore.addListener( @@ -136,7 +136,7 @@ const LoggedInView = React.createClass({ }, componentWillUnmount: function() { - document.removeEventListener('keydown', this._onKeyDown); + document.removeEventListener('keydown', this._onNativeKeyDown, false); this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("sync", this.onSync); this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents); @@ -272,6 +272,42 @@ const LoggedInView = React.createClass({ }); }, + /* + SOME HACKERY BELOW: + React optimizes event handlers, by always attaching only 1 handler to the document for a given type. + It then internally determines the order in which React event handlers should be called, + emulating the capture and bubbling phases the DOM also has. + + But, as the native handler for React is always attached on the document, + it will always run last for bubbling (first for capturing) handlers, + and thus React basically has its own event phases, and will always run + after (before for capturing) any native other event handlers (as they tend to be attached last). + + So ideally one wouldn't mix React and native event handlers to have bubbling working as expected, + but we do need a native event handler here on the document, + to get keydown events when there is no focused element (target=body). + + We also do need bubbling here to give child components a chance to call `stopPropagation()`, + for keydown events it can handle itself, and shouldn't be redirected to the composer. + + So we listen with React on this component to get any events on focused elements, and get bubbling working as expected. + We also listen with a native listener on the document to get keydown events when no element is focused. + Bubbling is irrelevant here as the target is the body element. + */ + _onReactKeyDown: function(ev) { + // events caught while bubbling up on the root element + // of this component, so something must be focused. + this._onKeyDown(ev); + }, + + _onNativeKeyDown: function(ev) { + // only pass this if there is no focused element. + // if there is, _onKeyDown will be called by the + // react keydown handler that respects the react bubbling order. + if (ev.target === document.body) { + this._onKeyDown(ev); + } + }, _onKeyDown: function(ev) { /* @@ -333,6 +369,21 @@ const LoggedInView = React.createClass({ if (handled) { ev.stopPropagation(); ev.preventDefault(); + } else { + const targetTag = ev.target.tagName; + const focusedOnInputControl = targetTag === "INPUT" || + targetTag === "TEXTAREA" || + targetTag === "SELECT" || + !!ev.target.getAttribute("contenteditable"); + const isClickShortcut = ev.target !== document.body && + (ev.key === "Space" || ev.key === "Enter"); + + if (!focusedOnInputControl && !isClickShortcut) { + dis.dispatch({action: 'focus_composer'}, true); + ev.stopPropagation(); + // we should *not* preventDefault() here as + // that would prevent typing in the now-focussed composer + } } }, @@ -544,7 +595,7 @@ const LoggedInView = React.createClass({ } return ( -