Document keyboard shortcuts (#7908)
This commit is contained in:
parent
84bd136657
commit
a58b1e9d79
6 changed files with 91 additions and 19 deletions
59
docs/features/keyboardShortcuts.md
Normal file
59
docs/features/keyboardShortcuts.md
Normal 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`.
|
|
@ -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,
|
||||||
|
|
|
@ -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]) => {
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue