Skip to content

Commit 9799b55

Browse files
Keyboard Controls documentation
This uses the sidebar but not the docs system which allows us to pull live shortcut bindings. Particularly useful if we allow custom shortcut bindings going forward. - Moves the tab order of the sidebar docs forward and adds a focus ring. - Handle shortcuts triggered when the sim has focus. - Maximise the mini sim for region nav as it's too small for accessible use. - Added support for escape to close keyboard controls - note this doesn't work for reference docs due to the iframe. Closes #10556
1 parent 59d54e0 commit 9799b55

12 files changed

+570
-9
lines changed

localtypings/pxteditor.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,8 @@ declare namespace pxt.editor {
901901

902902
export type Activity = "tutorial" | "recipe" | "example";
903903

904+
export type BuiltInHelp = "keyboardControls";
905+
904906
export interface IProjectView {
905907
state: IAppState;
906908
setState(st: IAppState): void;
@@ -958,6 +960,7 @@ declare namespace pxt.editor {
958960
setSideFile(fn: IFile, line?: number): void;
959961
navigateToError(diag: pxtc.KsDiagnostic): void;
960962
setSideDoc(path: string, blocksEditor?: boolean): void;
963+
toggleBuiltInSideDoc(help: BuiltInHelp, focusIfOpen: boolean): void;
961964
setSideMarkdown(md: string): void;
962965
setSideDocCollapsed(shouldCollapse?: boolean): void;
963966
removeFile(fn: IFile, skipConfirm?: boolean): void;

pxtsim/accessibility.ts

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ namespace pxsim.accessibility {
1313
if (e.key === "Escape") {
1414
e.preventDefault();
1515
return "escape"
16+
} else if (e.key === "/" && meta) {
17+
e.preventDefault();
18+
return "togglekeyboardcontrolshelp";
1619
} else if (e.key === "u" && meta) {
1720
e.preventDefault();
1821
return "navigateregions"

pxtsim/embed.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ namespace pxsim {
7575
url: string;
7676
}
7777

78-
export type SimulatorAction = "escape" | "navigateregions";
78+
export type SimulatorAction = "escape" | "navigateregions" | "togglekeyboardcontrolshelp";
7979

8080
export interface SimulatorActionMessage extends SimulatorMessage {
8181
action: SimulatorAction;

theme/pxt.less

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
@import 'tutorial';
1212
@import 'tutorial-sidebar';
1313
@import 'sidedoc';
14+
@import 'sidedoc-keyboard-nav-help';
1415
@import 'home';
1516
@import 'serial';
1617
@import 'docs';

theme/sidedoc-keyboard-nav-help.less

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/* Import all components */
2+
@import 'themes/default/globals/site.variables';
3+
@import 'themes/pxt/globals/site.variables';
4+
5+
/* Reference import */
6+
@import (reference) "semantic.less";
7+
8+
/*******************************
9+
Keyboard nav help
10+
*******************************/
11+
12+
#keyboardnavhelp {
13+
font-family: @docsPageFont !important;
14+
color: @docsTextColor;
15+
background-color: @docsBackgroundColor;
16+
17+
height: 100%;
18+
padding: 1rem;
19+
overflow: auto;
20+
.key {
21+
display: inline-flex;
22+
justify-content: center;
23+
padding: 0.2rem;
24+
border: 1px solid var(--pxt-neutral-foreground1);
25+
border-radius: 5px;
26+
min-width: 1.8em;
27+
}
28+
.shortcut {
29+
gap: 0.5rem;
30+
}
31+
.hint {
32+
font-size: 85%;
33+
line-height: 1;
34+
}
35+
table {
36+
width: 100%;
37+
table-layout: fixed;
38+
text-align: left;
39+
}
40+
th, td {
41+
vertical-align: top;
42+
padding: 0.4rem 0.2rem
43+
}
44+
tr {
45+
margin-bottom: 0.3rem;
46+
}
47+
h3 {
48+
margin-top: 2rem;
49+
}
50+
}

theme/sidedoc.less

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ code.hljs {
6363
border-top-left-radius: 5px;
6464
border-bottom-left-radius: 5px;
6565
z-index: @sidedocZIndex;
66+
67+
&:has(aside:focus-visible) {
68+
outline: @editorFocusBorderSize solid var(--pxt-focus-border);
69+
}
6670
}
6771

6872
.sideDocs #sidedocsframe {

webapp/src/app.tsx

+19-2
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,12 @@ export class ProjectView
14371437
}
14381438
}
14391439

1440+
toggleBuiltInSideDoc(help: pxt.editor.BuiltInHelp, focusIfVisible: boolean) {
1441+
let sd = this.refs["sidedoc"] as container.SideDocs;
1442+
if (!sd) return;
1443+
sd.toggleBuiltInHelp(help, focusIfVisible);
1444+
}
1445+
14401446
setTutorialInstructionsExpanded(value: boolean): void {
14411447
const tutorialOptions = this.state.tutorialOptions;
14421448
tutorialOptions.tutorialStepExpanded = value;
@@ -1797,6 +1803,13 @@ export class ProjectView
17971803
this.shouldTryDecompile = true;
17981804
}
17991805

1806+
// Onboard accessible blocks if accessible blocks has just been enabled
1807+
const onboardAccessibleBlocks = pxt.storage.getLocal("onboardAccessibleBlocks") === "1"
1808+
const sideDocsLoadUrl = onboardAccessibleBlocks ? `${container.builtInPrefix}keyboardControls` : ""
1809+
if (onboardAccessibleBlocks) {
1810+
pxt.storage.setLocal("onboardAccessibleBlocks", "0")
1811+
}
1812+
18001813
this.setState({
18011814
home: false,
18021815
showFiles: h.githubId ? true : false,
@@ -1805,7 +1818,7 @@ export class ProjectView
18051818
header: h,
18061819
projectName: h.name,
18071820
currFile: file,
1808-
sideDocsLoadUrl: '',
1821+
sideDocsLoadUrl: sideDocsLoadUrl,
18091822
debugging: false,
18101823
isMultiplayerGame: false
18111824
});
@@ -5174,6 +5187,10 @@ export class ProjectView
51745187
}
51755188

51765189
async toggleAccessibleBlocks() {
5190+
const nextEnabled = !this.getData<boolean>(auth.ACCESSIBLE_BLOCKS);
5191+
if (nextEnabled) {
5192+
pxt.storage.setLocal("onboardAccessibleBlocks", "1")
5193+
}
51775194
await core.toggleAccessibleBlocks()
51785195
this.reloadEditor();
51795196
}
@@ -5501,8 +5518,8 @@ export class ProjectView
55015518
<projects.Projects parent={this} ref={this.handleHomeRef} />
55025519
</div>
55035520
</div> : undefined}
5504-
{showEditorToolbar && <editortoolbar.EditorToolbar ref="editortools" parent={this} />}
55055521
{sideDocs ? <container.SideDocs ref="sidedoc" parent={this} sideDocsCollapsed={this.state.sideDocsCollapsed} docsUrl={this.state.sideDocsLoadUrl} /> : undefined}
5522+
{showEditorToolbar && <editortoolbar.EditorToolbar ref="editortools" parent={this} />}
55065523
{sandbox ? undefined : <scriptsearch.ScriptSearch parent={this} ref={this.handleScriptSearchRef} />}
55075524
{sandbox ? undefined : <extensions.Extensions parent={this} ref={this.handleExtensionRef} />}
55085525
{inHome ? <projects.ImportDialog parent={this} ref={this.handleImportDialogRef} /> : undefined}

webapp/src/blocks.tsx

+21-2
Original file line numberDiff line numberDiff line change
@@ -569,10 +569,25 @@ export class Editor extends toolboxeditor.ToolboxEditor {
569569
focusRingDiv.className = "blocklyWorkspaceFocusRingLayer";
570570
this.editor.getSvgGroup().addEventListener("focus", () => {
571571
focusRingDiv.dataset.focused = "true";
572-
})
572+
});
573573
this.editor.getSvgGroup().addEventListener("blur", () => {
574574
delete focusRingDiv.dataset.focused;
575-
})
575+
});
576+
577+
const listShortcuts = Blockly.ShortcutRegistry.registry.getRegistry()["list_shortcuts"];
578+
Blockly.ShortcutRegistry.registry.unregister(listShortcuts.name);
579+
Blockly.ShortcutRegistry.registry.register({
580+
...listShortcuts,
581+
keyCodes: [
582+
Blockly.ShortcutRegistry.registry.createSerializedKey(Blockly.utils.KeyCodes.SLASH, [
583+
Blockly.utils.KeyCodes.META,
584+
]),
585+
Blockly.ShortcutRegistry.registry.createSerializedKey(Blockly.utils.KeyCodes.SLASH, [
586+
Blockly.utils.KeyCodes.CTRL,
587+
]),
588+
]
589+
});
590+
576591

577592
const cleanUpWorkspace = Blockly.ShortcutRegistry.registry.getRegistry()["clean_up_workspace"];
578593
Blockly.ShortcutRegistry.registry.unregister(cleanUpWorkspace.name);
@@ -592,6 +607,10 @@ export class Editor extends toolboxeditor.ToolboxEditor {
592607
this.parent.setSimulatorFullScreen(false);
593608
return;
594609
}
610+
case "togglekeyboardcontrolshelp": {
611+
this.parent.toggleBuiltInSideDoc("keyboardControls", false);
612+
return
613+
}
595614
case "navigateregions" : {
596615
this.parent.showNavigateRegions();
597616
return
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import * as React from "react";
2+
import { getActionShortcut, SHORTCUT_NAMES } from "../shortcut_formatting";
3+
4+
const KeyboardControlsHelp = () => {
5+
const ref = React.useRef<HTMLElement>(null);
6+
React.useEffect(() => {
7+
ref.current?.focus()
8+
}, []);
9+
const ctrl = lf("Ctrl");
10+
const cmd = pxt.BrowserUtils.isMac() ? "⌘" : ctrl;
11+
const optionOrCtrl = pxt.BrowserUtils.isMac() ? "⌥" : ctrl;
12+
const contextMenuRow = <Row name={lf("Open context menu")} shortcuts={[SHORTCUT_NAMES.MENU]} />
13+
const cleanUpRow = <Row name={lf("Workspace: Format code")} shortcuts={[SHORTCUT_NAMES.CLEAN_UP]} />
14+
const orAsJoiner = lf("or")
15+
const enterOrSpace = { shortcuts: [[lf("Enter")], [lf("Space")]], joiner: orAsJoiner}
16+
const editOrConfirmRow = <Row name={lf("Edit or confirm")} {...enterOrSpace} />
17+
return (
18+
<aside id="keyboardnavhelp" aria-label={lf("Keyboard Controls")} ref={ref} tabIndex={0}>
19+
<h2>{lf("Keyboard Controls")}</h2>
20+
<table>
21+
<tbody>
22+
<Row name={lf("Show/hide shortcut help")} shortcuts={[SHORTCUT_NAMES.LIST_SHORTCUTS]} />
23+
<Row name={lf("Jump to region")} shortcuts={[[cmd, "U"]]} />
24+
<Row name={lf("Block and toolbox navigation")} shortcuts={[SHORTCUT_NAMES.UP, SHORTCUT_NAMES.DOWN, SHORTCUT_NAMES.LEFT, SHORTCUT_NAMES.RIGHT]} />
25+
<Row name={lf("Toolbox or insert")} shortcuts={[SHORTCUT_NAMES.TOOLBOX, SHORTCUT_NAMES.INSERT]} joiner={orAsJoiner} />
26+
{editOrConfirmRow}
27+
<Row name={lf("Move mode")} shortcuts={[["M"]]} >
28+
<br /><span className="hint">{lf("Move with arrow keys")}</span>
29+
<br /><span className="hint">{lf("Hold {0} for free movement", optionOrCtrl)}</span>
30+
</Row>
31+
<Row name={lf("Copy / paste")} shortcuts={[SHORTCUT_NAMES.COPY, SHORTCUT_NAMES.PASTE]} joiner="/" />
32+
{cleanUpRow}
33+
{contextMenuRow}
34+
</tbody>
35+
</table>
36+
<h3>{lf("Editor Overview")}</h3>
37+
<table>
38+
<tbody>
39+
<Row name={lf("Move between menus, simulator and the workspace")} shortcuts={[[lf("Tab")], [lf("Shift"), lf("Tab")]]} joiner="row"/>
40+
<Row name={lf("Jump to region")} shortcuts={[[cmd, "U"]]} />
41+
<Row name={lf("Exit")} shortcuts={[SHORTCUT_NAMES.EXIT]} />
42+
<Row name={lf("Toolbox")} shortcuts={[SHORTCUT_NAMES.TOOLBOX]} />
43+
<Row name={lf("Toolbox: Move in and out of categories")} shortcuts={[SHORTCUT_NAMES.LEFT, SHORTCUT_NAMES.RIGHT]} />
44+
<Row name={lf("Toolbox: Navigate categories or blocks")} shortcuts={[SHORTCUT_NAMES.UP, SHORTCUT_NAMES.DOWN]} />
45+
<Row name={lf("Toolbox: Insert block")} {...enterOrSpace} />
46+
<Row name={lf("Workspace: Select workspace")} shortcuts={[["W"]]} />
47+
{cleanUpRow}
48+
</tbody>
49+
</table>
50+
<h3>{lf("Edit Blocks")}</h3>
51+
<table>
52+
<tbody>
53+
<Row name={lf("Move in and out of a block")} shortcuts={[SHORTCUT_NAMES.LEFT, SHORTCUT_NAMES.RIGHT]} />
54+
{editOrConfirmRow}
55+
<Row name={lf("Cancel or exit")} shortcuts={[SHORTCUT_NAMES.EXIT]} />
56+
<Row name={lf("Insert block at current position")} shortcuts={[SHORTCUT_NAMES.INSERT]} />
57+
<Row name={lf("Copy")} shortcuts={[SHORTCUT_NAMES.COPY]} />
58+
<Row name={lf("Paste")} shortcuts={[SHORTCUT_NAMES.PASTE]} />
59+
<Row name={lf("Cut")} shortcuts={[SHORTCUT_NAMES.CUT]} />
60+
<Row name={lf("Delete")} shortcuts={[SHORTCUT_NAMES.DELETE, [lf("Backspace")]]} joiner={orAsJoiner} />
61+
<Row name={lf("Undo")} shortcuts={[SHORTCUT_NAMES.UNDO]} />
62+
<Row name={lf("Redo")} shortcuts={[SHORTCUT_NAMES.REDO]} />
63+
{contextMenuRow}
64+
</tbody>
65+
</table>
66+
<h3>{lf("Moving Blocks")}</h3>
67+
<table>
68+
<tbody>
69+
<Row name={lf("Move mode")} shortcuts={[["M"]]} />
70+
<Row name={lf("Move mode: Move to new position")} shortcuts={[SHORTCUT_NAMES.UP, SHORTCUT_NAMES.DOWN, SHORTCUT_NAMES.LEFT, SHORTCUT_NAMES.RIGHT]} />
71+
<Row name={lf("Move mode: Free movement")}>
72+
{lf("Hold {0} and press arrow keys", optionOrCtrl)}
73+
</Row>
74+
<Row name={lf("Move mode: Confirm")} {...enterOrSpace} />
75+
<Row name={lf("Move mode: Cancel")} shortcuts={[SHORTCUT_NAMES.EXIT]} />
76+
<Row name={lf("Disconnect blocks")} shortcuts={[SHORTCUT_NAMES.DISCONNECT]} />
77+
</tbody>
78+
</table>
79+
</aside>
80+
);
81+
}
82+
83+
const Shortcut = ({ keys }: { keys: string[] }) => {
84+
const joiner = pxt.BrowserUtils.isMac() ? " " : " + "
85+
return (
86+
<span className="shortcut">
87+
{keys.reduce((acc, key) => {
88+
return acc.length === 0
89+
? [...acc, <Key key={key} value={key} />]
90+
: [...acc, joiner, <Key key={key} value={key} />]
91+
}, [])}
92+
</span>
93+
);
94+
}
95+
96+
interface RowProps {
97+
name: string;
98+
shortcuts?: Array<string | string[]>;
99+
joiner?: string;
100+
children?: React.ReactNode;
101+
}
102+
103+
const Row = ({ name, shortcuts = [], joiner, children}: RowProps) => {
104+
const shortcutElements = shortcuts.map((s, idx) => {
105+
if (typeof s === "string") {
106+
// Pull keys from shortcut registry.
107+
return <Shortcut key={idx} keys={getActionShortcut(s)} />
108+
} else {
109+
// Display keys as specified.
110+
return <Shortcut key={idx} keys={s} />
111+
}
112+
})
113+
return joiner === "row" ? (
114+
<>
115+
<tr>
116+
<td width="50%" rowSpan={shortcuts.length}>{name}</td>
117+
<td width="50%">
118+
{shortcutElements[0]}
119+
</td>
120+
</tr>
121+
{shortcutElements.map((el, idx) => idx === 0
122+
? undefined
123+
: (<tr key={idx}>
124+
<td width="50%">
125+
{el}
126+
</td>
127+
</tr>))}
128+
</>
129+
) : (
130+
<tr>
131+
<td width="50%">{name}</td>
132+
<td width="50%">
133+
{shortcutElements.reduce((acc, shortcut) => {
134+
return acc.length === 0
135+
? [...acc, shortcut]
136+
: [...acc, joiner ? ` ${joiner} ` : " ", shortcut]
137+
}, [])}
138+
{children}
139+
<br />
140+
</td>
141+
</tr>
142+
)
143+
}
144+
145+
const Key = ({ value }: { value: string }) => {
146+
let aria;
147+
switch (value) {
148+
case "↑": {
149+
aria = lf("Up Arrow");
150+
break;
151+
}
152+
case "↓": {
153+
aria = lf("Down Arrow");
154+
break;
155+
}
156+
case "←": {
157+
aria = lf("Left Arrow");
158+
break;
159+
}
160+
case "→": {
161+
aria = lf("Right Arrow");
162+
break;
163+
}
164+
case "⌘": {
165+
aria = lf("Command");
166+
break;
167+
}
168+
case "⌥": {
169+
aria = lf("Option");
170+
break;
171+
}
172+
}
173+
return <span className="key" aria-label={aria}>{lf("{0}", value)}</span>
174+
}
175+
176+
export default KeyboardControlsHelp;

0 commit comments

Comments
 (0)