Skip to content

Commit 248e903

Browse files
Add global shortcuts to improve keyboard navigation
Adds Ctrl/Cmd+B shortcut that shows a numbered navigation overlay. Integrates with a pxt-microbit change that propagates global-nav actions from the simulator (otherwise they don't take effect when the iframe has focus). Co-authored-by: Grace <145345672+microbit-grace@users.noreply.github.com>
1 parent a299d4a commit 248e903

File tree

9 files changed

+496
-0
lines changed

9 files changed

+496
-0
lines changed

localtypings/pxteditor.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,8 @@ declare namespace pxt.editor {
716716
zoomOut(): void;
717717
resize(): void;
718718
setScale(scale: number): void;
719+
focusWorkspace(): void;
720+
focusToolbox(): void;
719721
}
720722

721723
export interface IFile {
@@ -819,6 +821,7 @@ declare namespace pxt.editor {
819821
extensionsVisible?: boolean;
820822
isMultiplayerGame?: boolean; // Arcade: Does the current project contain multiplayer blocks?
821823
onboarding?: pxt.tour.BubbleStep[];
824+
navigateRegions?: boolean;
822825
feedback?: FeedbackState;
823826
themePickerOpen?: boolean;
824827
}
@@ -1056,6 +1059,8 @@ declare namespace pxt.editor {
10561059
hideLightbox(): void;
10571060
showOnboarding(): void;
10581061
hideOnboarding(): void;
1062+
showNavigateRegions(): void;
1063+
hideNavigateRegions(): void;
10591064
showKeymap(show: boolean): void;
10601065
toggleKeymap(): void;
10611066
signOutGithub(): void;

pxtsim/accessibility.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,31 @@ namespace pxsim.accessibility {
88
elem.setAttribute("tabindex", "0");
99
}
1010

11+
export function getKeyboardShortcutEditorAction(e: KeyboardEvent): pxsim.SimulatorAction | null {
12+
const meta = e.metaKey || e.ctrlKey;
13+
if (e.key === "Escape") {
14+
e.preventDefault();
15+
return "escape"
16+
} else if (e.key === "b" && meta) {
17+
e.preventDefault();
18+
return "navigateregions"
19+
}
20+
return null
21+
}
22+
23+
export function postKeyboardEvent() {
24+
document.addEventListener("keydown", (e) => {
25+
const action = getKeyboardShortcutEditorAction(e)
26+
if (action) {
27+
const message = {
28+
type: "pxtsim",
29+
action
30+
} as pxsim.SimulatorActionMessage;
31+
Runtime.postMessage(message)
32+
}
33+
});
34+
}
35+
1136
export function enableKeyboardInteraction(elem: Element, handlerKeyDown?: () => void, handlerKeyUp?: () => void): void {
1237
if (handlerKeyDown) {
1338
elem.addEventListener('keydown', (e: KeyboardEvent) => {

pxtsim/embed.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ namespace pxsim {
7575
url: string;
7676
}
7777

78+
export type SimulatorAction = "escape" | "navigateregions";
79+
80+
export interface SimulatorActionMessage extends SimulatorMessage {
81+
action: SimulatorAction;
82+
}
83+
7884
export interface SimulatorStateMessage extends SimulatorMessage {
7985
type: "status";
8086
frameid?: string;

theme/common.less

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,11 @@ div.simframe > iframe {
347347
top:0; left: 0; width:100%; height:100%;
348348
}
349349

350+
#boardview:focus-visible {
351+
outline: 3px solid var(--pxt-focus-border);
352+
outline-offset: 3px;
353+
}
354+
350355
.simHeadless {
351356
height: 0 !important;
352357
width: 0 !important;

theme/navigateregions.less

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
.navigate-regions-container {
9+
position: fixed;
10+
top: 0;
11+
left: 0;
12+
right: 0;
13+
bottom: 0;
14+
background-color: rgba(0,0,0,0);
15+
width: 100%;
16+
height: 100%;
17+
z-index: @modalDimmerZIndex;
18+
19+
.region-button {
20+
position: absolute;
21+
background-color: rgba(0, 0, 0, .6);
22+
border-radius: 0;
23+
border: 3px solid white;
24+
padding: 0;
25+
26+
&.simulator-region {
27+
z-index: 1;
28+
}
29+
30+
&.simulator-collapsed {
31+
border-start-end-radius: 100px;
32+
border-end-end-radius: 100px;
33+
}
34+
35+
&:focus-visible {
36+
background-color: rgba(0, 0, 0, .3);
37+
38+
div {
39+
border: 5px solid white;
40+
background-color: black;
41+
}
42+
43+
p {
44+
font-weight: bold;
45+
}
46+
}
47+
48+
div {
49+
background-color: rgba(0, 0, 0, .6);
50+
border: 2px solid white;
51+
width: fit-content;
52+
margin: auto;
53+
padding-right: 1em;
54+
padding-left: 1em;
55+
border-radius: 5px;
56+
57+
@media only screen and (max-width: @largestMobileScreen) {
58+
padding-right: 0.5em;
59+
padding-left: 0.5em;
60+
}
61+
}
62+
p {
63+
color: white;
64+
font-size: 2rem;
65+
66+
@media only screen and (max-width: @largestTabletScreen),
67+
only screen and (max-height: @tallEditorBreakpoint) and (min-width: @largestMobileScreen) {
68+
font-size: 1.5rem;
69+
}
70+
}
71+
}
72+
}

theme/pxt.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
@import 'accessibility';
3333
@import 'highcontrast';
3434
@import 'greenscreen';
35+
@import 'navigateregions';
3536

3637
@import 'extension';
3738
@import 'extensionErrors';

webapp/src/app.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import Util = pxt.Util;
8080
import { HintManager } from "./hinttooltip";
8181
import { mergeProjectCode, appendTemporaryAssets } from "./mergeProjects";
8282
import { Tour } from "./components/onboarding/Tour";
83+
import { NavigateRegionsOverlay } from "./components/NavigateRegionsOverlay";
8384
import { parseTourStepsAsync } from "./onboarding";
8485
import { initGitHubDb } from "./idbworkspace";
8586
import { BlockDefinition, CategoryNameID } from "./toolbox";
@@ -5247,6 +5248,21 @@ export class ProjectView
52475248

52485249
}
52495250

5251+
///////////////////////////////////////////////////////////
5252+
//////////// Navigate regions /////////////
5253+
///////////////////////////////////////////////////////////
5254+
5255+
hideNavigateRegions() {
5256+
this.setState({ navigateRegions: false });
5257+
}
5258+
5259+
showNavigateRegions() {
5260+
const dialog = Array.from(document.querySelectorAll("[role=dialog]")).find(dialog => (dialog as any).checkVisibility());
5261+
if (!dialog) {
5262+
this.setState(state => state.home ? state : { navigateRegions: true })
5263+
}
5264+
}
5265+
52505266
///////////////////////////////////////////////////////////
52515267
//////////// Key map /////////////
52525268
///////////////////////////////////////////////////////////
@@ -5507,6 +5523,7 @@ export class ProjectView
55075523
{lightbox ? <sui.Dimmer isOpen={true} active={lightbox} portalClassName={'tutorial'} className={'ui modal'}
55085524
shouldFocusAfterRender={false} closable={true} onClose={this.hideLightbox} /> : undefined}
55095525
{this.state.onboarding && <Tour tourSteps={this.state.onboarding} onClose={this.hideOnboarding} />}
5526+
{accessibleBlocks && this.state.navigateRegions && <NavigateRegionsOverlay parent={this}/>}
55105527
{this.state.themePickerOpen && <ThemePickerModal themes={this.themeManager.getAllColorThemes()} onThemeClicked={theme => this.setColorThemeById(theme?.id, true)} onClose={this.hideThemePicker} />}
55115528
</div>
55125529
);

webapp/src/blocks.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,35 @@ export class Editor extends toolboxeditor.ToolboxEditor {
585585
return true
586586
}
587587
});
588+
589+
const triggerEditorAction = (action: pxsim.SimulatorAction) => {
590+
switch (action) {
591+
case "escape": {
592+
this.parent.setSimulatorFullScreen(false);
593+
return;
594+
}
595+
case "navigateregions" : {
596+
this.parent.showNavigateRegions();
597+
return
598+
}
599+
}
600+
}
601+
602+
const simulatorOrigins = [
603+
window.location.origin,
604+
// Simulator deployed origin.
605+
"https://trg-microbit.userpxt.io"
606+
]
607+
window.addEventListener("message", (e: MessageEvent) => {
608+
// Listen to simulator iframe keydown post messages.
609+
if (simulatorOrigins.includes(e.origin) && e.data.type === "pxtsim") {
610+
triggerEditorAction((e.data as pxsim.SimulatorActionMessage).action)
611+
}
612+
}, false)
613+
document.addEventListener("keydown", (e: KeyboardEvent) => {
614+
const action = pxsim.accessibility.getKeyboardShortcutEditorAction(e)
615+
triggerEditorAction(action)
616+
});
588617
}
589618
}
590619

0 commit comments

Comments
 (0)