Skip to content
20 changes: 13 additions & 7 deletions webapp/src/components/KeyboardControlsHelp.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import * as Blockly from "blockly";
import { getShortcutKeysShortAll, LIST_SHORTCUTS_SHORTCUT } from "../shortcut_formatting";
import { CONTROL_KEY_SHORT, getShortcutKeysShortAll, LIST_SHORTCUTS_SHORTCUT } from "../shortcut_formatting";

const names = Blockly.ShortcutItems.names;
const isMacPlatform = pxt.BrowserUtils.isMac();
Expand All @@ -10,20 +10,19 @@ const KeyboardControlsHelp = () => {
React.useEffect(() => {
ref.current?.focus()
}, []);
const ctrl = lf("{id:keyboard symbol}Ctrl");
const ctrl = CONTROL_KEY_SHORT;
const cmd = isMacPlatform ? "⌘" : ctrl;
const orAsJoiner = lf("or")
const enterOrSpace = { shortcuts: getShortcutKeysShortAll(names.PERFORM_ACTION), joiner: orAsJoiner}
// Split around {0} so the modifier renders as a <Key> (for its aria-label).
const moveAnywhere = lf("Hold {0} and press arrow keys").split("{0}");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's actually a function for this in react-common/components/util.tsx:

export function jsxLF(loc: string, ...rest: JSX.Element[]) {

here's an example of usage:

message = jsxLF(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks. Adapted in 4fbad89.

return (
<aside id="keyboardnavhelp" aria-label={lf("Keyboard Controls")} ref={ref} tabIndex={0}>
<h2>{lf("Keyboard Controls")}</h2>
<h3>{lf("Global")}</h3>
<table>
<tbody>
<Row name={lf("Show/hide shortcut help")} shortcuts={[LIST_SHORTCUTS_SHORTCUT]} />
<Row name={lf("Screen reader mode")} shortcuts={[names.TOGGLE_SCREENREADER]}>
<p className="hint">{lf("Additional audio cues and navigation aids for screen reader users")}</p>
</Row>
<Row name={lf("Move between menus, simulator and the workspace")} shortcuts={[[lf("{id:keyboard symbol}Tab")], [lf("{id:keyboard symbol}Shift"), lf("{id:keyboard symbol}Tab")]]} joiner="row"/>
<Row name={lf("Area menu")} shortcuts={[[cmd, "B"]]}>
<p className="hint">{lf("Then press an area's number, or Tab to it and press Enter")}</p>
Expand All @@ -37,14 +36,17 @@ const KeyboardControlsHelp = () => {
<Row name={lf("Next block stack")} shortcuts={[names.NEXT_STACK]} />
<Row name={lf("Previous block stack")} shortcuts={[names.PREVIOUS_STACK]} />
<Row name={lf("Select workspace")} shortcuts={[names.FOCUS_WORKSPACE]} />
<Row name={lf("Open context menu")} shortcuts={[names.MENU]} />
<Row name={lf("Context menu")} shortcuts={[names.MENU]} />
<Row name={lf("Format code")} shortcuts={[names.CLEANUP]} />
<Row name={lf("Undo / redo")} shortcuts={[names.UNDO, names.REDO]} joiner="/" />
{pxt.canDownload() &&
<Row name={lf("Download your code to the {0}", pxt.appTarget.appTheme.boardName)} shortcuts={[["L"]]} />}
<Row name={lf("Toolbox")} shortcuts={[names.FOCUS_TOOLBOX]} />
{pxt.appTarget.simulator &&
<Row name={lf("Start or stop simulator")} shortcuts={[["S"]]} />}
<Row name={lf("Screen reader mode")} shortcuts={[names.TOGGLE_SCREENREADER]}>
<p className="hint">{lf("Additional audio cues and navigation aids for screen reader users")}</p>
</Row>
</tbody>
</table>
<h3>{lf("Toolbox")}</h3>
Expand Down Expand Up @@ -78,7 +80,7 @@ const KeyboardControlsHelp = () => {
<tbody>
<Row name={lf("Move to positions")} shortcuts={[names.NAVIGATE_UP, names.NAVIGATE_DOWN, names.NAVIGATE_LEFT, names.NAVIGATE_RIGHT]} />
<Row name={lf("Move anywhere")}>
{lf("Hold {0} and press arrow keys", cmd)}
{moveAnywhere[0]}<Key value={cmd} />{moveAnywhere[1]}
</Row>
<Row name={lf("Confirm")} {...enterOrSpace} />
<Row name={lf("Cancel")} shortcuts={[names.ABORT_MOVE]} />
Expand Down Expand Up @@ -177,6 +179,10 @@ const Key = ({ value }: { value: string }) => {
aria = lf("Option");
break;
}
case CONTROL_KEY_SHORT: {
aria = Blockly.Msg['CONTROL_KEY'];
break;
}
Comment thread
microbit-matt-hillsdon marked this conversation as resolved.
}
return <span className="key" aria-label={aria}>{value}</span>
}
Expand Down
21 changes: 15 additions & 6 deletions webapp/src/shortcut_formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as Blockly from 'blockly';

const isMacPlatform = pxt.BrowserUtils.isMac();

export const CONTROL_KEY_SHORT = lf("{id:keyboard symbol}Ctrl");

/**
* Name of the shortcut that opens the help dialog. Registered in blocks.tsx
* initKeyboardControls. The literal string is also hardcoded by Blockly core's
Expand Down Expand Up @@ -47,7 +49,7 @@ const longModifierNames: Record<string, string> = {
};

const shortModifierNames: Record<string, string> = {
'Control': Blockly.Msg['CONTROL_KEY'],
'Control': CONTROL_KEY_SHORT,
'Meta': '⌘',
'Alt': isMacPlatform ? '⌥' : Blockly.Msg['ALT_KEY'],
};
Expand Down Expand Up @@ -149,7 +151,8 @@ function getKeyName(keyCode: number): string {
*
* Mirrors Blockly's internal getShortcutKeys (core/utils/shortcut_formatting.ts).
* Kept as a local copy because Blockly doesn't expose this on its public API
* surface; the algorithm should track Blockly's version.
* surface; the algorithm tracks Blockly's version with an exception to avoid
* the menu key for the context menu shortcut (not present on all keyboards).
*
* @param shortcutName The action name, e.g. "cut".
* @param modifierNames The names to use for the Meta/Control/Alt modifiers.
Expand Down Expand Up @@ -186,12 +189,13 @@ function getShortcutKeys(

// If there are modifiers return only one shortcut on the assumption they are
// intended for different platforms. Otherwise assume they are alternatives.
// Skips a bare key here for the menu key case.
const hasModifiers = currentPlatform.some((shortcut) =>
shortcut.some(
(key) => "Meta" === key || "Alt" === key || "Control" === key,
),
shortcut.some(isModifierName),
);
const chosen = hasModifiers ? [currentPlatform[0]] : currentPlatform;
const chosen = hasModifiers
? [currentPlatform.find((shortcut) => shortcut.some(isModifierName)) ?? currentPlatform[0]]
: currentPlatform;
return chosen.map((shortcut) =>
shortcut
.map((maybeNumeric) =>
Expand All @@ -217,3 +221,8 @@ function modifierOrder(key: string): number {
// Regular keys at the end.
return order === -1 ? Number.MAX_VALUE : order;
}

/** Whether a serialized key part is one of the (non-Shift) modifier names. */
function isModifierName(key: string): boolean {
return key === "Meta" || key === "Alt" || key === "Control";
}