From 7d087524a576b0e8373ca56ccf264fc885a7ca9a Mon Sep 17 00:00:00 2001 From: Clemens Zeidler Date: Sun, 28 Mar 2021 19:28:48 +1300 Subject: [PATCH 1/3] Make use of the KeyBindingManager in LeftPanel LeftPanel was making key action decisions based on the forwarded event. Use the KeyBindingManager now. Signed-off-by: Clemens Zeidler --- src/components/structures/LeftPanel.tsx | 17 +++++++++-------- src/components/structures/RoomSearch.tsx | 12 ++++++++---- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 2861cfd7e7..cbfc7b476b 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -34,7 +34,6 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore"; import ResizeNotifier from "../../utils/ResizeNotifier"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore"; -import {Key} from "../../Keyboard"; import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; @@ -43,6 +42,7 @@ import LeftPanelWidget from "./LeftPanelWidget"; import {replaceableComponent} from "../../utils/replaceableComponent"; import {mediaFromMxc} from "../../customisations/Media"; import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; +import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; interface IProps { isMinimized: boolean; @@ -297,17 +297,18 @@ export default class LeftPanel extends React.Component { private onKeyDown = (ev: React.KeyboardEvent) => { if (!this.focusedElement) return; - switch (ev.key) { - case Key.ARROW_UP: - case Key.ARROW_DOWN: + const action = getKeyBindingsManager().getRoomListAction(ev); + switch (action) { + case RoomListAction.NextRoom: + case RoomListAction.PrevRoom: ev.stopPropagation(); ev.preventDefault(); - this.onMoveFocus(ev.key === Key.ARROW_UP); + this.onMoveFocus(action === RoomListAction.PrevRoom); break; } }; - private onEnter = () => { + private selectRoom = () => { const firstRoom = this.listContainerRef.current.querySelector(".mx_RoomTile"); if (firstRoom) { firstRoom.click(); @@ -388,8 +389,8 @@ export default class LeftPanel extends React.Component { > { break; case RoomListAction.NextRoom: case RoomListAction.PrevRoom: - this.props.onVerticalArrow(ev); + // we don't handle these actions here put pass the event on to the interested party (LeftPanel) + this.props.onKeyDown(ev); break; case RoomListAction.SelectRoom: { - const shouldClear = this.props.onEnter(ev); + const shouldClear = this.props.onSelectRoom(); if (shouldClear) { // wrap in set immediate to delay it so that we don't clear the filter & then change room setImmediate(() => { From 57cd8afbc49d84a2bd999ee6df8b5cd67ad0b6eb Mon Sep 17 00:00:00 2001 From: Clemens Zeidler Date: Sun, 28 Mar 2021 19:59:33 +1300 Subject: [PATCH 2/3] Split ApplySelection into CompleteOrPrevSelection and CompleteOrNextSelection When moving through the autocomplete selection list distinguish between the following cases: 1) When there is no autocomplete window open, only open one and select the first item when the CompleteOrPrevSelection / CompleteOrNextSelection actions are emitted (e.g. by pressing SHIFT + TAB, TAB) 2) Otherwise navigate through the selection list (e.g. SHIFT + TAB, TAB, UP, DOWN) - Remove references to raw keyboard events in autocomplete.ts - Clarify the purpose of startSelection (previously onTab) Signed-off-by: Clemens Zeidler --- src/KeyBindingsDefaults.ts | 8 ++++---- src/KeyBindingsManager.ts | 13 ++++++++---- .../views/rooms/BasicMessageComposer.tsx | 20 +++++++++---------- src/editor/autocomplete.ts | 12 +++++------ 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts index 0e9d14ea8f..ac9ef1f8cc 100644 --- a/src/KeyBindingsDefaults.ts +++ b/src/KeyBindingsDefaults.ts @@ -161,27 +161,27 @@ const messageComposerBindings = (): KeyBinding[] => { const autocompleteBindings = (): KeyBinding[] => { return [ { - action: AutocompleteAction.ApplySelection, + action: AutocompleteAction.CompleteOrNextSelection, keyCombo: { key: Key.TAB, }, }, { - action: AutocompleteAction.ApplySelection, + action: AutocompleteAction.CompleteOrNextSelection, keyCombo: { key: Key.TAB, ctrlKey: true, }, }, { - action: AutocompleteAction.ApplySelection, + action: AutocompleteAction.CompleteOrPrevSelection, keyCombo: { key: Key.TAB, shiftKey: true, }, }, { - action: AutocompleteAction.ApplySelection, + action: AutocompleteAction.CompleteOrPrevSelection, keyCombo: { key: Key.TAB, ctrlKey: true, diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index 45ef97b121..d862f10c02 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -52,14 +52,19 @@ export enum MessageComposerAction { /** Actions for text editing autocompletion */ export enum AutocompleteAction { - /** Apply the current autocomplete selection */ - ApplySelection = 'ApplySelection', - /** Cancel autocompletion */ - Cancel = 'Cancel', + /** + * Select previous selection or, if the autocompletion window is not shown, open the window and select the first + * selection. + */ + CompleteOrPrevSelection = 'ApplySelection', + /** Select next selection or, if the autocompletion window is not shown, open it and select the first selection */ + CompleteOrNextSelection = 'CompleteOrNextSelection', /** Move to the previous autocomplete selection */ PrevSelection = 'PrevSelection', /** Move to the next autocomplete selection */ NextSelection = 'NextSelection', + /** Close the autocompletion window */ + Cancel = 'Cancel', } /** Actions for the room list sidebar */ diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 5dabd80399..9d9e3a1ba0 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -485,16 +485,14 @@ export default class BasicMessageEditor extends React.Component if (model.autoComplete && model.autoComplete.hasCompletions()) { const autoComplete = model.autoComplete; switch (autocompleteAction) { + case AutocompleteAction.CompleteOrPrevSelection: case AutocompleteAction.PrevSelection: - autoComplete.onUpArrow(event); + autoComplete.selectPreviousSelection(); handled = true; break; + case AutocompleteAction.CompleteOrNextSelection: case AutocompleteAction.NextSelection: - autoComplete.onDownArrow(event); - handled = true; - break; - case AutocompleteAction.ApplySelection: - autoComplete.onTab(event); + autoComplete.selectNextSelection(); handled = true; break; case AutocompleteAction.Cancel: @@ -504,8 +502,10 @@ export default class BasicMessageEditor extends React.Component default: return; // don't preventDefault on anything else } - } else if (autocompleteAction === AutocompleteAction.ApplySelection) { - this.tabCompleteName(event); + } else if (autocompleteAction === AutocompleteAction.CompleteOrPrevSelection + || autocompleteAction === AutocompleteAction.CompleteOrNextSelection) { + // there is no current autocomplete window, try to open it + this.tabCompleteName(); handled = true; } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { this.formatBarRef.current.hide(); @@ -517,7 +517,7 @@ export default class BasicMessageEditor extends React.Component } }; - private async tabCompleteName(event: React.KeyboardEvent) { + private async tabCompleteName() { try { await new Promise(resolve => this.setState({showVisualBell: false}, resolve)); const {model} = this.props; @@ -540,7 +540,7 @@ export default class BasicMessageEditor extends React.Component // Don't try to do things with the autocomplete if there is none shown if (model.autoComplete) { - await model.autoComplete.onTab(event); + await model.autoComplete.startSelection(); if (!model.autoComplete.hasSelection()) { this.setState({showVisualBell: true}); model.autoComplete.close(); diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index d8cea961d4..2f56494ea0 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -68,24 +68,24 @@ export default class AutocompleteWrapperModel { this.updateCallback({close: true}); } - public async onTab(e: KeyboardEvent) { + /** + * If there is no current autocompletion, start one and move to the first selection. + */ + public async startSelection() { const acComponent = this.getAutocompleterComponent(); - if (acComponent.countCompletions() === 0) { // Force completions to show for the text currently entered await acComponent.forceComplete(); // Select the first item by moving "down" await acComponent.moveSelection(+1); - } else { - await acComponent.moveSelection(e.shiftKey ? -1 : +1); } } - public onUpArrow(e: KeyboardEvent) { + public selectPreviousSelection() { this.getAutocompleterComponent().moveSelection(-1); } - public onDownArrow(e: KeyboardEvent) { + public selectNextSelection() { this.getAutocompleterComponent().moveSelection(+1); } From be00320def83fa1f00b7daefd82956803dcc8a3a Mon Sep 17 00:00:00 2001 From: Clemens Zeidler Date: Sun, 28 Mar 2021 22:26:05 +1300 Subject: [PATCH 3/3] Make use of the KeyBindingsManager in the ScrollPanel Signed-off-by: Clemens Zeidler --- src/components/structures/LoggedInView.tsx | 1 + src/components/structures/ScrollPanel.js | 32 ++++++++-------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 5634c1a0c8..0255a3bf35 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -444,6 +444,7 @@ class LoggedInView extends React.Component { case RoomAction.RoomScrollDown: case RoomAction.JumpToFirstMessage: case RoomAction.JumpToLatestMessage: + // pass the event down to the scroll panel this._onScrollKeyPressed(ev); handled = true; break; diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 3a9b2b8a77..976734680c 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -16,10 +16,10 @@ limitations under the License. import React, {createRef} from "react"; import PropTypes from 'prop-types'; -import { Key } from '../../Keyboard'; import Timer from '../../utils/Timer'; import AutoHideScrollbar from "./AutoHideScrollbar"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import {getKeyBindingsManager, RoomAction} from "../../KeyBindingsManager"; const DEBUG_SCROLL = false; @@ -535,29 +535,19 @@ export default class ScrollPanel extends React.Component { * @param {object} ev the keyboard event */ handleScrollKey = ev => { - switch (ev.key) { - case Key.PAGE_UP: - if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - this.scrollRelative(-1); - } + const roomAction = getKeyBindingsManager().getRoomAction(ev); + switch (roomAction) { + case RoomAction.ScrollUp: + this.scrollRelative(-1); break; - - case Key.PAGE_DOWN: - if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - this.scrollRelative(1); - } + case RoomAction.RoomScrollDown: + this.scrollRelative(1); break; - - case Key.HOME: - if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - this.scrollToTop(); - } + case RoomAction.JumpToFirstMessage: + this.scrollToTop(); break; - - case Key.END: - if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - this.scrollToBottom(); - } + case RoomAction.JumpToLatestMessage: + this.scrollToBottom(); break; } };