From af2a0c71f9cbbeab675ac657f55ec532bd6e8e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 11 Mar 2022 17:08:24 +0100 Subject: [PATCH 1/7] Add `hideEditUI` flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/settings/Settings.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index fc4c4a68ced..ac9ad292207 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -171,6 +171,9 @@ export interface IBaseSetting { extraSettings?: string[]; requiresRefresh?: boolean; }; + + // Optional value to hide the UI to edit this setting + hideEditUI?: boolean; } export interface IFeature extends Omit, "isFeature"> { From 674dfc6fc0050ac00ebd76a986a96b7cc6c73f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 11 Mar 2022 17:10:10 +0100 Subject: [PATCH 2/7] `hideEditUI` for some shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/accessibility/KeyboardShortcutUtils.ts | 6 ++++++ src/accessibility/KeyboardShortcuts.ts | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/accessibility/KeyboardShortcutUtils.ts b/src/accessibility/KeyboardShortcutUtils.ts index 434116d4303..17f3b6f56dd 100644 --- a/src/accessibility/KeyboardShortcutUtils.ts +++ b/src/accessibility/KeyboardShortcutUtils.ts @@ -43,6 +43,7 @@ const getUIOnlyShortcuts = (): IKeyboardShortcuts => { ctrlOrCmdKey: ctrlEnterToSend, }, displayName: _td("Send message"), + hideEditUI: true, }, [KeyBindingAction.NewLine]: { default: { @@ -50,18 +51,21 @@ const getUIOnlyShortcuts = (): IKeyboardShortcuts => { shiftKey: !ctrlEnterToSend, }, displayName: _td("New line"), + hideEditUI: true, }, [KeyBindingAction.CompleteAutocomplete]: { default: { key: Key.ENTER, }, displayName: _td("Complete"), + hideEditUI: true, }, [KeyBindingAction.ForceCompleteAutocomplete]: { default: { key: Key.TAB, }, displayName: _td("Force complete"), + hideEditUI: true, }, [KeyBindingAction.SearchInRoom]: { default: { @@ -69,6 +73,7 @@ const getUIOnlyShortcuts = (): IKeyboardShortcuts => { key: Key.F, }, displayName: _td("Search (must be enabled)"), + hideEditUI: true, }, }; @@ -82,6 +87,7 @@ const getUIOnlyShortcuts = (): IKeyboardShortcuts => { key: DIGITS, }, displayName: _td("Switch to space by number"), + hideEditUI: true, }; } diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index 86bb72b46a9..620efe0beae 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -626,6 +626,7 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { shiftKey: isMac, }, displayName: _td("Redo edit"), + hideEditUI: true, }, [KeyBindingAction.PreviousVisitedRoomOrCommunity]: { default: { @@ -634,6 +635,7 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { key: isMac ? Key.SQUARE_BRACKET_LEFT : Key.ARROW_LEFT, }, displayName: _td("Previous recently visited room or community"), + hideEditUI: true, }, [KeyBindingAction.NextVisitedRoomOrCommunity]: { default: { @@ -642,6 +644,7 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { key: isMac ? Key.SQUARE_BRACKET_RIGHT : Key.ARROW_RIGHT, }, displayName: _td("Next recently visited room or community"), + hideEditUI: true, }, [KeyBindingAction.SwitchToSpaceByNumber]: { default: { @@ -649,6 +652,7 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { key: DIGITS, }, displayName: _td("Switch to space by number"), + hideEditUI: true, }, [KeyBindingAction.OpenUserSettings]: { default: { @@ -662,62 +666,74 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { key: Key.ESCAPE, }, displayName: _td("Close dialog or context menu"), + hideEditUI: true, }, [KeyBindingAction.Enter]: { default: { key: Key.ENTER, }, displayName: _td("Activate selected button"), + hideEditUI: true, }, [KeyBindingAction.Space]: { default: { key: Key.SPACE, }, + hideEditUI: true, }, [KeyBindingAction.Backspace]: { default: { key: Key.BACKSPACE, }, + hideEditUI: true, }, [KeyBindingAction.Delete]: { default: { key: Key.DELETE, }, + hideEditUI: true, }, [KeyBindingAction.Home]: { default: { key: Key.HOME, }, + hideEditUI: true, }, [KeyBindingAction.End]: { default: { key: Key.END, }, + hideEditUI: true, }, [KeyBindingAction.ArrowLeft]: { default: { key: Key.ARROW_LEFT, }, + hideEditUI: true, }, [KeyBindingAction.ArrowUp]: { default: { key: Key.ARROW_UP, }, + hideEditUI: true, }, [KeyBindingAction.ArrowRight]: { default: { key: Key.ARROW_RIGHT, }, + hideEditUI: true, }, [KeyBindingAction.ArrowDown]: { default: { key: Key.ARROW_DOWN, }, + hideEditUI: true, }, [KeyBindingAction.Comma]: { default: { key: Key.COMMA, }, + hideEditUI: true, }, }; From f7236d3b56144a4ee9daf20566a0b80c1c9f1cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 12 Mar 2022 13:29:53 +0100 Subject: [PATCH 3/7] Add `feature_customizable_keybindings` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/settings/Settings.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index ac9ad292207..754cffb4f33 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -324,6 +324,13 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td("Show current avatar and name for users in message history"), default: false, }, + "feature_customizable_keybindings": { + isFeature: true, + labsGroup: LabGroup.Experimental, + supportedLevels: LEVELS_FEATURE, + displayName: _td("Allow for customizing keyboard shortcuts"), + default: false, + }, "doNotDisturb": { supportedLevels: [SettingLevel.DEVICE], default: false, From b94bc8f615634bb02cdb3275f8ca1f78d9d6ee56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 12 Mar 2022 14:56:27 +0100 Subject: [PATCH 4/7] Add `getHideEditUI()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/settings/SettingsStore.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index ca9bfe3703e..3a0b008445b 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -272,6 +272,16 @@ export default class SettingsStore { return SETTINGS[settingName].isFeature; } + /** + * Determines if the user should be able to edit the setting using the UI + * @param {string} settingName The setting to look up. + * @return {boolean} True if the user shouldn't be able to edit the setting from UI. + */ + public static getHideEditUI(settingName: string): boolean | null { + if (!SETTINGS[settingName]) return null; + return SETTINGS[settingName].hideEditUI; + } + public static getBetaInfo(settingName: string): ISetting["betaInfo"] { // consider a beta disabled if the config is explicitly set to false, in which case treat as normal Labs flag if (SettingsStore.isFeature(settingName) From 63202984f2d797fb3c2127930217249ffab33283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 12 Mar 2022 14:57:41 +0100 Subject: [PATCH 5/7] Use `getKeyboardShortcutValue()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/KeyBindingsDefaults.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts index d4f4ffc6811..da744dcc2f6 100644 --- a/src/KeyBindingsDefaults.ts +++ b/src/KeyBindingsDefaults.ts @@ -28,11 +28,11 @@ import { CategoryName, KeyBindingAction, } from "./accessibility/KeyboardShortcuts"; -import { getKeyboardShortcuts } from "./accessibility/KeyboardShortcutUtils"; +import { getKeyboardShortcutValue } from "./accessibility/KeyboardShortcutUtils"; export const getBindingsByCategory = (category: CategoryName): KeyBinding[] => { return CATEGORIES[category].settingNames.reduce((bindings, name) => { - const value = getKeyboardShortcuts()[name]?.default; + const value = getKeyboardShortcutValue(name); if (value) { bindings.push({ action: name as KeyBindingAction, From 982912e828de705a93fe1df41e826f2e02880489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 12 Mar 2022 15:05:49 +0100 Subject: [PATCH 6/7] Move `KEYBOARD_SHORTCUTS` to `SETTINGS` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/KeyBindingsDefaults.ts | 2 +- src/accessibility/KeyboardShortcutUtils.ts | 63 ++- src/accessibility/KeyboardShortcuts.ts | 425 +--------------- .../tabs/user/KeyboardUserSettingsTab.tsx | 6 + src/settings/Settings.tsx | 460 +++++++++++++++++- .../KeyboardShortcutUtils-test.ts | 70 --- .../views/settings/KeyboardShortcut-test.tsx | 33 +- .../KeyboardShortcut-test.tsx.snap | 70 ++- .../user/KeyboardUserSettingsTab-test.tsx | 121 ++--- .../KeyboardUserSettingsTab-test.tsx.snap | 12 +- 10 files changed, 600 insertions(+), 662 deletions(-) delete mode 100644 test/accessibility/KeyboardShortcutUtils-test.ts diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts index da744dcc2f6..843c8b5b33b 100644 --- a/src/KeyBindingsDefaults.ts +++ b/src/KeyBindingsDefaults.ts @@ -32,7 +32,7 @@ import { getKeyboardShortcutValue } from "./accessibility/KeyboardShortcutUtils" export const getBindingsByCategory = (category: CategoryName): KeyBinding[] => { return CATEGORIES[category].settingNames.reduce((bindings, name) => { - const value = getKeyboardShortcutValue(name); + const value = getKeyboardShortcutValue(name, false); if (value) { bindings.push({ action: name as KeyBindingAction, diff --git a/src/accessibility/KeyboardShortcutUtils.ts b/src/accessibility/KeyboardShortcutUtils.ts index 17f3b6f56dd..5f230955b07 100644 --- a/src/accessibility/KeyboardShortcutUtils.ts +++ b/src/accessibility/KeyboardShortcutUtils.ts @@ -24,10 +24,19 @@ import { DIGITS, IKeyboardShortcuts, KeyBindingAction, - KEYBOARD_SHORTCUTS, MAC_ONLY_SHORTCUTS, } from "./KeyboardShortcuts"; +const isShortcutEnabled = (name): boolean => { + const overrideBrowserShortcuts = PlatformPeg.get().overrideBrowserShortcuts(); + + if (!SettingsStore.isEnabled(name)) return false; + if (MAC_ONLY_SHORTCUTS.includes(name) && !isMac) return false; + if (DESKTOP_SHORTCUTS.includes(name) && !overrideBrowserShortcuts) return false; + + return true; +}; + /** * 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 @@ -94,44 +103,28 @@ const getUIOnlyShortcuts = (): IKeyboardShortcuts => { return keyboardShortcuts; }; -/** - * This function gets keyboard shortcuts that can be consumed by the KeyBindingDefaults. - */ -export const getKeyboardShortcuts = (): IKeyboardShortcuts => { - const overrideBrowserShortcuts = PlatformPeg.get().overrideBrowserShortcuts(); - - return Object.keys(KEYBOARD_SHORTCUTS).filter((k: KeyBindingAction) => { - if (KEYBOARD_SHORTCUTS[k]?.controller?.settingDisabled) return false; - if (MAC_ONLY_SHORTCUTS.includes(k) && !isMac) return false; - if (DESKTOP_SHORTCUTS.includes(k) && !overrideBrowserShortcuts) return false; +export const getKeyboardShortcutValue = (name: string, fallbackToUIOnly = true): KeyCombo | null => { + if (!isShortcutEnabled(name)) return null; - return true; - }).reduce((o, key) => { - o[key] = KEYBOARD_SHORTCUTS[key]; - return o; - }, {} as IKeyboardShortcuts); + try { + return SettingsStore.getValue("feature_customizable_keybindings") + ? SettingsStore.getValue(name) + : SettingsStore.getDefaultValue(name); + } catch (error) { + if (!fallbackToUIOnly) return null; + return getUIOnlyShortcuts()[name]?.default; + } }; -/** - * Gets keyboard shortcuts that should be presented to the user in the UI. - */ -export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => { - const entries = [ - ...Object.entries(getUIOnlyShortcuts()), - ...Object.entries(getKeyboardShortcuts()), - ]; - - return entries.reduce((acc, [key, value]) => { - acc[key] = value; - return acc; - }, {} as IKeyboardShortcuts); -}; +export const getKeyboardShortcutDisplayName = (name: string): string | null => { + if (!isShortcutEnabled(name)) return null; -export const getKeyboardShortcutValue = (name: string): KeyCombo => { - return getKeyboardShortcutsForUI()[name]?.default; + const keyboardShortcutDisplayName = SettingsStore.getDisplayName(name) ?? getUIOnlyShortcuts()[name]?.displayName; + return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName); }; -export const getKeyboardShortcutDisplayName = (name: string): string | null => { - const keyboardShortcutDisplayName = getKeyboardShortcutsForUI()[name]?.displayName; - return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName); +export const getKeyboardShortcutHideEditUI = (name: string): boolean | null => { + if (!isShortcutEnabled(name)) return null; + + return SettingsStore.getHideEditUI(name) ?? false; }; diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index 620efe0beae..7fd002c997a 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -18,7 +18,6 @@ limitations under the License. import { _td } from "../languageHandler"; import { isMac, Key } from "../Keyboard"; import { IBaseSetting } from "../settings/Settings"; -import IncompatibleController from "../settings/controllers/IncompatibleController"; import { KeyCombo } from "../KeyBindingsManager"; export enum KeyBindingAction { @@ -317,430 +316,8 @@ export const MAC_ONLY_SHORTCUTS = [ KeyBindingAction.OpenUserSettings, ]; -// This is very intentionally modelled after SETTINGS as it will make it easier -// to implement customizable keyboard shortcuts -// TODO: TravisR will fix this nightmare when the new version of the SettingsStore becomes a thing -// XXX: Exported for tests -export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { - [KeyBindingAction.FormatBold]: { - default: { - ctrlOrCmdKey: true, - key: Key.B, - }, - displayName: _td("Toggle Bold"), - }, - [KeyBindingAction.FormatItalics]: { - default: { - ctrlOrCmdKey: true, - key: Key.I, - }, - displayName: _td("Toggle Italics"), - }, - [KeyBindingAction.FormatQuote]: { - default: { - ctrlOrCmdKey: true, - key: Key.GREATER_THAN, - }, - displayName: _td("Toggle Quote"), - }, - [KeyBindingAction.FormatCode]: { - default: { - ctrlOrCmdKey: true, - key: Key.E, - }, - displayName: _td("Toggle Code Block"), - }, - [KeyBindingAction.FormatLink]: { - default: { - ctrlOrCmdKey: true, - shiftKey: true, - key: Key.L, - }, - displayName: _td("Toggle Link"), - }, - [KeyBindingAction.CancelReplyOrEdit]: { - default: { - key: Key.ESCAPE, - }, - displayName: _td("Cancel replying to a message"), - }, - [KeyBindingAction.EditNextMessage]: { - default: { - key: Key.ARROW_DOWN, - }, - displayName: _td("Navigate to next message to edit"), - }, - [KeyBindingAction.EditPrevMessage]: { - default: { - key: Key.ARROW_UP, - }, - displayName: _td("Navigate to previous message to edit"), - }, - [KeyBindingAction.MoveCursorToStart]: { - default: { - ctrlOrCmdKey: true, - key: Key.HOME, - }, - displayName: _td("Jump to start of the composer"), - }, - [KeyBindingAction.MoveCursorToEnd]: { - default: { - ctrlOrCmdKey: true, - key: Key.END, - }, - displayName: _td("Jump to end of the composer"), - }, - [KeyBindingAction.SelectNextSendHistory]: { - default: { - altKey: true, - ctrlKey: true, - key: Key.ARROW_DOWN, - }, - displayName: _td("Navigate to next message in composer history"), - }, - [KeyBindingAction.SelectPrevSendHistory]: { - default: { - altKey: true, - ctrlKey: true, - key: Key.ARROW_UP, - }, - displayName: _td("Navigate to previous message in composer history"), - }, - [KeyBindingAction.ShowStickerPicker]: { - default: { - ctrlOrCmdKey: true, - key: Key.SEMICOLON, - }, - displayName: _td("Send a sticker"), - }, - [KeyBindingAction.ToggleMicInCall]: { - default: { - ctrlOrCmdKey: true, - key: Key.D, - }, - displayName: _td("Toggle microphone mute"), - }, - [KeyBindingAction.ToggleWebcamInCall]: { - default: { - ctrlOrCmdKey: true, - key: Key.E, - }, - displayName: _td("Toggle webcam on/off"), - }, - [KeyBindingAction.DismissReadMarker]: { - default: { - key: Key.ESCAPE, - }, - displayName: _td("Dismiss read marker and jump to bottom"), - }, - [KeyBindingAction.JumpToOldestUnread]: { - default: { - shiftKey: true, - key: Key.PAGE_UP, - }, - displayName: _td("Jump to oldest unread message"), - }, - [KeyBindingAction.UploadFile]: { - default: { - ctrlOrCmdKey: true, - shiftKey: true, - key: Key.U, - }, - displayName: _td("Upload a file"), - }, - [KeyBindingAction.ScrollUp]: { - default: { - key: Key.PAGE_UP, - }, - displayName: _td("Scroll up in the timeline"), - }, - [KeyBindingAction.ScrollDown]: { - default: { - key: Key.PAGE_DOWN, - }, - displayName: _td("Scroll down in the timeline"), - }, - [KeyBindingAction.FilterRooms]: { - default: { - ctrlOrCmdKey: true, - key: Key.K, - }, - displayName: _td("Jump to room search"), - }, - [KeyBindingAction.SelectRoomInRoomList]: { - default: { - key: Key.ENTER, - }, - displayName: _td("Select room from the room list"), - }, - [KeyBindingAction.CollapseRoomListSection]: { - default: { - key: Key.ARROW_LEFT, - }, - displayName: _td("Collapse room list section"), - }, - [KeyBindingAction.ExpandRoomListSection]: { - default: { - key: Key.ARROW_RIGHT, - }, - displayName: _td("Expand room list section"), - }, - [KeyBindingAction.ClearRoomFilter]: { - default: { - key: Key.ESCAPE, - }, - displayName: _td("Clear room list filter field"), - controller: new IncompatibleController("feature_spotlight", { key: null }), - }, - [KeyBindingAction.NextRoom]: { - default: { - key: Key.ARROW_DOWN, - }, - displayName: _td("Navigate down in the room list"), - }, - [KeyBindingAction.PrevRoom]: { - default: { - key: Key.ARROW_UP, - }, - displayName: _td("Navigate up in the room list"), - }, - [KeyBindingAction.ToggleUserMenu]: { - default: { - ctrlOrCmdKey: true, - key: Key.BACKTICK, - }, - displayName: _td("Toggle the top left menu"), - }, - [KeyBindingAction.ToggleRoomSidePanel]: { - default: { - ctrlOrCmdKey: true, - key: Key.PERIOD, - }, - displayName: _td("Toggle right panel"), - }, - [KeyBindingAction.ShowKeyboardSettings]: { - default: { - ctrlOrCmdKey: true, - key: Key.SLASH, - }, - displayName: _td("Open this settings tab"), - }, - [KeyBindingAction.GoToHome]: { - default: { - ctrlOrCmdKey: true, - altKey: !isMac, - shiftKey: isMac, - key: Key.H, - }, - displayName: _td("Go to Home View"), - }, - [KeyBindingAction.SelectNextUnreadRoom]: { - default: { - shiftKey: true, - altKey: true, - key: Key.ARROW_DOWN, - }, - displayName: _td("Next unread room or DM"), - }, - [KeyBindingAction.SelectPrevUnreadRoom]: { - default: { - shiftKey: true, - altKey: true, - key: Key.ARROW_UP, - }, - displayName: _td("Previous unread room or DM"), - }, - [KeyBindingAction.SelectNextRoom]: { - default: { - altKey: true, - key: Key.ARROW_DOWN, - }, - displayName: _td("Next room or DM"), - }, - [KeyBindingAction.SelectPrevRoom]: { - default: { - altKey: true, - key: Key.ARROW_UP, - }, - displayName: _td("Previous room or DM"), - }, - [KeyBindingAction.CancelAutocomplete]: { - default: { - key: Key.ESCAPE, - }, - displayName: _td("Cancel autocomplete"), - }, - [KeyBindingAction.NextSelectionInAutocomplete]: { - default: { - key: Key.ARROW_DOWN, - }, - displayName: _td("Next autocomplete suggestion"), - }, - [KeyBindingAction.PrevSelectionInAutocomplete]: { - default: { - key: Key.ARROW_UP, - }, - displayName: _td("Previous autocomplete suggestion"), - }, - [KeyBindingAction.ToggleSpacePanel]: { - default: { - ctrlOrCmdKey: true, - shiftKey: true, - key: Key.D, - }, - displayName: _td("Toggle space panel"), - }, - [KeyBindingAction.ToggleHiddenEventVisibility]: { - default: { - ctrlOrCmdKey: true, - shiftKey: true, - key: Key.H, - }, - displayName: _td("Toggle hidden event visibility"), - }, - [KeyBindingAction.JumpToFirstMessage]: { - default: { - key: Key.HOME, - ctrlKey: true, - }, - displayName: _td("Jump to first message"), - }, - [KeyBindingAction.JumpToLatestMessage]: { - default: { - key: Key.END, - ctrlKey: true, - }, - displayName: _td("Jump to last message"), - }, - [KeyBindingAction.EditUndo]: { - default: { - key: Key.Z, - ctrlOrCmdKey: true, - }, - displayName: _td("Undo edit"), - }, - [KeyBindingAction.EditRedo]: { - default: { - key: isMac ? Key.Z : Key.Y, - ctrlOrCmdKey: true, - shiftKey: isMac, - }, - displayName: _td("Redo edit"), - hideEditUI: true, - }, - [KeyBindingAction.PreviousVisitedRoomOrCommunity]: { - default: { - metaKey: isMac, - altKey: !isMac, - key: isMac ? Key.SQUARE_BRACKET_LEFT : Key.ARROW_LEFT, - }, - displayName: _td("Previous recently visited room or community"), - hideEditUI: true, - }, - [KeyBindingAction.NextVisitedRoomOrCommunity]: { - default: { - metaKey: isMac, - altKey: !isMac, - key: isMac ? Key.SQUARE_BRACKET_RIGHT : Key.ARROW_RIGHT, - }, - displayName: _td("Next recently visited room or community"), - hideEditUI: true, - }, - [KeyBindingAction.SwitchToSpaceByNumber]: { - default: { - ctrlOrCmdKey: true, - key: DIGITS, - }, - displayName: _td("Switch to space by number"), - hideEditUI: true, - }, - [KeyBindingAction.OpenUserSettings]: { - default: { - metaKey: true, - key: Key.COMMA, - }, - displayName: _td("Open user settings"), - }, - [KeyBindingAction.Escape]: { - default: { - key: Key.ESCAPE, - }, - displayName: _td("Close dialog or context menu"), - hideEditUI: true, - }, - [KeyBindingAction.Enter]: { - default: { - key: Key.ENTER, - }, - displayName: _td("Activate selected button"), - hideEditUI: true, - }, - [KeyBindingAction.Space]: { - default: { - key: Key.SPACE, - }, - hideEditUI: true, - }, - [KeyBindingAction.Backspace]: { - default: { - key: Key.BACKSPACE, - }, - hideEditUI: true, - }, - [KeyBindingAction.Delete]: { - default: { - key: Key.DELETE, - }, - hideEditUI: true, - }, - [KeyBindingAction.Home]: { - default: { - key: Key.HOME, - }, - hideEditUI: true, - }, - [KeyBindingAction.End]: { - default: { - key: Key.END, - }, - hideEditUI: true, - }, - [KeyBindingAction.ArrowLeft]: { - default: { - key: Key.ARROW_LEFT, - }, - hideEditUI: true, - }, - [KeyBindingAction.ArrowUp]: { - default: { - key: Key.ARROW_UP, - }, - hideEditUI: true, - }, - [KeyBindingAction.ArrowRight]: { - default: { - key: Key.ARROW_RIGHT, - }, - hideEditUI: true, - }, - [KeyBindingAction.ArrowDown]: { - default: { - key: Key.ARROW_DOWN, - }, - hideEditUI: true, - }, - [KeyBindingAction.Comma]: { - default: { - key: Key.COMMA, - }, - hideEditUI: true, - }, -}; - // For tests -export function mock({ keyboardShortcuts, macOnlyShortcuts, desktopShortcuts }): void { - Object.keys(KEYBOARD_SHORTCUTS).forEach((k) => delete KEYBOARD_SHORTCUTS[k]); - if (keyboardShortcuts) Object.assign(KEYBOARD_SHORTCUTS, keyboardShortcuts); +export function mock({ macOnlyShortcuts, desktopShortcuts }): void { MAC_ONLY_SHORTCUTS.splice(0, MAC_ONLY_SHORTCUTS.length); if (macOnlyShortcuts) macOnlyShortcuts.forEach((e) => MAC_ONLY_SHORTCUTS.push(e)); DESKTOP_SHORTCUTS.splice(0, DESKTOP_SHORTCUTS.length); diff --git a/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx b/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx index ef38c16078b..0327e07ccc8 100644 --- a/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx @@ -74,3 +74,9 @@ const KeyboardUserSettingsTab: React.FC = () => { }; export default KeyboardUserSettingsTab; + +// For tests +export const mockVisibleCategories = (categories) => { + visibleCategories.splice(0, visibleCategories.length); + if (categories) visibleCategories.push(...categories); +}; diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 754cffb4f33..74da30390cf 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -32,7 +32,7 @@ import SystemFontController from './controllers/SystemFontController'; import UseSystemFontController from './controllers/UseSystemFontController'; import { SettingLevel } from "./SettingLevel"; import SettingController from "./controllers/SettingController"; -import { isMac } from '../Keyboard'; +import { isMac, Key } from '../Keyboard'; import UIFeatureController from "./controllers/UIFeatureController"; import { UIFeature } from "./UIFeature"; import { OrderedMultiController } from "./controllers/OrderedMultiController"; @@ -42,6 +42,7 @@ import IncompatibleController from "./controllers/IncompatibleController"; import { ImageSize } from "./enums/ImageSize"; import { MetaSpace } from "../stores/spaces"; import SdkConfig from "../SdkConfig"; +import { DIGITS, KeyBindingAction } from "../accessibility/KeyboardShortcuts"; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -1013,4 +1014,461 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_UI_FEATURE, default: true, }, + // TODO: TravisR will fix this nightmare when the new version of the SettingsStore becomes a thing + [KeyBindingAction.FormatBold]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.B, + }, + displayName: _td("Toggle Bold"), + }, + [KeyBindingAction.FormatItalics]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.I, + }, + displayName: _td("Toggle Italics"), + }, + [KeyBindingAction.FormatQuote]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.GREATER_THAN, + }, + displayName: _td("Toggle Quote"), + }, + [KeyBindingAction.CancelReplyOrEdit]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ESCAPE, + }, + displayName: _td("Cancel replying to a message"), + }, + [KeyBindingAction.EditNextMessage]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_DOWN, + }, + displayName: _td("Navigate to next message to edit"), + }, + [KeyBindingAction.EditPrevMessage]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_UP, + }, + displayName: _td("Navigate to previous message to edit"), + }, + [KeyBindingAction.MoveCursorToStart]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.HOME, + }, + displayName: _td("Jump to start of the composer"), + }, + [KeyBindingAction.MoveCursorToEnd]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.END, + }, + displayName: _td("Jump to end of the composer"), + }, + [KeyBindingAction.SelectNextSendHistory]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + altKey: true, + ctrlKey: true, + key: Key.ARROW_DOWN, + }, + displayName: _td("Navigate to next message in composer history"), + }, + [KeyBindingAction.SelectPrevSendHistory]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + altKey: true, + ctrlKey: true, + key: Key.ARROW_UP, + }, + displayName: _td("Navigate to previous message in composer history"), + }, + [KeyBindingAction.ShowStickerPicker]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.SEMICOLON, + }, + displayName: _td("Send a sticker"), + }, + [KeyBindingAction.ToggleMicInCall]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.D, + }, + displayName: _td("Toggle microphone mute"), + }, + [KeyBindingAction.ToggleWebcamInCall]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.E, + }, + displayName: _td("Toggle webcam on/off"), + }, + [KeyBindingAction.DismissReadMarker]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ESCAPE, + }, + displayName: _td("Dismiss read marker and jump to bottom"), + }, + [KeyBindingAction.JumpToOldestUnread]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + shiftKey: true, + key: Key.PAGE_UP, + }, + displayName: _td("Jump to oldest unread message"), + }, + [KeyBindingAction.UploadFile]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + shiftKey: true, + key: Key.U, + }, + displayName: _td("Upload a file"), + }, + [KeyBindingAction.ScrollUp]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.PAGE_UP, + }, + displayName: _td("Scroll up in the timeline"), + }, + [KeyBindingAction.ScrollDown]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.PAGE_DOWN, + }, + displayName: _td("Scroll down in the timeline"), + }, + [KeyBindingAction.FilterRooms]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.K, + }, + displayName: _td("Jump to room search"), + }, + [KeyBindingAction.SelectRoomInRoomList]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ENTER, + }, + displayName: _td("Select room from the room list"), + }, + [KeyBindingAction.CollapseRoomListSection]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_LEFT, + }, + displayName: _td("Collapse room list section"), + }, + [KeyBindingAction.ExpandRoomListSection]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_RIGHT, + }, + displayName: _td("Expand room list section"), + }, + [KeyBindingAction.ClearRoomFilter]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ESCAPE, + }, + displayName: _td("Clear room list filter field"), + controller: new IncompatibleController("feature_spotlight", { key: null }), + }, + [KeyBindingAction.NextRoom]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_DOWN, + }, + displayName: _td("Navigate down in the room list"), + }, + [KeyBindingAction.PrevRoom]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_UP, + }, + displayName: _td("Navigate up in the room list"), + }, + [KeyBindingAction.ToggleUserMenu]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.BACKTICK, + }, + displayName: _td("Toggle the top left menu"), + }, + [KeyBindingAction.ToggleRoomSidePanel]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.PERIOD, + }, + displayName: _td("Toggle right panel"), + }, + [KeyBindingAction.ShowKeyboardSettings]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: Key.SLASH, + }, + displayName: _td("Open this settings tab"), + }, + [KeyBindingAction.GoToHome]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + altKey: !isMac, + shiftKey: isMac, + key: Key.H, + }, + displayName: _td("Go to Home View"), + }, + [KeyBindingAction.SelectNextUnreadRoom]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + shiftKey: true, + altKey: true, + key: Key.ARROW_DOWN, + }, + displayName: _td("Next unread room or DM"), + }, + [KeyBindingAction.SelectPrevUnreadRoom]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + shiftKey: true, + altKey: true, + key: Key.ARROW_UP, + }, + displayName: _td("Previous unread room or DM"), + }, + [KeyBindingAction.SelectNextRoom]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + altKey: true, + key: Key.ARROW_DOWN, + }, + displayName: _td("Next room or DM"), + }, + [KeyBindingAction.SelectPrevRoom]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + altKey: true, + key: Key.ARROW_UP, + }, + displayName: _td("Previous room or DM"), + }, + [KeyBindingAction.CancelAutocomplete]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ESCAPE, + }, + displayName: _td("Cancel autocomplete"), + }, + [KeyBindingAction.NextSelectionInAutocomplete]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_DOWN, + }, + displayName: _td("Next autocomplete suggestion"), + }, + [KeyBindingAction.PrevSelectionInAutocomplete]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_UP, + }, + displayName: _td("Previous autocomplete suggestion"), + }, + [KeyBindingAction.ToggleSpacePanel]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + shiftKey: true, + key: Key.D, + }, + displayName: _td("Toggle space panel"), + }, + [KeyBindingAction.ToggleHiddenEventVisibility]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + shiftKey: true, + key: Key.H, + }, + displayName: _td("Toggle hidden event visibility"), + }, + [KeyBindingAction.JumpToFirstMessage]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.HOME, + ctrlKey: true, + }, + displayName: _td("Jump to first message"), + }, + [KeyBindingAction.JumpToLatestMessage]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.END, + ctrlKey: true, + }, + displayName: _td("Jump to last message"), + }, + [KeyBindingAction.EditUndo]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.Z, + ctrlOrCmdKey: true, + }, + displayName: _td("Undo edit"), + }, + [KeyBindingAction.EditRedo]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: isMac ? Key.Z : Key.Y, + ctrlOrCmdKey: true, + shiftKey: isMac, + }, + displayName: _td("Redo edit"), + hideEditUI: true, + }, + [KeyBindingAction.PreviousVisitedRoomOrCommunity]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + metaKey: isMac, + altKey: !isMac, + key: isMac ? Key.SQUARE_BRACKET_LEFT : Key.ARROW_LEFT, + }, + displayName: _td("Previous recently visited room or community"), + hideEditUI: true, + }, + [KeyBindingAction.NextVisitedRoomOrCommunity]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + metaKey: isMac, + altKey: !isMac, + key: isMac ? Key.SQUARE_BRACKET_RIGHT : Key.ARROW_RIGHT, + }, + displayName: _td("Next recently visited room or community"), + hideEditUI: true, + }, + [KeyBindingAction.SwitchToSpaceByNumber]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + ctrlOrCmdKey: true, + key: DIGITS, + }, + displayName: _td("Switch to space by number"), + hideEditUI: true, + }, + [KeyBindingAction.OpenUserSettings]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + metaKey: true, + key: Key.COMMA, + }, + displayName: _td("Open user settings"), + }, + [KeyBindingAction.Escape]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ESCAPE, + }, + displayName: _td("Close dialog or context menu"), + hideEditUI: true, + }, + [KeyBindingAction.Enter]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ENTER, + }, + displayName: _td("Activate selected button"), + hideEditUI: true, + }, + [KeyBindingAction.Space]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.SPACE, + }, + hideEditUI: true, + }, + [KeyBindingAction.Backspace]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.BACKSPACE, + }, + hideEditUI: true, + }, + [KeyBindingAction.Delete]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.DELETE, + }, + hideEditUI: true, + }, + [KeyBindingAction.Home]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.HOME, + }, + hideEditUI: true, + }, + [KeyBindingAction.End]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.END, + }, + hideEditUI: true, + }, + [KeyBindingAction.ArrowLeft]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_LEFT, + }, + hideEditUI: true, + }, + [KeyBindingAction.ArrowUp]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_UP, + }, + hideEditUI: true, + }, + [KeyBindingAction.ArrowRight]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_RIGHT, + }, + hideEditUI: true, + }, + [KeyBindingAction.ArrowDown]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.ARROW_DOWN, + }, + hideEditUI: true, + }, + [KeyBindingAction.Comma]: { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: { + key: Key.COMMA, + }, + hideEditUI: true, + }, }; diff --git a/test/accessibility/KeyboardShortcutUtils-test.ts b/test/accessibility/KeyboardShortcutUtils-test.ts deleted file mode 100644 index 46221985823..00000000000 --- a/test/accessibility/KeyboardShortcutUtils-test.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright 2022 Šimon Brandner - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { - KEYBOARD_SHORTCUTS, - mock, -} from "../../src/accessibility/KeyboardShortcuts"; -import { getKeyboardShortcuts, getKeyboardShortcutsForUI } from "../../src/accessibility/KeyboardShortcutUtils"; -import PlatformPeg from "../../src/PlatformPeg"; - -describe("KeyboardShortcutUtils", () => { - it("doesn't change KEYBOARD_SHORTCUTS when getting shortcuts", async () => { - mock({ - keyboardShortcuts: { - "Keybind1": {}, - "Keybind2": {}, - }, - macOnlyShortcuts: ["Keybind1"], - desktopShortcuts: ["Keybind2"], - }); - PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => false }); - const copyKeyboardShortcuts = Object.assign({}, KEYBOARD_SHORTCUTS); - - getKeyboardShortcuts(); - expect(KEYBOARD_SHORTCUTS).toEqual(copyKeyboardShortcuts); - getKeyboardShortcutsForUI(); - expect(KEYBOARD_SHORTCUTS).toEqual(copyKeyboardShortcuts); - }); - - it("correctly filters shortcuts", async () => { - mock({ - keyboardShortcuts: { - "Keybind1": {}, - "Keybind2": {}, - "Keybind3": { "controller": { settingDisabled: true } }, - "Keybind4": {}, - }, - macOnlyShortcuts: ["Keybind1"], - desktopShortcuts: ["Keybind2"], - - }); - PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => false }); - expect(getKeyboardShortcuts()).toEqual({ "Keybind4": {} }); - - mock({ - keyboardShortcuts: { - "Keybind1": {}, - "Keybind2": {}, - }, - macOnlyShortcuts: undefined, - desktopShortcuts: ["Keybind2"], - }); - PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => true }); - expect(getKeyboardShortcuts()).toEqual({ "Keybind1": {}, "Keybind2": {} }); - jest.resetModules(); - }); -}); diff --git a/test/components/views/settings/KeyboardShortcut-test.tsx b/test/components/views/settings/KeyboardShortcut-test.tsx index a44ab47feed..2e4b144af7f 100644 --- a/test/components/views/settings/KeyboardShortcut-test.tsx +++ b/test/components/views/settings/KeyboardShortcut-test.tsx @@ -20,37 +20,37 @@ import { mount, ReactWrapper } from "enzyme"; import { Key } from "../../../../src/Keyboard"; import PlatformPeg from "../../../../src/PlatformPeg"; +import { KeyboardShortcut, KeyboardKey } from "../../../../src/components/views/settings/KeyboardShortcut"; -const PATH_TO_COMPONENT = "../../../../src/components/views/settings/KeyboardShortcut.tsx"; - -const renderKeyboardShortcut = async (component, props?): Promise => { - const Component = (await import(PATH_TO_COMPONENT))[component]; - return mount(); +const renderKeyboardKey = (props): ReactWrapper => { + return mount(); }; -describe("KeyboardShortcut", () => { - beforeEach(() => { - jest.resetModules(); - }); +const renderKeyboardShortcut = (props): ReactWrapper => { + return mount(); +}; +describe("KeyboardKey", () => { it("renders key icon", async () => { - const body = await renderKeyboardShortcut("KeyboardKey", { name: Key.ARROW_DOWN }); + const body = await renderKeyboardKey({ name: Key.ARROW_DOWN }); expect(body).toMatchSnapshot(); }); it("renders alternative key name", async () => { - const body = await renderKeyboardShortcut("KeyboardKey", { name: Key.PAGE_DOWN }); + const body = await renderKeyboardKey({ name: Key.PAGE_DOWN }); expect(body).toMatchSnapshot(); }); it("doesn't render + if last", async () => { - const body = await renderKeyboardShortcut("KeyboardKey", { name: Key.A, last: true }); + const body = await renderKeyboardKey({ name: Key.A, last: true }); expect(body).toMatchSnapshot(); }); +}); - it("doesn't render same modifier twice", async () => { +describe("KeyboardShortcut", () => { + it("doesn't render same modifier twice (meta)", async () => { PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => false }); - const body1 = await renderKeyboardShortcut("KeyboardShortcut", { + const body1 = renderKeyboardShortcut({ value: { key: Key.A, ctrlOrCmdKey: true, @@ -58,8 +58,10 @@ describe("KeyboardShortcut", () => { }, }); expect(body1).toMatchSnapshot(); + }); - const body2 = await renderKeyboardShortcut("KeyboardShortcut", { + it("doesn't render same modifier twice (ctrl)", () => { + const body2 = renderKeyboardShortcut({ value: { key: Key.A, ctrlOrCmdKey: true, @@ -67,6 +69,5 @@ describe("KeyboardShortcut", () => { }, }); expect(body2).toMatchSnapshot(); - jest.resetModules(); }); }); diff --git a/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap b/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap index e4468c802f0..80b69ac7512 100644 --- a/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`KeyboardShortcut doesn't render + if last 1`] = ` +exports[`KeyboardKey doesn't render + if last 1`] = ` `; -exports[`KeyboardShortcut doesn't render same modifier twice 1`] = ` +exports[`KeyboardKey renders alternative key name 1`] = ` + + + + Page Down + + + + + +`; + +exports[`KeyboardKey renders key icon 1`] = ` + + + + ↓ + + + + + +`; + +exports[`KeyboardShortcut doesn't render same modifier twice (ctrl) 1`] = ` @@ -32,7 +58,7 @@ exports[`KeyboardShortcut doesn't render same modifier twice 1`] = ` > - missing translation: en|Ctrl + Ctrl + @@ -51,26 +77,24 @@ exports[`KeyboardShortcut doesn't render same modifier twice 1`] = ` `; -exports[`KeyboardShortcut doesn't render same modifier twice 2`] = ` +exports[`KeyboardShortcut doesn't render same modifier twice (meta) 1`] = ` -
+
- missing translation: en|Ctrl + Ctrl + @@ -88,29 +112,3 @@ exports[`KeyboardShortcut doesn't render same modifier twice 2`] = `
`; - -exports[`KeyboardShortcut renders alternative key name 1`] = ` - - - - missing translation: en|Page Down - - - + - -`; - -exports[`KeyboardShortcut renders key icon 1`] = ` - - - - ↓ - - - + - -`; diff --git a/test/components/views/settings/tabs/user/KeyboardUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/KeyboardUserSettingsTab-test.tsx index 3838ce26656..5a0ce8291cf 100644 --- a/test/components/views/settings/tabs/user/KeyboardUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/KeyboardUserSettingsTab-test.tsx @@ -19,88 +19,63 @@ import React from "react"; import { mount, ReactWrapper } from "enzyme"; import { Key } from "../../../../../../src/Keyboard"; +import KeyboardUserSettingsTab, { + mockVisibleCategories, +} from "../../../../../../src/components/views/settings/tabs/user/KeyboardUserSettingsTab"; +import PlatformPeg from "../../../../../../src/PlatformPeg"; +import SettingsStore from "../../../../../../src/settings/SettingsStore"; -const PATH_TO_KEYBOARD_SHORTCUTS = "../../../../../../src/accessibility/KeyboardShortcuts"; -const PATH_TO_KEYBOARD_SHORTCUT_UTILS = "../../../../../../src/accessibility/KeyboardShortcutUtils"; -const PATH_TO_COMPONENT = "../../../../../../src/components/views/settings/tabs/user/KeyboardUserSettingsTab"; - -const mockKeyboardShortcuts = (override) => { - jest.doMock(PATH_TO_KEYBOARD_SHORTCUTS, () => { - const original = jest.requireActual(PATH_TO_KEYBOARD_SHORTCUTS); - return { - ...original, - ...override, - }; - }); -}; - -const mockKeyboardShortcutUtils = (override) => { - jest.doMock(PATH_TO_KEYBOARD_SHORTCUT_UTILS, () => { - const original = jest.requireActual(PATH_TO_KEYBOARD_SHORTCUT_UTILS); - return { - ...original, - ...override, - }; - }); -}; - -const renderKeyboardUserSettingsTab = async (component): Promise => { - const Component = (await import(PATH_TO_COMPONENT))[component]; - return mount(); +const renderKeyboardUserSettingsTab = (): ReactWrapper => { + return mount(); }; describe("KeyboardUserSettingsTab", () => { - beforeEach(() => { - jest.resetModules(); - }); - it("renders list of keyboard shortcuts", async () => { - mockKeyboardShortcuts({ - "CATEGORIES": { - "Composer": { - settingNames: ["keybind1", "keybind2"], - categoryLabel: "Composer", - }, - "Navigation": { - settingNames: ["keybind3"], - categoryLabel: "Navigation", - }, - }, - }); - mockKeyboardShortcutUtils({ - "getKeyboardShortcutValue": (name) => { - switch (name) { - case "keybind1": - return { - key: Key.A, - ctrlKey: true, - }; - case "keybind2": { - return { - key: Key.B, - ctrlKey: true }; - } - case "keybind3": { - return { - key: Key.ENTER, - }; - } + PlatformPeg.get = () => ({ overrideBrowserShortcuts: () => false }); + mockVisibleCategories([ + ["Composer", { + settingNames: ["keybind1", "keybind2"], + categoryLabel: "Composer", + }], + ["Navigation", { + settingNames: ["keybind3"], + categoryLabel: "Navigation", + }], + ]); + SettingsStore.getValue = jest.fn().mockImplementation((name) => { + switch (name) { + case "feature_customizable_keybindings": + return true; + case "keybind1": + return { + key: Key.A, + ctrlKey: true, + }; + case "keybind2": { + return { + key: Key.B, + ctrlKey: true }; } - }, - "getKeyboardShortcutDisplayName": (name) => { - switch (name) { - case "keybind1": - return "Cancel replying to a message"; - case "keybind2": - return "Toggle Bold"; - - case "keybind3": - return "Select room from the room list"; + case "keybind3": { + return { + key: Key.ENTER, + }; } - }, + } + }); + SettingsStore.getDisplayName = jest.fn().mockImplementation((name) => { + switch (name) { + case "keybind1": + return "Cancel replying to a message"; + case "keybind2": + return "Toggle Bold"; + case "keybind3": + return "Select room from the room list"; + } }); + SettingsStore.isEnabled = jest.fn().mockReturnValue(true); - const body = await renderKeyboardUserSettingsTab("default"); + const body = await renderKeyboardUserSettingsTab(); expect(body).toMatchSnapshot(); }); }); diff --git a/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap index caeea350f7b..71bab7f6129 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap @@ -8,7 +8,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
- missing translation: en|Keyboard + Keyboard
- missing translation: en|Composer + Composer
@@ -59,7 +59,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = ` > - missing translation: en|Ctrl + Ctrl + @@ -103,7 +103,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = ` > - missing translation: en|Ctrl + Ctrl + @@ -145,7 +145,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
- missing translation: en|Navigation + Navigation
@@ -173,7 +173,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = ` > - missing translation: en|Enter + Enter From 1f648dc98c494bd239876cfe2452b3c25cf20928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 12 Mar 2022 15:05:54 +0100 Subject: [PATCH 7/7] Rest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/user/_KeyboardUserSettingsTab.scss | 11 ++- .../user/ChangeKeyboardShortcutDialog.tsx | 80 +++++++++++++++++++ .../tabs/user/KeyboardUserSettingsTab.tsx | 56 +++++++++++-- 3 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 src/components/views/settings/tabs/user/ChangeKeyboardShortcutDialog.tsx diff --git a/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.scss b/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.scss index 1ba3a8599b8..c7251826750 100644 --- a/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.scss @@ -18,7 +18,16 @@ limitations under the License. .mx_KeyboardUserSettingsTab .mx_SettingsTab_section { .mx_KeyboardShortcut_shortcutRow { display: flex; - justify-content: space-between; align-items: center; + + .mx_KeyboardShortcut_shortcutRow_displayName { + margin-right: auto; + } + + .mx_AccessibleButton { + margin: 0 4px; + } + + margin: 4px 0; } } diff --git a/src/components/views/settings/tabs/user/ChangeKeyboardShortcutDialog.tsx b/src/components/views/settings/tabs/user/ChangeKeyboardShortcutDialog.tsx new file mode 100644 index 00000000000..2c22c1bce97 --- /dev/null +++ b/src/components/views/settings/tabs/user/ChangeKeyboardShortcutDialog.tsx @@ -0,0 +1,80 @@ +/* +Copyright 2022 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { KeyboardEvent, useState } from "react"; + +import { KeyCombo } from "../../../../../KeyBindingsManager"; +import { Key } from "../../../../../Keyboard"; +import { _t } from "../../../../../languageHandler"; +import BaseDialog from "../../../dialogs/BaseDialog"; +import { IDialogProps } from "../../../dialogs/IDialogProps"; +import DialogButtons from "../../../elements/DialogButtons"; +import KeyboardShortcut from "../../KeyboardShortcut"; + +const eventIntoKeyCombo = (event: KeyboardEvent): KeyCombo | null => { + const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; + const isKeyAModifier = [Key.ALT, Key.CONTROL, Key.META, Key.SHIFT].includes(event.key); + // Don't allow KeyCombos without modifiers + if (!hasModifier) return null; + // Don't allow KeyCombos without a key pressed + if (isKeyAModifier || !event.key) return null; + + return { + key: event.key, + altKey: event.altKey, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + shiftKey: event.shiftKey, + }; +}; + +export interface IProps extends IDialogProps { + value: KeyCombo; +} + +export const ChangeKeyboardShortcutDialog: React.FC = ({ onFinished, value }) => { + const [currentValue, setValue] = useState(value); + + const onDialogFinished = () => { + onFinished(currentValue); + }; + + const onKeyDown = (event: KeyboardEvent): void => { + event.preventDefault(); + event.stopPropagation(); + + const key = event.key; + if (!key) return; + + setValue(eventIntoKeyCombo(event)); + }; + + const onCancel = (): void => { + onFinished(null); + }; + + const onPrimaryButtonClick = (): void => { + if (!currentValue) return; + onFinished(currentValue); + }; + + return + + + ; +}; + +export default ChangeKeyboardShortcutDialog; diff --git a/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx b/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx index 0327e07ccc8..aaadac2b21d 100644 --- a/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx @@ -25,41 +25,80 @@ import { import SdkConfig from "../../../../../SdkConfig"; import { _t } from "../../../../../languageHandler"; import { - getKeyboardShortcutDisplayName, getKeyboardShortcutValue, + getKeyboardShortcutDisplayName, + getKeyboardShortcutHideEditUI, + getKeyboardShortcutValue, } from "../../../../../accessibility/KeyboardShortcutUtils"; import { KeyboardShortcut } from "../../KeyboardShortcut"; +import AccessibleButton from "../../../elements/AccessibleButton"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import { SettingLevel } from "../../../../../settings/SettingLevel"; +import Modal from "../../../../../Modal"; +import { + ChangeKeyboardShortcutDialog, + IProps as IChangeKeyboardShortcutDialogProps, +} from "./ChangeKeyboardShortcutDialog"; interface IKeyboardShortcutRowProps { name: string; + allowCustomization: boolean; } // Filter out the labs section if labs aren't enabled. const visibleCategories = Object.entries(CATEGORIES).filter(([categoryName]) => categoryName !== CategoryName.LABS || SdkConfig.get("show_labs_settings")); -const KeyboardShortcutRow: React.FC = ({ name }) => { +const KeyboardShortcutRow: React.FC = ({ name, allowCustomization }) => { const displayName = getKeyboardShortcutDisplayName(name); + const hideEditUI = getKeyboardShortcutHideEditUI(name); const value = getKeyboardShortcutValue(name); if (!displayName || !value) return null; + const onEditClick = async (): Promise => { + const { finished } = Modal.createDialog(ChangeKeyboardShortcutDialog, { + value, + } as IChangeKeyboardShortcutDialogProps); + const [newValue] = await finished; + if (!newValue) return; + + //SettingsStore.setValue(name, null, SettingLevel.DEVICE, newValue); + }; + + const onResetClick = (): void => { + SettingsStore.setValue(name, null, SettingLevel.DEVICE, SettingsStore.getDefaultValue(name)); + }; + return
- { displayName } +
+ { displayName } +
+ { allowCustomization && + { _t("Edit") } + { _t("Reset") } + }
; }; interface IKeyboardShortcutSectionProps { categoryName: CategoryName; category: ICategory; + allowCustomization: boolean; } -const KeyboardShortcutSection: React.FC = ({ categoryName, category }) => { +const KeyboardShortcutSection: React.FC = ( + { categoryName, category, allowCustomization }, +) => { if (!category.categoryLabel) return null; return
{ _t(category.categoryLabel) }
{ category.settingNames.map((shortcutName) => { - return ; + return ; }) }
; }; @@ -68,7 +107,12 @@ const KeyboardUserSettingsTab: React.FC = () => { return
{ _t("Keyboard") }
{ visibleCategories.map(([categoryName, category]: [CategoryName, ICategory]) => { - return ; + return ; }) }
; };