Merge pull request #3224 from matrix-org/bwindels/focus-composer-on-type
Focus composer when typing anywhere in the app
This commit is contained in:
commit
4fa7302f69
8 changed files with 57 additions and 29 deletions
|
@ -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) {
|
||||
|
|
|
@ -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 (
|
||||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
|
||||
<div onKeyDown={this._onReactKeyDown} className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
|
||||
{ topBar }
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
||||
|
|
|
@ -268,8 +268,6 @@ export default React.createClass({
|
|||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
this.focusComposer = false;
|
||||
|
||||
// this can technically be done anywhere but doing this here keeps all
|
||||
// the routing url path logic together.
|
||||
if (this.onAliasClick) {
|
||||
|
@ -362,10 +360,6 @@ export default React.createClass({
|
|||
const durationMs = this.stopPageChangeTimer();
|
||||
Analytics.trackPageChange(durationMs);
|
||||
}
|
||||
if (this.focusComposer) {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
this.focusComposer = false;
|
||||
}
|
||||
},
|
||||
|
||||
startPageChangeTimer() {
|
||||
|
@ -793,8 +787,6 @@ export default React.createClass({
|
|||
// that has been passed out-of-band (eg.
|
||||
// room name and avatar from an invite email)
|
||||
_viewRoom: function(roomInfo) {
|
||||
this.focusComposer = true;
|
||||
|
||||
const newState = {
|
||||
view: VIEWS.LOGGED_IN,
|
||||
currentRoomId: roomInfo.room_id || null,
|
||||
|
@ -1368,7 +1360,6 @@ export default React.createClass({
|
|||
self.firstSyncComplete = true;
|
||||
self.firstSyncPromise.resolve();
|
||||
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
self.setState({
|
||||
ready: true,
|
||||
showNotifierToolbar: Notifier.shouldShowToolbar(),
|
||||
|
|
|
@ -135,12 +135,10 @@ module.exports = React.createClass({
|
|||
|
||||
_onResendAllClick: function() {
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onCancelAllClick: function() {
|
||||
Resend.cancelUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onShowDevicesClick: function() {
|
||||
|
|
|
@ -48,7 +48,7 @@ class MenuOption extends React.Component {
|
|||
});
|
||||
|
||||
return <div className={optClasses}
|
||||
onClick={this._onClick} onKeyPress={this._onKeyPress}
|
||||
onClick={this._onClick}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
>
|
||||
{ this.props.children }
|
||||
|
|
|
@ -222,7 +222,6 @@ export default class MessageEditor extends React.Component {
|
|||
dis.dispatch({action: 'edit_event', event: nextEvent});
|
||||
} else {
|
||||
dis.dispatch({action: 'edit_event', event: null});
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
@ -230,7 +229,6 @@ export default class MessageEditor extends React.Component {
|
|||
|
||||
_cancelEdit = () => {
|
||||
dis.dispatch({action: "edit_event", event: null});
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
|
||||
_hasModifications(newContent) {
|
||||
|
@ -257,7 +255,6 @@ export default class MessageEditor extends React.Component {
|
|||
this.context.matrixClient.sendMessage(roomId, editContent);
|
||||
|
||||
dis.dispatch({action: "edit_event", event: null});
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
|
||||
_cancelPreviousPendingEdit() {
|
||||
|
|
|
@ -113,7 +113,7 @@ module.exports = React.createClass({
|
|||
this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey);
|
||||
},
|
||||
|
||||
onCustomKeyPress: function(event) {
|
||||
onCustomKeyDown: function(event) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -133,7 +133,7 @@ module.exports = React.createClass({
|
|||
picker = (
|
||||
<Field id={`powerSelector_custom_${this.props.powerLevelKey}`} type="number"
|
||||
label={this.props.label || _t("Power level")} max={this.props.maxValue}
|
||||
onBlur={this.onCustomBlur} onKeyPress={this.onCustomKeyPress} onChange={this.onCustomChange}
|
||||
onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} onChange={this.onCustomChange}
|
||||
value={String(this.state.customValue)} disabled={this.props.disabled} />
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import Resend from './Resend';
|
||||
import sdk from './index';
|
||||
import dis from './dispatcher';
|
||||
import Modal from './Modal';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
|
@ -65,10 +64,6 @@ export async function getUnknownDevicesForRoom(matrixClient, room) {
|
|||
return unknownDevices;
|
||||
}
|
||||
|
||||
function focusComposer() {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the UnknownDeviceDialog for a given room. The dialog will inform the user
|
||||
* that messages they sent to this room have not been sent due to unknown devices
|
||||
|
@ -90,7 +85,6 @@ export function showUnknownDeviceDialogForMessages(matrixClient, room) {
|
|||
sendAnywayLabel: _t("Send anyway"),
|
||||
sendLabel: _t("Send"),
|
||||
onSend: onSendClicked,
|
||||
onFinished: focusComposer,
|
||||
}, 'mx_Dialog_unknownDevice');
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue