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:
Bruno Windels 2019-07-18 16:10:23 +00:00 committed by GitHub
commit 4fa7302f69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 57 additions and 29 deletions

View file

@ -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) {

View file

@ -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}>

View file

@ -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(),

View file

@ -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() {

View file

@ -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 }

View file

@ -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() {

View file

@ -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 {

View file

@ -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');
});
}