Skip to content

Commit 81a60ee

Browse files
cpcallenEdward Jung
and
Edward Jung
authored
feat: Keyboard shortcut modal dialog (#75)
* feat: Add a modal dialog to display available shortcuts. * chore: Remove hard coded shortcut table * feat: Added categorisation to the short dialog. --------- Co-authored-by: Edward Jung <edwardjung@google.com>
1 parent 3099eb8 commit 81a60ee

File tree

6 files changed

+409
-23
lines changed

6 files changed

+409
-23
lines changed

src/announcer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import * as Blockly from 'blockly/core';
78
import {ShortcutRegistry} from 'blockly/core';
89
// @ts-expect-error No types in js file
910
import {keyCodeArrayToString} from './keynames';
@@ -13,6 +14,8 @@ import {keyCodeArrayToString} from './keynames';
1314
*/
1415
export class Announcer {
1516
outputDiv: HTMLElement | null;
17+
modalContainer: HTMLElement | null = null;
18+
shortcutDialog: HTMLElement | null = null;
1619
/**
1720
* Constructor for an Announcer.
1821
*/

src/constants.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,50 @@ export enum LOGGING_MSG_TYPE {
6363
WARN = 'warn',
6464
LOG = 'log',
6565
}
66+
67+
/**
68+
* Platform specific modifier key used in shortcuts.
69+
*/
70+
export enum MODIFIER_KEY {
71+
Window = 'Ctrl',
72+
ChromeOS = 'Ctrl',
73+
macOS = '⌘ Command',
74+
Linux = 'Meta',
75+
}
76+
77+
/**
78+
* Categories used to organised the shortcut dialog.
79+
* Shortcut name should match those obtained from the Blockly shortcut register.
80+
*/
81+
export const SHORTCUT_CATEGORIES = {
82+
'General': [
83+
'escape',
84+
'exit',
85+
'delete',
86+
'run_code',
87+
'toggle_keyboard_nav',
88+
'Announce',
89+
'List shortcuts',
90+
'toolbox',
91+
'disconnect',
92+
],
93+
'Editing': ['cut', 'copy', 'paste', 'undo', 'redo', 'mark', 'insert'],
94+
'Code navigation': [
95+
'previous',
96+
'next',
97+
'in',
98+
'out',
99+
'Context in',
100+
'Context out',
101+
'Go to previous sibling',
102+
'Go to next sibling',
103+
'Jump to root of current stack',
104+
],
105+
'Workspace navigation': [
106+
'workspace_down',
107+
'workspace_left',
108+
'workspace_up',
109+
'workspace_right',
110+
'Clean up workspace',
111+
],
112+
};

src/keynames.js

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,37 +120,65 @@ const keyNames = {
120120
224: 'win',
121121
};
122122

123+
const modifierKeys = ['control', 'alt', 'meta'];
124+
123125
/**
124-
* Convert from a serialized key code to a string.
126+
* Assign the appropriate class names for the key.
127+
* Modifier keys are indicated so they can be switched to a platform specific
128+
* key.
129+
*/
130+
function getKeyClassName(keyName) {
131+
return modifierKeys.includes(keyName.toLowerCase()) ? 'key modifier' : 'key';
132+
}
133+
134+
function toTitleCase(str) {
135+
return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase();
136+
}
137+
138+
/**
139+
* Convert from a serialized key code to a HTML string.
125140
* This should be the inverse of ShortcutRegistry.createSerializedKey, but
126141
* should also convert ascii characters to strings.
127142
* @param {string} keycode The key code as a string of characters separated
128143
* by the + character.
129144
* @returns {string} A single string representing the key code.
130145
*/
131-
function keyCodeToString(keycode) {
132-
let result = '';
146+
function keyCodeToString(keycode, index) {
147+
let result = `<span class="shortcut-combo shortcut-combo-${index}">`;
133148
const pieces = keycode.split('+');
134149

135150
let piece = pieces[0];
136151
let strrep = keyNames[piece] ?? piece;
137-
result += strrep;
138152

139-
for (let i = 1; i < pieces.length; i++) {
153+
for (let i = 0; i < pieces.length; i++) {
140154
piece = pieces[i];
141155
strrep = keyNames[piece] ?? piece;
142-
result += `+${strrep}`;
156+
const className = getKeyClassName(strrep);
157+
158+
if (i === pieces.length - 1 && i !== 0) {
159+
strrep = strrep.toUpperCase();
160+
} else {
161+
strrep = toTitleCase(strrep);
162+
}
163+
164+
if (i > 0) {
165+
result += '+';
166+
}
167+
result += `<span class="${className}">${strrep}</span>`;
143168
}
169+
result += '</span>';
144170
return result;
145171
}
146172

147173
/**
148174
* Convert an array of key codes into a comma-separated list of strings
149175
* @param {Array<string>} keycodeArr The array of key codes to convert.
150176
* @returns {string} The input array as a comma-separated list of
151-
* human-readable strings.
177+
* human-readable strings wrapped in HTML.
152178
*/
153179
export function keyCodeArrayToString(keycodeArr) {
154-
const stringified = keycodeArr.map((keycode) => keyCodeToString(keycode));
155-
return stringified.join(', ');
180+
const stringified = keycodeArr.map((keycode, index) =>
181+
keyCodeToString(keycode, index),
182+
);
183+
return stringified.join('<span class="separator">/</span>');
156184
}

src/navigation_controller.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import * as Constants from './constants';
2727
import {Navigation} from './navigation';
2828
import {Announcer} from './announcer';
2929
import {LineCursor} from './line_cursor';
30+
import {ShortcutDialog} from './shortcut_dialog';
3031

3132
const KeyCodes = BlocklyUtils.KeyCodes;
3233
const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind(
@@ -44,6 +45,7 @@ export class NavigationController {
4445
copyWorkspace: WorkspaceSvg | null = null;
4546
navigation: Navigation = new Navigation();
4647
announcer: Announcer = new Announcer();
48+
shortcutDialog: ShortcutDialog = new ShortcutDialog();
4749

4850
isAutoNavigationEnabled: boolean = false;
4951
hasNavigationFocus: boolean = false;
@@ -220,7 +222,7 @@ export class NavigationController {
220222
[name: string]: ShortcutRegistry.KeyboardShortcut;
221223
} = {
222224
/** Go to the previous location. */
223-
previous: {
225+
previous: {
224226
name: Constants.SHORTCUT_NAMES.PREVIOUS,
225227
preconditionFn: (workspace) => workspace.keyboardAccessibilityMode,
226228
callback: (workspace, _, shortcut) => {
@@ -367,7 +369,7 @@ export class NavigationController {
367369
insert: {
368370
name: Constants.SHORTCUT_NAMES.INSERT,
369371
preconditionFn: (workspace) =>
370-
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
372+
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
371373
callback: (workspace) => {
372374
switch (this.navigation.getState(workspace)) {
373375
case Constants.STATE.WORKSPACE:
@@ -442,7 +444,7 @@ export class NavigationController {
442444
disconnect: {
443445
name: Constants.SHORTCUT_NAMES.DISCONNECT,
444446
preconditionFn: (workspace) =>
445-
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
447+
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
446448
callback: (workspace) => {
447449
switch (this.navigation.getState(workspace)) {
448450
case Constants.STATE.WORKSPACE:
@@ -459,7 +461,7 @@ export class NavigationController {
459461
focusToolbox: {
460462
name: Constants.SHORTCUT_NAMES.TOOLBOX,
461463
preconditionFn: (workspace) =>
462-
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
464+
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
463465
callback: (workspace) => {
464466
switch (this.navigation.getState(workspace)) {
465467
case Constants.STATE.WORKSPACE:
@@ -507,7 +509,7 @@ export class NavigationController {
507509
wsMoveLeft: {
508510
name: Constants.SHORTCUT_NAMES.MOVE_WS_CURSOR_LEFT,
509511
preconditionFn: (workspace) =>
510-
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
512+
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
511513
callback: (workspace) => {
512514
return this.navigation.moveWSCursor(workspace, -1, 0);
513515
},
@@ -518,7 +520,7 @@ export class NavigationController {
518520
wsMoveRight: {
519521
name: Constants.SHORTCUT_NAMES.MOVE_WS_CURSOR_RIGHT,
520522
preconditionFn: (workspace) =>
521-
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
523+
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
522524
callback: (workspace) => {
523525
return this.navigation.moveWSCursor(workspace, 1, 0);
524526
},
@@ -529,7 +531,7 @@ export class NavigationController {
529531
wsMoveUp: {
530532
name: Constants.SHORTCUT_NAMES.MOVE_WS_CURSOR_UP,
531533
preconditionFn: (workspace) =>
532-
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
534+
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
533535
callback: (workspace) => {
534536
return this.navigation.moveWSCursor(workspace, 0, -1);
535537
},
@@ -540,7 +542,7 @@ export class NavigationController {
540542
wsMoveDown: {
541543
name: Constants.SHORTCUT_NAMES.MOVE_WS_CURSOR_DOWN,
542544
preconditionFn: (workspace) =>
543-
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
545+
workspace.keyboardAccessibilityMode && !workspace.options.readOnly,
544546
callback: (workspace) => {
545547
return this.navigation.moveWSCursor(workspace, 0, 1);
546548
},
@@ -593,9 +595,9 @@ export class NavigationController {
593595
paste: {
594596
name: Constants.SHORTCUT_NAMES.PASTE,
595597
preconditionFn: (workspace) =>
596-
workspace.keyboardAccessibilityMode &&
597-
!workspace.options.readOnly &&
598-
!Blockly.Gesture.inProgress(),
598+
workspace.keyboardAccessibilityMode &&
599+
!workspace.options.readOnly &&
600+
!Blockly.Gesture.inProgress(),
599601
callback: () => {
600602
if (!this.copyData || !this.copyWorkspace) return false;
601603
return this.navigation.paste(this.copyData, this.copyWorkspace);
@@ -688,11 +690,11 @@ export class NavigationController {
688690
allowCollision: true,
689691
},
690692

691-
/** List all current shortcuts in the announcer area. */
693+
/** List all of the currently registered shortcuts. */
692694
announceShortcuts: {
693695
name: Constants.SHORTCUT_NAMES.LIST_SHORTCUTS,
694696
callback: (workspace) => {
695-
this.announcer.listShortcuts();
697+
this.shortcutDialog.toggle();
696698
return true;
697699
},
698700
keyCodes: [KeyCodes.SLASH],
@@ -845,7 +847,7 @@ export class NavigationController {
845847
},
846848
keyCodes: [
847849
createSerializedKey(KeyCodes.TAB, [KeyCodes.SHIFT]),
848-
KeyCodes.TAB
850+
KeyCodes.TAB,
849851
],
850852
},
851853
};
@@ -859,6 +861,11 @@ export class NavigationController {
859861
for (const shortcut of Object.values(this.shortcuts)) {
860862
ShortcutRegistry.registry.register(shortcut);
861863
}
864+
865+
// Initalise the shortcut modal with available shortcuts. Needs
866+
// to be done separately rather at construction, as many shortcuts
867+
// are not registered at that point.
868+
this.shortcutDialog.createModalContent();
862869
}
863870

864871
/**

0 commit comments

Comments
 (0)