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);
|
this.inprogress.push(upload);
|
||||||
dis.dispatch({action: 'upload_started'});
|
dis.dispatch({action: 'upload_started'});
|
||||||
|
|
||||||
// Focus the composer view
|
|
||||||
dis.dispatch({action: 'focus_composer'});
|
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
|
|
||||||
function onProgress(ev) {
|
function onProgress(ev) {
|
||||||
|
|
|
@ -106,7 +106,7 @@ const LoggedInView = React.createClass({
|
||||||
|
|
||||||
CallMediaHandler.loadDevices();
|
CallMediaHandler.loadDevices();
|
||||||
|
|
||||||
document.addEventListener('keydown', this._onKeyDown);
|
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||||
|
|
||||||
this._sessionStore = sessionStore;
|
this._sessionStore = sessionStore;
|
||||||
this._sessionStoreToken = this._sessionStore.addListener(
|
this._sessionStoreToken = this._sessionStore.addListener(
|
||||||
|
@ -136,7 +136,7 @@ const LoggedInView = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
document.removeEventListener('keydown', this._onKeyDown);
|
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
||||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||||
this._matrixClient.removeListener("sync", this.onSync);
|
this._matrixClient.removeListener("sync", this.onSync);
|
||||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
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) {
|
_onKeyDown: function(ev) {
|
||||||
/*
|
/*
|
||||||
|
@ -333,6 +369,21 @@ const LoggedInView = React.createClass({
|
||||||
if (handled) {
|
if (handled) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
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 (
|
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 }
|
{ topBar }
|
||||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||||
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
||||||
|
|
|
@ -268,8 +268,6 @@ export default React.createClass({
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
|
||||||
this.focusComposer = false;
|
|
||||||
|
|
||||||
// this can technically be done anywhere but doing this here keeps all
|
// this can technically be done anywhere but doing this here keeps all
|
||||||
// the routing url path logic together.
|
// the routing url path logic together.
|
||||||
if (this.onAliasClick) {
|
if (this.onAliasClick) {
|
||||||
|
@ -362,10 +360,6 @@ export default React.createClass({
|
||||||
const durationMs = this.stopPageChangeTimer();
|
const durationMs = this.stopPageChangeTimer();
|
||||||
Analytics.trackPageChange(durationMs);
|
Analytics.trackPageChange(durationMs);
|
||||||
}
|
}
|
||||||
if (this.focusComposer) {
|
|
||||||
dis.dispatch({action: 'focus_composer'});
|
|
||||||
this.focusComposer = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
startPageChangeTimer() {
|
startPageChangeTimer() {
|
||||||
|
@ -793,8 +787,6 @@ export default React.createClass({
|
||||||
// that has been passed out-of-band (eg.
|
// that has been passed out-of-band (eg.
|
||||||
// room name and avatar from an invite email)
|
// room name and avatar from an invite email)
|
||||||
_viewRoom: function(roomInfo) {
|
_viewRoom: function(roomInfo) {
|
||||||
this.focusComposer = true;
|
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
view: VIEWS.LOGGED_IN,
|
view: VIEWS.LOGGED_IN,
|
||||||
currentRoomId: roomInfo.room_id || null,
|
currentRoomId: roomInfo.room_id || null,
|
||||||
|
@ -1368,7 +1360,6 @@ export default React.createClass({
|
||||||
self.firstSyncComplete = true;
|
self.firstSyncComplete = true;
|
||||||
self.firstSyncPromise.resolve();
|
self.firstSyncPromise.resolve();
|
||||||
|
|
||||||
dis.dispatch({action: 'focus_composer'});
|
|
||||||
self.setState({
|
self.setState({
|
||||||
ready: true,
|
ready: true,
|
||||||
showNotifierToolbar: Notifier.shouldShowToolbar(),
|
showNotifierToolbar: Notifier.shouldShowToolbar(),
|
||||||
|
|
|
@ -135,12 +135,10 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_onResendAllClick: function() {
|
_onResendAllClick: function() {
|
||||||
Resend.resendUnsentEvents(this.props.room);
|
Resend.resendUnsentEvents(this.props.room);
|
||||||
dis.dispatch({action: 'focus_composer'});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_onCancelAllClick: function() {
|
_onCancelAllClick: function() {
|
||||||
Resend.cancelUnsentEvents(this.props.room);
|
Resend.cancelUnsentEvents(this.props.room);
|
||||||
dis.dispatch({action: 'focus_composer'});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_onShowDevicesClick: function() {
|
_onShowDevicesClick: function() {
|
||||||
|
|
|
@ -48,7 +48,7 @@ class MenuOption extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className={optClasses}
|
return <div className={optClasses}
|
||||||
onClick={this._onClick} onKeyPress={this._onKeyPress}
|
onClick={this._onClick}
|
||||||
onMouseEnter={this._onMouseEnter}
|
onMouseEnter={this._onMouseEnter}
|
||||||
>
|
>
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
|
|
|
@ -222,7 +222,6 @@ export default class MessageEditor extends React.Component {
|
||||||
dis.dispatch({action: 'edit_event', event: nextEvent});
|
dis.dispatch({action: 'edit_event', event: nextEvent});
|
||||||
} else {
|
} else {
|
||||||
dis.dispatch({action: 'edit_event', event: null});
|
dis.dispatch({action: 'edit_event', event: null});
|
||||||
dis.dispatch({action: 'focus_composer'});
|
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -230,7 +229,6 @@ export default class MessageEditor extends React.Component {
|
||||||
|
|
||||||
_cancelEdit = () => {
|
_cancelEdit = () => {
|
||||||
dis.dispatch({action: "edit_event", event: null});
|
dis.dispatch({action: "edit_event", event: null});
|
||||||
dis.dispatch({action: 'focus_composer'});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_hasModifications(newContent) {
|
_hasModifications(newContent) {
|
||||||
|
@ -257,7 +255,6 @@ export default class MessageEditor extends React.Component {
|
||||||
this.context.matrixClient.sendMessage(roomId, editContent);
|
this.context.matrixClient.sendMessage(roomId, editContent);
|
||||||
|
|
||||||
dis.dispatch({action: "edit_event", event: null});
|
dis.dispatch({action: "edit_event", event: null});
|
||||||
dis.dispatch({action: 'focus_composer'});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_cancelPreviousPendingEdit() {
|
_cancelPreviousPendingEdit() {
|
||||||
|
|
|
@ -113,7 +113,7 @@ module.exports = React.createClass({
|
||||||
this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey);
|
this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey);
|
||||||
},
|
},
|
||||||
|
|
||||||
onCustomKeyPress: function(event) {
|
onCustomKeyDown: function(event) {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -133,7 +133,7 @@ module.exports = React.createClass({
|
||||||
picker = (
|
picker = (
|
||||||
<Field id={`powerSelector_custom_${this.props.powerLevelKey}`} type="number"
|
<Field id={`powerSelector_custom_${this.props.powerLevelKey}`} type="number"
|
||||||
label={this.props.label || _t("Power level")} max={this.props.maxValue}
|
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} />
|
value={String(this.state.customValue)} disabled={this.props.disabled} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
|
|
||||||
import Resend from './Resend';
|
import Resend from './Resend';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
import dis from './dispatcher';
|
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
|
@ -65,10 +64,6 @@ export async function getUnknownDevicesForRoom(matrixClient, room) {
|
||||||
return unknownDevices;
|
return unknownDevices;
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusComposer() {
|
|
||||||
dis.dispatch({action: 'focus_composer'});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the UnknownDeviceDialog for a given room. The dialog will inform the user
|
* 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
|
* 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"),
|
sendAnywayLabel: _t("Send anyway"),
|
||||||
sendLabel: _t("Send"),
|
sendLabel: _t("Send"),
|
||||||
onSend: onSendClicked,
|
onSend: onSendClicked,
|
||||||
onFinished: focusComposer,
|
|
||||||
}, 'mx_Dialog_unknownDevice');
|
}, 'mx_Dialog_unknownDevice');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue