Skip to content

Commit 5ede095

Browse files
microbit-matt-hillsdonmicrobit-gracethsparksriknoll
authored
Add global shortcuts to improve keyboard navigation (#10570)
* 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> * Remove debug log * RTL fixes for overlay * Feeback: sim origin, remove useEffect, defined checks * Pull up checkVisible polyfill so it's always available * Move sim message handling to correct location I've moved the key handling too as they share code. Kept it gated on accessible blocks setting, though this isn't really necessary if we're happy to enable more widely. * Comment typo fix * Use theme colors; fix high contrast z-index bug * Fix missed theme color Co-authored-by: Thomas Sparks <69657545+thsparks@users.noreply.github.com> --------- Co-authored-by: Grace <145345672+microbit-grace@users.noreply.github.com> Co-authored-by: Thomas Sparks <69657545+thsparks@users.noreply.github.com> Co-authored-by: Richard Knoll <riknoll@users.noreply.github.com>
1 parent ab510f8 commit 5ede095

File tree

10 files changed

+515
-3
lines changed

10 files changed

+515
-3
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;

pxtblocks/loader.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { initContextMenu } from "./contextMenu";
2424
import { renderCodeCard } from "./codecardRenderer";
2525
import { FieldDropdown } from "./fields/field_dropdown";
2626
import { setDraggableShadowBlocks, setDuplicateOnDrag, setDuplicateOnDragStrategy } from "./plugins/duplicateOnDrag";
27-
import { applyPolyfills } from "./polyfills";
2827
import { initCopyPaste } from "./copyPaste";
2928

3029
export const DRAGGABLE_PARAM_INPUT_PREFIX = "HANDLER_DRAG_PARAM_";
@@ -597,8 +596,6 @@ function init(blockInfo: pxtc.BlocksInfo) {
597596
if (blocklyInitialized) return;
598597
blocklyInitialized = true;
599598

600-
applyPolyfills();
601-
602599
initFieldEditors();
603600
initContextMenu();
604601
initOnStart();

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 getGlobalAction(e: KeyboardEvent): pxsim.GlobalAction | 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 = getGlobalAction(e)
26+
if (action) {
27+
const message = {
28+
type: "action",
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ namespace pxsim {
7575
url: string;
7676
}
7777

78+
export type GlobalAction = "escape" | "navigateregions";
79+
80+
export interface SimulatorActionMessage extends SimulatorMessage {
81+
type: "action";
82+
action: GlobalAction;
83+
}
84+
7885
export interface SimulatorStateMessage extends SimulatorMessage {
7986
type: "status";
8087
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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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: var(--pxt-neutral-alpha0);
15+
width: 100%;
16+
height: 100%;
17+
z-index: @modalDimmerZIndex;
18+
19+
.region-button {
20+
position: absolute;
21+
background-color: var(--pxt-neutral-alpha50);
22+
border-radius: 0;
23+
border: 3px solid var(--pxt-neutral-background1);
24+
padding: 0;
25+
26+
&.simulator-region {
27+
// Must be more than the z-index of 1 that high contrast mode adds to all buttons.
28+
z-index: 2 !important;
29+
}
30+
31+
&.simulator-collapsed {
32+
border-start-end-radius: 100px;
33+
border-end-end-radius: 100px;
34+
}
35+
36+
&:focus-visible {
37+
background-color: var(--pxt-neutral-alpha20);
38+
39+
div {
40+
border-width: 5px;
41+
background-color: var(--pxt-neutral-foreground1);
42+
}
43+
44+
p {
45+
font-weight: bold;
46+
}
47+
}
48+
49+
50+
51+
div {
52+
background-color: var(--pxt-neutral-alpha80);
53+
border: 2px solid var(--pxt-secondary-foreground);
54+
width: 3.125em;
55+
56+
margin: auto;
57+
border-radius: 5px;
58+
59+
@media only screen and (max-width: @largestMobileScreen) {
60+
padding-right: 0.5em;
61+
padding-left: 0.5em;
62+
}
63+
}
64+
p {
65+
color: var(--pxt-neutral-background1);
66+
font-size: 2rem;
67+
68+
@media only screen and (max-width: @largestTabletScreen),
69+
only screen and (max-height: @tallEditorBreakpoint) and (min-width: @largestMobileScreen) {
70+
font-size: 1.5rem;
71+
}
72+
}
73+
}
74+
75+
}

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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,20 @@ 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";
8687
import { FeedbackModal } from "../../react-common/components/controls/Feedback/Feedback";
8788
import { ThemeManager } from "../../react-common/components/theming/themeManager";
89+
import { applyPolyfills } from "./polyfills";
8890

8991
pxt.blocks.requirePxtBlockly = () => pxtblockly as any;
9092
pxt.blocks.requireBlockly = () => Blockly;
9193
pxt.blocks.registerFieldEditor = (selector, proto, validator) => pxtblockly.registerFieldEditor(selector, proto, validator);
9294

9395
pxsim.util.injectPolyphils();
96+
applyPolyfills();
9497

9598
let theEditor: ProjectView;
9699
let hash: { cmd: string, arg: string };
@@ -236,6 +239,7 @@ export class ProjectView
236239
this.exitTutorial = this.exitTutorial.bind(this);
237240
this.setEditorOffset = this.setEditorOffset.bind(this);
238241
this.resetTutorialTemplateCode = this.resetTutorialTemplateCode.bind(this);
242+
this.initGlobalActionHandlers();
239243
this.initSimulatorMessageHandlers();
240244
this.showThemePicker = this.showThemePicker.bind(this);
241245
this.hideThemePicker = this.hideThemePicker.bind(this);
@@ -303,10 +307,39 @@ export class ProjectView
303307
};
304308
simulator.driver.postMessage(playerOneConnectedMsg);
305309
}
310+
} else if (msg.type === "action") {
311+
const { action } = msg as pxsim.SimulatorActionMessage;
312+
this.runGlobalAction(action);
306313
}
307314
}, false);
308315
}
309316

317+
private initGlobalActionHandlers() {
318+
document.addEventListener("keydown", (e: KeyboardEvent) => {
319+
const action = pxsim.accessibility.getGlobalAction(e)
320+
this.runGlobalAction(action)
321+
});
322+
}
323+
324+
/**
325+
* Run a global action based on shortcuts triggered in sim or main window.
326+
*/
327+
private runGlobalAction(action: pxsim.GlobalAction) {
328+
if (!data.getData<boolean>(auth.ACCESSIBLE_BLOCKS)) {
329+
return;
330+
}
331+
switch (action) {
332+
case "escape": {
333+
this.setSimulatorFullScreen(false);
334+
return;
335+
}
336+
case "navigateregions" : {
337+
this.showNavigateRegions();
338+
return
339+
}
340+
}
341+
}
342+
310343
/**
311344
* Add Github extensions **without** conflict resolution
312345
*/
@@ -5247,6 +5280,21 @@ export class ProjectView
52475280

52485281
}
52495282

5283+
///////////////////////////////////////////////////////////
5284+
//////////// Navigate regions /////////////
5285+
///////////////////////////////////////////////////////////
5286+
5287+
hideNavigateRegions() {
5288+
this.setState({ navigateRegions: false });
5289+
}
5290+
5291+
showNavigateRegions() {
5292+
const dialog = Array.from(document.querySelectorAll("[role=dialog]")).find(dialog => (dialog as any).checkVisibility());
5293+
if (!dialog) {
5294+
this.setState(state => state.home ? state : { navigateRegions: true })
5295+
}
5296+
}
5297+
52505298
///////////////////////////////////////////////////////////
52515299
//////////// Key map /////////////
52525300
///////////////////////////////////////////////////////////
@@ -5507,6 +5555,7 @@ export class ProjectView
55075555
{lightbox ? <sui.Dimmer isOpen={true} active={lightbox} portalClassName={'tutorial'} className={'ui modal'}
55085556
shouldFocusAfterRender={false} closable={true} onClose={this.hideLightbox} /> : undefined}
55095557
{this.state.onboarding && <Tour tourSteps={this.state.onboarding} onClose={this.hideOnboarding} />}
5558+
{this.state.navigateRegions && <NavigateRegionsOverlay parent={this}/>}
55105559
{this.state.themePickerOpen && <ThemePickerModal themes={this.themeManager.getAllColorThemes()} onThemeClicked={theme => this.setColorThemeById(theme?.id, true)} onClose={this.hideThemePicker} />}
55115560
</div>
55125561
);

0 commit comments

Comments
 (0)