Improve the look of the keyboard settings tab (#7562)

* First cut of new keyboard shortcuts

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Remove unused code

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* i18n

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Amend shortcuts

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Improve CATEGORIES struct

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add tests for registerShortcut()

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Simplifie code tiny bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Translate ALTERNATE_KEY_NAME

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix `key` usage

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Export components for tests

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Write snapshot tests

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
pull/21833/head
Šimon Brandner 2022-01-24 12:33:27 +01:00 committed by GitHub
parent 55ec1bdc85
commit 5f18e4888c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 932 additions and 383 deletions

View File

@ -16,30 +16,10 @@ limitations under the License.
*/
.mx_KeyboardUserSettingsTab .mx_SettingsTab_section {
display: flex;
flex-wrap: wrap;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
flex-direction: column;
margin-bottom: -50px;
max-height: 1100px; // XXX: this may need adjusting when adding new shortcuts
.mx_KeyboardShortcutsDialog_category {
width: 33.3333%; // 3 columns
margin: 0 0 40px;
& > div {
padding-left: 5px;
}
}
h3 {
margin: 0 0 10px;
}
h5 {
margin: 15px 0 5px;
font-weight: normal;
.mx_KeyboardShortcut_shortcutRow {
display: flex;
justify-content: space-between;
align-items: center;
}
kbd {
@ -59,8 +39,4 @@ limitations under the License.
margin-left: 5px;
}
}
.mx_KeyboardShortcutsDialog_inline div {
display: inline;
}
}

View File

@ -1,5 +1,6 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2021 - 2022 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,8 +17,14 @@ limitations under the License.
import { _td } from "../languageHandler";
import { isMac, Key } from "../Keyboard";
import { ISetting } from "../settings/Settings";
export enum Categories {
export interface ICategory {
categoryLabel: string;
settingNames: string[];
}
export enum CategoryName {
NAVIGATION = "Navigation",
CALLS = "Calls",
COMPOSER = "Composer",
@ -26,258 +33,378 @@ export enum Categories {
AUTOCOMPLETE = "Autocomplete",
}
export enum Modifiers {
ALT = "Alt", // Option on Mac and displayed as an Icon
ALT_GR = "Alt Gr",
SHIFT = "Shift",
SUPER = "Super", // should this be "Windows"?
// Instead of using below, consider CMD_OR_CTRL
COMMAND = "Command", // This gets displayed as an Icon
CONTROL = "Ctrl",
}
// Meta-modifier: isMac ? CMD : CONTROL
export const CMD_OR_CTRL = isMac ? Modifiers.COMMAND : Modifiers.CONTROL;
// Meta-key representing the digits [0-9] often found at the top of standard keyboard layouts
export const DIGITS = "digits";
interface IKeybind {
modifiers?: Modifiers[];
key: string; // TS: fix this once Key is an enum
export const ALTERNATE_KEY_NAME: Record<string, string> = {
[Key.PAGE_UP]: _td("Page Up"),
[Key.PAGE_DOWN]: _td("Page Down"),
[Key.ESCAPE]: _td("Esc"),
[Key.ENTER]: _td("Enter"),
[Key.SPACE]: _td("Space"),
[Key.HOME]: _td("Home"),
[Key.END]: _td("End"),
[Key.ALT]: _td("Alt"),
[Key.CONTROL]: _td("Ctrl"),
[Key.SHIFT]: _td("Shift"),
[DIGITS]: _td("[number]"),
};
export const KEY_ICON: Record<string, string> = {
[Key.ARROW_UP]: "↑",
[Key.ARROW_DOWN]: "↓",
[Key.ARROW_LEFT]: "←",
[Key.ARROW_RIGHT]: "→",
};
if (isMac) {
KEY_ICON[Key.META] = "⌘";
KEY_ICON[Key.SHIFT] = "⌥";
}
export interface IShortcut {
keybinds: IKeybind[];
description: string;
}
export const shortcuts: Record<Categories, IShortcut[]> = {
[Categories.COMPOSER]: [
{
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.B,
}],
description: _td("Toggle Bold"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.I,
}],
description: _td("Toggle Italics"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.GREATER_THAN,
}],
description: _td("Toggle Quote"),
}, {
keybinds: [{
modifiers: [Modifiers.SHIFT],
key: Key.ENTER,
}],
description: _td("New line"),
}, {
keybinds: [{
key: Key.ARROW_UP,
}, {
key: Key.ARROW_DOWN,
}],
description: _td("Navigate recent messages to edit"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.HOME,
}, {
modifiers: [CMD_OR_CTRL],
key: Key.END,
}],
description: _td("Jump to start/end of the composer"),
}, {
keybinds: [{
modifiers: [Modifiers.CONTROL, Modifiers.ALT],
key: Key.ARROW_UP,
}, {
modifiers: [Modifiers.CONTROL, Modifiers.ALT],
key: Key.ARROW_DOWN,
}],
description: _td("Navigate composer history"),
}, {
keybinds: [{
key: Key.ESCAPE,
}],
description: _td("Cancel replying to a message"),
},
],
[Categories.CALLS]: [
{
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.D,
}],
description: _td("Toggle microphone mute"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.E,
}],
description: _td("Toggle video on/off"),
},
],
[Categories.ROOM]: [
{
keybinds: [{
key: Key.PAGE_UP,
}, {
key: Key.PAGE_DOWN,
}],
description: _td("Scroll up/down in the timeline"),
}, {
keybinds: [{
key: Key.ESCAPE,
}],
description: _td("Dismiss read marker and jump to bottom"),
}, {
keybinds: [{
modifiers: [Modifiers.SHIFT],
key: Key.PAGE_UP,
}],
description: _td("Jump to oldest unread message"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL, Modifiers.SHIFT],
key: Key.U,
}],
description: _td("Upload a file"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.F,
}],
description: _td("Search (must be enabled)"),
},
],
[Categories.ROOM_LIST]: [
{
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.K,
}],
description: _td("Jump to room search"),
}, {
keybinds: [{
key: Key.ARROW_UP,
}, {
key: Key.ARROW_DOWN,
}],
description: _td("Navigate up/down in the room list"),
}, {
keybinds: [{
key: Key.ENTER,
}],
description: _td("Select room from the room list"),
}, {
keybinds: [{
key: Key.ARROW_LEFT,
}],
description: _td("Collapse room list section"),
}, {
keybinds: [{
key: Key.ARROW_RIGHT,
}],
description: _td("Expand room list section"),
}, {
keybinds: [{
key: Key.ESCAPE,
}],
description: _td("Clear room list filter field"),
},
],
[Categories.NAVIGATION]: [
{
keybinds: [{
modifiers: [Modifiers.ALT, Modifiers.SHIFT],
key: Key.ARROW_UP,
}, {
modifiers: [Modifiers.ALT, Modifiers.SHIFT],
key: Key.ARROW_DOWN,
}],
description: _td("Previous/next unread room or DM"),
}, {
keybinds: [{
modifiers: [Modifiers.ALT],
key: Key.ARROW_UP,
}, {
modifiers: [Modifiers.ALT],
key: Key.ARROW_DOWN,
}],
description: _td("Previous/next room or DM"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.BACKTICK,
}],
description: _td("Toggle the top left menu"),
}, {
keybinds: [{
key: Key.ESCAPE,
}],
description: _td("Close dialog or context menu"),
}, {
keybinds: [{
key: Key.ENTER,
}, {
key: Key.SPACE,
}],
description: _td("Activate selected button"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL, Modifiers.SHIFT],
key: Key.D,
}],
description: _td("Toggle space panel"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.PERIOD,
}],
description: _td("Toggle right panel"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.SLASH,
}],
description: _td("Open this settings tab"),
}, {
keybinds: [{
modifiers: [Modifiers.CONTROL, isMac ? Modifiers.SHIFT : Modifiers.ALT],
key: Key.H,
}],
description: _td("Go to Home View"),
},
],
[Categories.AUTOCOMPLETE]: [
{
keybinds: [{
key: Key.ARROW_UP,
}, {
key: Key.ARROW_DOWN,
}],
description: _td("Move autocomplete selection up/down"),
}, {
keybinds: [{
key: Key.ESCAPE,
}],
description: _td("Cancel autocomplete"),
},
],
export const CATEGORIES: Record<CategoryName, ICategory> = {
[CategoryName.COMPOSER]: {
categoryLabel: _td("Composer"),
settingNames: [
"KeyBinding.toggleBoldInComposer",
"KeyBinding.toggleItalicsInComposer",
"KeyBinding.toggleQuoteInComposer",
"KeyBinding.newLineInComposer",
"KeyBinding.cancelReplyInComposer",
"KeyBinding.editNextMessage",
"KeyBinding.editPreviousMessage",
"KeyBinding.jumpToStartInComposer",
"KeyBinding.jumpToEndInComposer",
"KeyBinding.nextMessageInComposerHistory",
"KeyBinding.previousMessageInComposerHistory",
],
}, [CategoryName.CALLS]: {
categoryLabel: _td("Calls"),
settingNames: [
"KeyBinding.toggleMicInCall",
"KeyBinding.toggleWebcamInCall",
],
}, [CategoryName.ROOM]: {
categoryLabel: _td("Room"),
settingNames: [
"KeyBinding.dismissReadMarkerAndJumpToBottom",
"KeyBinding.jumpToOldestUnreadMessage",
"KeyBinding.uploadFileToRoom",
"KeyBinding.searchInRoom",
"KeyBinding.scrollUpInTimeline",
"KeyBinding.scrollDownInTimeline",
],
}, [CategoryName.ROOM_LIST]: {
categoryLabel: _td("Room List"),
settingNames: [
"KeyBinding.filterRooms",
"KeyBinding.selectRoomInRoomList",
"KeyBinding.collapseSectionInRoomList",
"KeyBinding.expandSectionInRoomList",
"KeyBinding.clearRoomFilter",
"KeyBinding.upperRoom",
"KeyBinding.downerRoom",
],
}, [CategoryName.NAVIGATION]: {
categoryLabel: _td("Navigation"),
settingNames: [
"KeyBinding.toggleTopLeftMenu",
"KeyBinding.closeDialogOrContextMenu",
"KeyBinding.activateSelectedButton",
"KeyBinding.toggleRightPanel",
"KeyBinding.showKeyBindingsSettings",
"KeyBinding.goToHomeView",
"KeyBinding.nextUnreadRoom",
"KeyBinding.previousUnreadRoom",
"KeyBinding.nextRoom",
"KeyBinding.previousRoom",
"KeyBinding.toggleSpacePanel",
],
}, [CategoryName.AUTOCOMPLETE]: {
categoryLabel: _td("Autocomplete"),
settingNames: [
"KeyBinding.cancelAutoComplete",
"KeyBinding.nextOptionInAutoComplete",
"KeyBinding.previousOptionInAutoComplete",
],
},
};
export const registerShortcut = (category: Categories, defn: IShortcut) => {
shortcuts[category].push(defn);
// 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
export const KEYBOARD_SHORTCUTS: { [setting: string]: ISetting } = {
"KeyBinding.toggleBoldInComposer": {
default: {
ctrlOrCmdKey: true,
key: Key.B,
},
displayName: _td("Toggle Bold"),
},
"KeyBinding.toggleItalicsInComposer": {
default: {
ctrlOrCmdKey: true,
key: Key.I,
},
displayName: _td("Toggle Italics"),
},
"KeyBinding.toggleQuoteInComposer": {
default: {
ctrlOrCmdKey: true,
key: Key.GREATER_THAN,
},
displayName: _td("Toggle Quote"),
},
"KeyBinding.newLineInComposer": {
default: {
shiftKey: true,
key: Key.ENTER,
},
displayName: _td("New line"),
},
"KeyBinding.cancelReplyInComposer": {
default: {
key: Key.ESCAPE,
},
displayName: _td("Cancel replying to a message"),
},
"KeyBinding.editNextMessage": {
default: {
key: Key.ARROW_UP,
},
displayName: _td("Navigate to next message to edit"),
},
"KeyBinding.editPreviousMessage": {
default: {
key: Key.ARROW_DOWN,
},
displayName: _td("Navigate to previous message to edit"),
},
"KeyBinding.jumpToStartInComposer": {
default: {
ctrlOrCmdKey: true,
key: Key.HOME,
},
displayName: _td("Jump to start of the composer"),
},
"KeyBinding.jumpToEndInComposer": {
default: {
ctrlOrCmdKey: true,
key: Key.END,
},
displayName: _td("Jump to end of the composer"),
},
"KeyBinding.nextMessageInComposerHistory": {
default: {
altKey: true,
ctrlKey: true,
key: Key.ARROW_UP,
},
displayName: _td("Navigate to next message in composer history"),
},
"KeyBinding.previousMessageInComposerHistory": {
default: {
altKey: true,
ctrlKey: true,
key: Key.ARROW_DOWN,
},
displayName: _td("Navigate to previous message in composer history"),
},
"KeyBinding.toggleMicInCall": {
default: {
ctrlOrCmdKey: true,
key: Key.D,
},
displayName: _td("Toggle microphone mute"),
},
"KeyBinding.toggleWebcamInCall": {
default: {
ctrlOrCmdKey: true,
key: Key.E,
},
displayName: _td("Toggle webcam on/off"),
},
"KeyBinding.dismissReadMarkerAndJumpToBottom": {
default: {
key: Key.ESCAPE,
},
displayName: _td("Dismiss read marker and jump to bottom"),
},
"KeyBinding.jumpToOldestUnreadMessage": {
default: {
shiftKey: true,
key: Key.PAGE_UP,
},
displayName: _td("Jump to oldest unread message"),
},
"KeyBinding.uploadFileToRoom": {
default: {
ctrlOrCmdKey: true,
shiftKey: true,
key: Key.U,
},
displayName: _td("Upload a file"),
},
"KeyBinding.searchInRoom": {
default: {
ctrlOrCmdKey: true,
key: Key.F,
},
displayName: _td("Search (must be enabled)"),
},
"KeyBinding.scrollUpInTimeline": {
default: {
key: Key.PAGE_UP,
},
displayName: _td("Scroll up in the timeline"),
},
"KeyBinding.scrollDownInTimeline": {
default: {
key: Key.PAGE_DOWN,
},
displayName: _td("Scroll down in the timeline"),
},
"KeyBinding.filterRooms": {
default: {
ctrlOrCmdKey: true,
key: Key.K,
},
displayName: _td("Jump to room search"),
},
"KeyBinding.selectRoomInRoomList": {
default: {
key: Key.ENTER,
},
displayName: _td("Select room from the room list"),
},
"KeyBinding.collapseSectionInRoomList": {
default: {
key: Key.ARROW_LEFT,
},
displayName: _td("Collapse room list section"),
},
"KeyBinding.expandSectionInRoomList": {
default: {
key: Key.ARROW_RIGHT,
},
displayName: _td("Expand room list section"),
},
"KeyBinding.clearRoomFilter": {
default: {
key: Key.ESCAPE,
},
displayName: _td("Clear room list filter field"),
},
"KeyBinding.upperRoom": {
default: {
key: Key.ARROW_UP,
},
displayName: _td("Navigate up in the room list"),
},
"KeyBinding.downerRoom": {
default: {
key: Key.ARROW_DOWN,
},
displayName: _td("Navigate down in the room list"),
},
"KeyBinding.toggleTopLeftMenu": {
default: {
ctrlOrCmdKey: true,
key: Key.BACKTICK,
},
displayName: _td("Toggle the top left menu"),
},
"KeyBinding.closeDialogOrContextMenu": {
default: {
key: Key.ESCAPE,
},
displayName: _td("Close dialog or context menu"),
},
"KeyBinding.activateSelectedButton": {
default: {
key: Key.ENTER,
},
displayName: _td("Activate selected button"),
},
"KeyBinding.toggleRightPanel": {
default: {
ctrlOrCmdKey: true,
key: Key.PERIOD,
},
displayName: _td("Toggle right panel"),
},
"KeyBinding.showKeyBindingsSettings": {
default: {
ctrlOrCmdKey: true,
key: Key.SLASH,
},
displayName: _td("Open this settings tab"),
},
"KeyBinding.goToHomeView": {
default: {
ctrlOrCmdKey: true,
altKey: true,
key: Key.H,
},
displayName: _td("Go to Home View"),
},
"KeyBinding.nextUnreadRoom": {
default: {
shiftKey: true,
altKey: true,
key: Key.ARROW_UP,
},
displayName: _td("Next unread room or DM"),
},
"KeyBinding.previousUnreadRoom": {
default: {
shiftKey: true,
altKey: true,
key: Key.ARROW_DOWN,
},
displayName: _td("Previous unread room or DM"),
},
"KeyBinding.nextRoom": {
default: {
altKey: true,
key: Key.ARROW_UP,
},
displayName: _td("Next room or DM"),
},
"KeyBinding.previousRoom": {
default: {
altKey: true,
key: Key.ARROW_DOWN,
},
displayName: _td("Previous room or DM"),
},
"KeyBinding.cancelAutoComplete": {
default: {
key: Key.ESCAPE,
},
displayName: _td("Cancel autocomplete"),
},
"KeyBinding.nextOptionInAutoComplete": {
default: {
key: Key.ARROW_UP,
},
displayName: _td("Next autocomplete suggestion"),
},
"KeyBinding.previousOptionInAutoComplete": {
default: {
key: Key.ARROW_DOWN,
},
displayName: _td("Previous autocomplete suggestion"),
},
"KeyBinding.toggleSpacePanel": {
default: {
ctrlOrCmdKey: true,
shiftKey: true,
key: Key.D,
},
displayName: _td("Toggle space panel"),
},
};
export const registerShortcut = (shortcutName: string, categoryName: CategoryName, shortcut: ISetting): void => {
KEYBOARD_SHORTCUTS[shortcutName] = shortcut;
CATEGORIES[categoryName].settingNames.push(shortcutName);
};

View File

@ -1,6 +1,6 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Copyright 2021 - 2022 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,102 +15,94 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import classNames from "classnames";
import React from "react";
import { Categories, DIGITS, IShortcut, Modifiers, shortcuts } from "../../../../../accessibility/KeyboardShortcuts";
import {
KEYBOARD_SHORTCUTS,
ALTERNATE_KEY_NAME,
KEY_ICON,
ICategory,
CATEGORIES,
CategoryName,
} from "../../../../../accessibility/KeyboardShortcuts";
import { isMac, Key } from "../../../../../Keyboard";
import { _t, _td } from "../../../../../languageHandler";
import { _t } from "../../../../../languageHandler";
// TS: once languageHandler is TS we can probably inline this into the enum
_td("Alt");
_td("Alt Gr");
_td("Shift");
_td("Super");
_td("Ctrl");
_td("Navigation");
_td("Calls");
_td("Composer");
_td("Room List");
_td("Autocomplete");
const categoryOrder = [
Categories.COMPOSER,
Categories.AUTOCOMPLETE,
Categories.ROOM,
Categories.ROOM_LIST,
Categories.NAVIGATION,
Categories.CALLS,
];
const modifierIcon: Record<string, string> = {
[Modifiers.COMMAND]: "⌘",
};
if (isMac) {
modifierIcon[Modifiers.ALT] = "⌥";
interface IKeyboardKeyProps {
name: string;
last?: boolean;
}
const alternateKeyName: Record<string, string> = {
[Key.PAGE_UP]: _td("Page Up"),
[Key.PAGE_DOWN]: _td("Page Down"),
[Key.ESCAPE]: _td("Esc"),
[Key.ENTER]: _td("Enter"),
[Key.SPACE]: _td("Space"),
[Key.HOME]: _td("Home"),
[Key.END]: _td("End"),
[DIGITS]: _td("[number]"),
};
const keyIcon: Record<string, string> = {
[Key.ARROW_UP]: "↑",
[Key.ARROW_DOWN]: "↓",
[Key.ARROW_LEFT]: "←",
[Key.ARROW_RIGHT]: "→",
export const KeyboardKey: React.FC<IKeyboardKeyProps> = ({ name, last }) => {
const icon = KEY_ICON[name];
const alternateName = ALTERNATE_KEY_NAME[name];
return <React.Fragment>
<kbd> { icon || (alternateName && _t(alternateName)) || name } </kbd>
{ !last && "+" }
</React.Fragment>;
};
interface IShortcutProps {
shortcut: IShortcut;
interface IKeyboardShortcutProps {
name: string;
}
const Shortcut: React.FC<IShortcutProps> = ({ shortcut }) => {
const classes = classNames({
"mx_KeyboardShortcutsDialog_inline": shortcut.keybinds.every(k => !k.modifiers || k.modifiers.length === 0),
});
export const KeyboardShortcut: React.FC<IKeyboardShortcutProps> = ({ name }) => {
const value = KEYBOARD_SHORTCUTS[name]?.default;
if (!value) return null;
return <div className={classes}>
<h5>{ _t(shortcut.description) }</h5>
{ shortcut.keybinds.map(s => {
let text = s.key;
if (alternateKeyName[s.key]) {
text = _t(alternateKeyName[s.key]);
} else if (keyIcon[s.key]) {
text = keyIcon[s.key];
}
const modifiersElement = [];
if (value.ctrlOrCmdKey) {
modifiersElement.push(<KeyboardKey key="ctrlOrCmdKey" name={isMac ? Key.META : Key.CONTROL} />);
} else if (value.ctrlKey) {
modifiersElement.push(<KeyboardKey key="ctrlKey" name={Key.CONTROL} />);
} else if (value.metaKey) {
modifiersElement.push(<KeyboardKey key="metaKey" name={Key.META} />);
}
if (value.altKey) {
modifiersElement.push(<KeyboardKey key="altKey" name={Key.ALT} />);
}
if (value.shiftKey) {
modifiersElement.push(<KeyboardKey key="shiftKey" name={Key.SHIFT} />);
}
return <div key={s.key}>
{ s.modifiers && s.modifiers.map(m => {
return <React.Fragment key={m}>
<kbd>{ modifierIcon[m] || _t(m) }</kbd>+
</React.Fragment>;
}) }
<kbd>{ text }</kbd>
</div>;
}) }
return <div>
{ modifiersElement }
<KeyboardKey name={value.key} last />
</div>;
};
interface IKeyboardShortcutRowProps {
name: string;
}
const KeyboardShortcutRow: React.FC<IKeyboardShortcutRowProps> = ({ name }) => {
return <div className="mx_KeyboardShortcut_shortcutRow">
{ KEYBOARD_SHORTCUTS[name].displayName }
<KeyboardShortcut name={name} />
</div>;
};
interface IKeyboardShortcutSectionProps {
categoryName: CategoryName;
category: ICategory;
}
const KeyboardShortcutSection: React.FC<IKeyboardShortcutSectionProps> = ({ categoryName, category }) => {
return <div className="mx_SettingsTab_section" key={categoryName}>
<div className="mx_SettingsTab_subheading">{ _t(category.categoryLabel) }</div>
<div> { category.settingNames.map((shortcutName) => {
return <KeyboardShortcutRow key={shortcutName} name={shortcutName} />;
}) } </div>
</div>;
};
const KeyboardUserSettingsTab: React.FC = () => {
return <div className="mx_SettingsTab mx_KeyboardUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Keyboard") }</div>
<div className="mx_SettingsTab_section">
{ categoryOrder.map(category => {
const list = shortcuts[category];
return <div className="mx_KeyboardShortcutsDialog_category" key={category}>
<h3>{ _t(category) }</h3>
<div>{ list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />) }</div>
</div>;
}) }
</div>
{ Object.entries(CATEGORIES).map(([categoryName, category]: [CategoryName, ICategory]) => {
return <KeyboardShortcutSection key={categoryName} categoryName={categoryName} category={category} />;
}) }
</div>;
};

View File

@ -1431,23 +1431,6 @@
"Access Token": "Access Token",
"Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.",
"Clear cache and reload": "Clear cache and reload",
"Alt": "Alt",
"Alt Gr": "Alt Gr",
"Shift": "Shift",
"Super": "Super",
"Ctrl": "Ctrl",
"Navigation": "Navigation",
"Calls": "Calls",
"Composer": "Composer",
"Room List": "Room List",
"Autocomplete": "Autocomplete",
"Page Up": "Page Up",
"Page Down": "Page Down",
"Esc": "Esc",
"Enter": "Enter",
"Space": "Space",
"End": "End",
"[number]": "[number]",
"Keyboard": "Keyboard",
"Labs": "Labs",
"Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.",
@ -1499,6 +1482,7 @@
"Keyboard shortcuts": "Keyboard shortcuts",
"To view all keyboard shortcuts, <a>click here</a>.": "To view all keyboard shortcuts, <a>click here</a>.",
"Displaying time": "Displaying time",
"Composer": "Composer",
"Code blocks": "Code blocks",
"Images, GIFs and videos": "Images, GIFs and videos",
"Timeline": "Timeline",
@ -2895,6 +2879,7 @@
"Mentions only": "Mentions only",
"See room timeline (devtools)": "See room timeline (devtools)",
"Room": "Room",
"Space": "Space",
"Space home": "Space home",
"Manage & explore rooms": "Manage & explore rooms",
"Move up": "Move up",
@ -3380,36 +3365,57 @@
"Failed to set direct chat tag": "Failed to set direct chat tag",
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room",
"Page Up": "Page Up",
"Page Down": "Page Down",
"Esc": "Esc",
"Enter": "Enter",
"End": "End",
"Alt": "Alt",
"Ctrl": "Ctrl",
"Shift": "Shift",
"[number]": "[number]",
"Calls": "Calls",
"Room List": "Room List",
"Navigation": "Navigation",
"Autocomplete": "Autocomplete",
"Toggle Bold": "Toggle Bold",
"Toggle Italics": "Toggle Italics",
"Toggle Quote": "Toggle Quote",
"New line": "New line",
"Navigate recent messages to edit": "Navigate recent messages to edit",
"Jump to start/end of the composer": "Jump to start/end of the composer",
"Navigate composer history": "Navigate composer history",
"Cancel replying to a message": "Cancel replying to a message",
"Navigate to next message to edit": "Navigate to next message to edit",
"Navigate to previous message to edit": "Navigate to previous message to edit",
"Jump to start of the composer": "Jump to start of the composer",
"Jump to end of the composer": "Jump to end of the composer",
"Navigate to next message in composer history": "Navigate to next message in composer history",
"Navigate to previous message in composer history": "Navigate to previous message in composer history",
"Toggle microphone mute": "Toggle microphone mute",
"Toggle video on/off": "Toggle video on/off",
"Scroll up/down in the timeline": "Scroll up/down in the timeline",
"Toggle webcam on/off": "Toggle webcam on/off",
"Dismiss read marker and jump to bottom": "Dismiss read marker and jump to bottom",
"Jump to oldest unread message": "Jump to oldest unread message",
"Upload a file": "Upload a file",
"Search (must be enabled)": "Search (must be enabled)",
"Scroll up in the timeline": "Scroll up in the timeline",
"Scroll down in the timeline": "Scroll down in the timeline",
"Jump to room search": "Jump to room search",
"Navigate up/down in the room list": "Navigate up/down in the room list",
"Select room from the room list": "Select room from the room list",
"Collapse room list section": "Collapse room list section",
"Expand room list section": "Expand room list section",
"Clear room list filter field": "Clear room list filter field",
"Previous/next unread room or DM": "Previous/next unread room or DM",
"Previous/next room or DM": "Previous/next room or DM",
"Navigate up in the room list": "Navigate up in the room list",
"Navigate down in the room list": "Navigate down in the room list",
"Toggle the top left menu": "Toggle the top left menu",
"Close dialog or context menu": "Close dialog or context menu",
"Activate selected button": "Activate selected button",
"Toggle space panel": "Toggle space panel",
"Toggle right panel": "Toggle right panel",
"Open this settings tab": "Open this settings tab",
"Go to Home View": "Go to Home View",
"Move autocomplete selection up/down": "Move autocomplete selection up/down",
"Cancel autocomplete": "Cancel autocomplete"
"Next unread room or DM": "Next unread room or DM",
"Previous unread room or DM": "Previous unread room or DM",
"Next room or DM": "Next room or DM",
"Previous room or DM": "Previous room or DM",
"Cancel autocomplete": "Cancel autocomplete",
"Next autocomplete suggestion": "Next autocomplete suggestion",
"Previous autocomplete suggestion": "Previous autocomplete suggestion",
"Toggle space panel": "Toggle space panel"
}

View File

@ -0,0 +1,45 @@
/*
Copyright 2022 Šimon Brandner <simon.bra.ag@gmail.com>
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 {
CATEGORIES,
CategoryName,
KEYBOARD_SHORTCUTS,
registerShortcut,
} from "../../src/accessibility/KeyboardShortcuts";
import { Key } from "../../src/Keyboard";
import { ISetting } from "../../src/settings/Settings";
describe("KeyboardShortcuts", () => {
describe("registerShortcut()", () => {
it("correctly registers shortcut", () => {
const shortcutName = "Keybinding.definitelyARealShortcut";
const shortcutCategory = CategoryName.NAVIGATION;
const shortcut: ISetting = {
displayName: "A real shortcut",
default: {
ctrlKey: true,
key: Key.A,
},
};
registerShortcut(shortcutName, shortcutCategory, shortcut);
expect(KEYBOARD_SHORTCUTS[shortcutName]).toBe(shortcut);
expect(CATEGORIES[shortcutCategory].settingNames.includes(shortcutName)).toBeTruthy();
});
});
});

View File

@ -0,0 +1,134 @@
/*
Copyright 2022 Šimon Brandner <simon.bra.ag@gmail.com>
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 from "react";
import { mount, ReactWrapper } from "enzyme";
import { Key } from "../../../../../../src/Keyboard";
const PATH_TO_KEYBOARD_SHORTCUTS = "../../../../../../src/accessibility/KeyboardShortcuts";
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 renderKeyboardUserSettingsTab = async (component, props?): Promise<ReactWrapper> => {
const Component = (await import(PATH_TO_COMPONENT))[component];
return mount(<Component {...props} />);
};
describe("KeyboardUserSettingsTab", () => {
beforeEach(() => {
jest.resetModules();
});
it("renders key icon", async () => {
const body = await renderKeyboardUserSettingsTab("KeyboardKey", { name: Key.ARROW_DOWN });
expect(body).toMatchSnapshot();
});
it("renders alternative key name", async () => {
const body = await renderKeyboardUserSettingsTab("KeyboardKey", { name: Key.PAGE_DOWN });
expect(body).toMatchSnapshot();
});
it("doesn't render + if last", async () => {
const body = await renderKeyboardUserSettingsTab("KeyboardKey", { name: Key.A, last: true });
expect(body).toMatchSnapshot();
});
it("doesn't render same modifier twice", async () => {
mockKeyboardShortcuts({
"KEYBOARD_SHORTCUTS": {
"keybind1": {
default: {
key: Key.A,
ctrlOrCmdKey: true,
metaKey: true,
},
displayName: "Cancel replying to a message",
},
},
});
const body1 = await renderKeyboardUserSettingsTab("KeyboardShortcut", { name: "keybind1" });
expect(body1).toMatchSnapshot();
jest.resetModules();
mockKeyboardShortcuts({
"KEYBOARD_SHORTCUTS": {
"keybind1": {
default: {
key: Key.A,
ctrlOrCmdKey: true,
ctrlKey: true,
},
displayName: "Cancel replying to a message",
},
},
});
const body2 = await renderKeyboardUserSettingsTab("KeyboardShortcut", { name: "keybind1" });
expect(body2).toMatchSnapshot();
jest.resetModules();
});
it("renders list of keyboard shortcuts", async () => {
mockKeyboardShortcuts({
"KEYBOARD_SHORTCUTS": {
"keybind1": {
default: {
key: Key.A,
ctrlKey: true,
},
displayName: "Cancel replying to a message",
},
"keybind2": {
default: {
key: Key.B,
ctrlKey: true,
},
displayName: "Toggle Bold",
},
"keybind3": {
default: {
key: Key.ENTER,
},
displayName: "Select room from the room list",
},
},
"CATEGORIES": {
"Composer": {
settingNames: ["keybind1", "keybind2"],
categoryLabel: "Composer",
},
"Navigation": {
settingNames: ["keybind3"],
categoryLabel: "Navigation",
},
},
});
const body = await renderKeyboardUserSettingsTab("default");
expect(body).toMatchSnapshot();
});
});

View File

@ -0,0 +1,269 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KeyboardUserSettingsTab doesn't render + if last 1`] = `
<KeyboardKey
last={true}
name="a"
>
<kbd>
a
</kbd>
</KeyboardKey>
`;
exports[`KeyboardUserSettingsTab doesn't render same modifier twice 1`] = `
<KeyboardShortcut
name="keybind1"
>
<div>
<KeyboardKey
key="ctrlOrCmdKey"
name="Control"
>
<kbd>
missing translation: en|Ctrl
</kbd>
+
</KeyboardKey>
<KeyboardKey
last={true}
name="a"
>
<kbd>
a
</kbd>
</KeyboardKey>
</div>
</KeyboardShortcut>
`;
exports[`KeyboardUserSettingsTab doesn't render same modifier twice 2`] = `
<KeyboardShortcut
name="keybind1"
>
<div>
<KeyboardKey
key="ctrlOrCmdKey"
name="Control"
>
<kbd>
missing translation: en|Ctrl
</kbd>
+
</KeyboardKey>
<KeyboardKey
last={true}
name="a"
>
<kbd>
a
</kbd>
</KeyboardKey>
</div>
</KeyboardShortcut>
`;
exports[`KeyboardUserSettingsTab renders alternative key name 1`] = `
<KeyboardKey
name="PageDown"
>
<kbd>
missing translation: en|Page Down
</kbd>
+
</KeyboardKey>
`;
exports[`KeyboardUserSettingsTab renders key icon 1`] = `
<KeyboardKey
name="ArrowDown"
>
<kbd>
</kbd>
+
</KeyboardKey>
`;
exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
<KeyboardUserSettingsTab>
<div
className="mx_SettingsTab mx_KeyboardUserSettingsTab"
>
<div
className="mx_SettingsTab_heading"
>
missing translation: en|Keyboard
</div>
<KeyboardShortcutSection
category={
Object {
"categoryLabel": "Composer",
"settingNames": Array [
"keybind1",
"keybind2",
],
}
}
categoryName="Composer"
key="Composer"
>
<div
className="mx_SettingsTab_section"
key="Composer"
>
<div
className="mx_SettingsTab_subheading"
>
missing translation: en|Composer
</div>
<div>
<KeyboardShortcutRow
key="keybind1"
name="keybind1"
>
<div
className="mx_KeyboardShortcut_shortcutRow"
>
Cancel replying to a message
<KeyboardShortcut
name="keybind1"
>
<div>
<KeyboardKey
key="ctrlKey"
name="Control"
>
<kbd>
missing translation: en|Ctrl
</kbd>
+
</KeyboardKey>
<KeyboardKey
last={true}
name="a"
>
<kbd>
a
</kbd>
</KeyboardKey>
</div>
</KeyboardShortcut>
</div>
</KeyboardShortcutRow>
<KeyboardShortcutRow
key="keybind2"
name="keybind2"
>
<div
className="mx_KeyboardShortcut_shortcutRow"
>
Toggle Bold
<KeyboardShortcut
name="keybind2"
>
<div>
<KeyboardKey
key="ctrlKey"
name="Control"
>
<kbd>
missing translation: en|Ctrl
</kbd>
+
</KeyboardKey>
<KeyboardKey
last={true}
name="b"
>
<kbd>
b
</kbd>
</KeyboardKey>
</div>
</KeyboardShortcut>
</div>
</KeyboardShortcutRow>
</div>
</div>
</KeyboardShortcutSection>
<KeyboardShortcutSection
category={
Object {
"categoryLabel": "Navigation",
"settingNames": Array [
"keybind3",
],
}
}
categoryName="Navigation"
key="Navigation"
>
<div
className="mx_SettingsTab_section"
key="Navigation"
>
<div
className="mx_SettingsTab_subheading"
>
missing translation: en|Navigation
</div>
<div>
<KeyboardShortcutRow
key="keybind3"
name="keybind3"
>
<div
className="mx_KeyboardShortcut_shortcutRow"
>
Select room from the room list
<KeyboardShortcut
name="keybind3"
>
<div>
<KeyboardKey
last={true}
name="Enter"
>
<kbd>
missing translation: en|Enter
</kbd>
</KeyboardKey>
</div>
</KeyboardShortcut>
</div>
</KeyboardShortcutRow>
</div>
</div>
</KeyboardShortcutSection>
</div>
</KeyboardUserSettingsTab>
`;