Document keyboard shortcuts (#7908)

This commit is contained in:
Šimon Brandner 2022-03-04 13:14:52 +01:00 committed by GitHub
parent 84bd136657
commit a58b1e9d79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 91 additions and 19 deletions

View file

@ -0,0 +1,59 @@
# Keyboard shortcuts
## Using the `KeyBindingManger`
The `KeyBindingManager` (accessible using `getKeyBindingManager()`) is a class
with several methods that allow you to get a `KeyBindingAction` based on a
`KeyboardEvent | React.KeyboardEvent`.
The event passed to the `KeyBindingManager` gets compared to the list of
shortcuts that are retrieved from the `IKeyBindingsProvider`s. The
`IKeyBindingsProvider` is in `KeyBindingDefaults`.
### Examples
Let's say we want to close a menu when the correct keys were pressed:
```ts
const onKeyDown = (ev: KeyboardEvent): void => {
let handled = true;
const action = getKeyBindingManager().getAccessibilityAction(ev)
switch (action) {
case KeyBindingAction.Escape:
closeMenu();
break;
default:
handled = false;
break;
}
if (handled) {
ev.preventDefault();
ev.stopPropagation();
}
}
```
## Managing keyboard shortcuts
There are a few things at play when it comes to keyboard shortcuts. The
`KeyBindingManager` gets `IKeyBindingsProvider`s one of which is
`defaultBindingsProvider` defined in `KeyBindingDefaults`. In
`KeyBindingDefaults` a `getBindingsByCategory()` method is used to create
`KeyBinding`s based on `KeyboardShortcutSetting`s defined in
`KeyboardShortcuts`.
### Adding keyboard shortcuts
To add a keyboard shortcut there are two files we have to look at:
`KeyboardShortcuts.ts` and `KeyBindingDefaults.ts`. In most cases we only need
to edit `KeyboardShortcuts.ts`: add a `KeyBindingAction` and add the
`KeyBindingAction` to the `KEYBOARD_SHORTCUTS` object.
Though, to make matters worse, sometimes we want to add a shortcut that has
multiple keybindings associated with. This keyboard shortcut won't be
customizable as it would be rather difficult to manage both from the point of
the settings and the UI. To do this, we have to add a `KeyBindingAction` and add
the UI representation of that keyboard shortcut to the `getUIOnlyShortcuts()`
method. Then, we also need to add the keybinding to the correct method in
`KeyBindingDefaults`.

View file

@ -26,13 +26,13 @@ import {
import { import {
CATEGORIES, CATEGORIES,
CategoryName, CategoryName,
getCustomizableShortcuts, getKeyboardShortcuts,
KeyBindingAction, KeyBindingAction,
} from "./accessibility/KeyboardShortcuts"; } from "./accessibility/KeyboardShortcuts";
export const getBindingsByCategory = (category: CategoryName): KeyBinding[] => { export const getBindingsByCategory = (category: CategoryName): KeyBinding[] => {
return CATEGORIES[category].settingNames.reduce((bindings, name) => { return CATEGORIES[category].settingNames.reduce((bindings, name) => {
const value = getCustomizableShortcuts()[name]?.default; const value = getKeyboardShortcuts()[name]?.default;
if (value) { if (value) {
bindings.push({ bindings.push({
action: name as KeyBindingAction, action: name as KeyBindingAction,

View file

@ -700,8 +700,12 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = {
}, },
}; };
// XXX: These have to be manually mirrored in KeyBindingDefaults /**
const getNonCustomizableShortcuts = (): IKeyboardShortcuts => { * This function gets the keyboard shortcuts that should be presented in the UI
* but they shouldn't be consumed by KeyBindingDefaults. That means that these
* have to be manually mirrored in KeyBindingDefaults.
*/
const getUIOnlyShortcuts = (): IKeyboardShortcuts => {
const ctrlEnterToSend = SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); const ctrlEnterToSend = SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend');
const keyboardShortcuts: IKeyboardShortcuts = { const keyboardShortcuts: IKeyboardShortcuts = {
@ -741,6 +745,9 @@ const getNonCustomizableShortcuts = (): IKeyboardShortcuts => {
}; };
if (PlatformPeg.get().overrideBrowserShortcuts()) { if (PlatformPeg.get().overrideBrowserShortcuts()) {
// XXX: This keyboard shortcut isn't manually added to
// KeyBindingDefaults as it can't be easily handled by the
// KeyBindingManager
keyboardShortcuts[KeyBindingAction.SwitchToSpaceByNumber] = { keyboardShortcuts[KeyBindingAction.SwitchToSpaceByNumber] = {
default: { default: {
ctrlOrCmdKey: true, ctrlOrCmdKey: true,
@ -753,7 +760,10 @@ const getNonCustomizableShortcuts = (): IKeyboardShortcuts => {
return keyboardShortcuts; return keyboardShortcuts;
}; };
export const getCustomizableShortcuts = (): IKeyboardShortcuts => { /**
* This function gets keyboard shortcuts that can be consumed by the KeyBindingDefaults.
*/
export const getKeyboardShortcuts = (): IKeyboardShortcuts => {
const overrideBrowserShortcuts = PlatformPeg.get().overrideBrowserShortcuts(); const overrideBrowserShortcuts = PlatformPeg.get().overrideBrowserShortcuts();
return Object.keys(KEYBOARD_SHORTCUTS).filter((k: KeyBindingAction) => { return Object.keys(KEYBOARD_SHORTCUTS).filter((k: KeyBindingAction) => {
@ -768,10 +778,13 @@ export const getCustomizableShortcuts = (): IKeyboardShortcuts => {
}, {} as IKeyboardShortcuts); }, {} as IKeyboardShortcuts);
}; };
export const getKeyboardShortcuts = (): IKeyboardShortcuts => { /**
* Gets keyboard shortcuts that should be presented to the user in the UI.
*/
export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => {
const entries = [ const entries = [
...Object.entries(getNonCustomizableShortcuts()), ...Object.entries(getUIOnlyShortcuts()),
...Object.entries(getCustomizableShortcuts()), ...Object.entries(getKeyboardShortcuts()),
]; ];
return entries.reduce((acc, [key, value]) => { return entries.reduce((acc, [key, value]) => {

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { import {
getKeyboardShortcuts, getKeyboardShortcutsForUI,
ALTERNATE_KEY_NAME, ALTERNATE_KEY_NAME,
KEY_ICON, KEY_ICON,
ICategory, ICategory,
@ -32,11 +32,11 @@ import { _t } from "../../../../../languageHandler";
// TODO: This should return KeyCombo but it has ctrlOrCmd instead of ctrlOrCmdKey // TODO: This should return KeyCombo but it has ctrlOrCmd instead of ctrlOrCmdKey
const getKeyboardShortcutValue = (name: string): KeyBindingConfig => { const getKeyboardShortcutValue = (name: string): KeyBindingConfig => {
return getKeyboardShortcuts()[name]?.default; return getKeyboardShortcutsForUI()[name]?.default;
}; };
const getKeyboardShortcutDisplayName = (name: string): string | null => { const getKeyboardShortcutDisplayName = (name: string): string | null => {
const keyboardShortcutDisplayName = getKeyboardShortcuts()[name]?.displayName; const keyboardShortcutDisplayName = getKeyboardShortcutsForUI()[name]?.displayName;
return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName); return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName);
}; };

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { import {
getCustomizableShortcuts, getKeyboardShortcutsForUI,
getKeyboardShortcuts, getKeyboardShortcuts,
KEYBOARD_SHORTCUTS, KEYBOARD_SHORTCUTS,
mock, mock,
@ -35,10 +35,10 @@ describe("KeyboardShortcuts", () => {
PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => false }); PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => false });
const copyKeyboardShortcuts = Object.assign({}, KEYBOARD_SHORTCUTS); const copyKeyboardShortcuts = Object.assign({}, KEYBOARD_SHORTCUTS);
getCustomizableShortcuts();
expect(KEYBOARD_SHORTCUTS).toEqual(copyKeyboardShortcuts);
getKeyboardShortcuts(); getKeyboardShortcuts();
expect(KEYBOARD_SHORTCUTS).toEqual(copyKeyboardShortcuts); expect(KEYBOARD_SHORTCUTS).toEqual(copyKeyboardShortcuts);
getKeyboardShortcutsForUI();
expect(KEYBOARD_SHORTCUTS).toEqual(copyKeyboardShortcuts);
}); });
it("correctly filters shortcuts", async () => { it("correctly filters shortcuts", async () => {
@ -54,7 +54,7 @@ describe("KeyboardShortcuts", () => {
}); });
PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => false }); PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => false });
expect(getCustomizableShortcuts()).toEqual({ "Keybind4": {} }); expect(getKeyboardShortcuts()).toEqual({ "Keybind4": {} });
mock({ mock({
keyboardShortcuts: { keyboardShortcuts: {
@ -65,7 +65,7 @@ describe("KeyboardShortcuts", () => {
desktopShortcuts: ["Keybind2"], desktopShortcuts: ["Keybind2"],
}); });
PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => true }); PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => true });
expect(getCustomizableShortcuts()).toEqual({ "Keybind1": {}, "Keybind2": {} }); expect(getKeyboardShortcuts()).toEqual({ "Keybind1": {}, "Keybind2": {} });
jest.resetModules(); jest.resetModules();
}); });
}); });

View file

@ -60,7 +60,7 @@ describe("KeyboardUserSettingsTab", () => {
it("doesn't render same modifier twice", async () => { it("doesn't render same modifier twice", async () => {
mockKeyboardShortcuts({ mockKeyboardShortcuts({
"getKeyboardShortcuts": () => ({ "getKeyboardShortcutsForUI": () => ({
"keybind1": { "keybind1": {
default: { default: {
key: Key.A, key: Key.A,
@ -76,7 +76,7 @@ describe("KeyboardUserSettingsTab", () => {
jest.resetModules(); jest.resetModules();
mockKeyboardShortcuts({ mockKeyboardShortcuts({
"getKeyboardShortcuts": () => ({ "getKeyboardShortcutsForUI": () => ({
"keybind1": { "keybind1": {
default: { default: {
key: Key.A, key: Key.A,
@ -94,7 +94,7 @@ describe("KeyboardUserSettingsTab", () => {
it("renders list of keyboard shortcuts", async () => { it("renders list of keyboard shortcuts", async () => {
mockKeyboardShortcuts({ mockKeyboardShortcuts({
"getKeyboardShortcuts": () => ({ "getKeyboardShortcutsForUI": () => ({
"keybind1": { "keybind1": {
default: { default: {
key: Key.A, key: Key.A,