From 64a2013a8aa67474773f5931b05ea1632aaf9146 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Mon, 9 Mar 2026 11:32:01 +0000 Subject: [PATCH 1/2] Add keyboardcontrols=1 flag to enable testing "on by default" This is a temporary flag to enable trialing enabling keyboard controls by default. Micro:bit Educational Foundation are going to do that in their tools that embed MakeCode and provide feedback. This might then be a route to simply enabling them in the next release. I've implemented this by flipping the default based on the flag. Now that the user isn't naturally exposed to the help by enabling keyboard controls, we show the same help toast as Blockly when the workspace is focused from the skip link. It might be nice to do this in other scenarios (e.g. first tab to the workspace) but that is a riskier change. There are still some arguable downsides from this flag that we might want to address before making it the default: - It enables "passive focus" dashed outline around blocks. The purpose of this is to remind the user of the workspace context (which is where e.g. a toolbox block insert will be relative to). But this is more relevant to keyboard users. Passive focus is enabled based on Blocky's logic about whether keyboard navigation has been used which can be a bit hard to predict as a user. - The active area indicator for the workspace (black focus ring) is also shown for all users. When a user clicks on the workspace background we also indicate that the workspace itself has focus via the yellow ring (as per blocks). We have in the past proposed making this conditional on keyboard navigation being in use (so it feels like focus-visible CSS). Both intentionally unaddressed for now so we can: 1. reduce risk of this change, which is needed in live MakeCode 2. gather feedback specifically from folks who notice these changes Another aspect we intend to monitor is feedback from folks who might unintentionally trigger shortcuts. For telemetry I've intentionally not triggered an event from the default setting and I've tagged the event source for subsequent changes with the fact it was on by default. --- localtypings/pxteditor.d.ts | 2 +- pxtlib/auth.ts | 2 +- webapp/src/accessibility.tsx | 3 ++- webapp/src/app.tsx | 9 ++++++++- webapp/src/auth.ts | 8 ++++++-- webapp/src/blocks.tsx | 14 ++++++++++++++ webapp/src/core.ts | 7 +++++++ 7 files changed, 39 insertions(+), 6 deletions(-) diff --git a/localtypings/pxteditor.d.ts b/localtypings/pxteditor.d.ts index f89e4e79db6d..48389184f0fd 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -915,7 +915,7 @@ declare namespace pxt.editor { forceUpdate(): void; reloadEditor(): void; - openBlocks(): void; + openBlocks(showKeyboardControlsHint?: boolean): void; openJavaScript(giveFocusOnLoading?: boolean): void; openPython(giveFocusOnLoading?: boolean): void; openAssets(): void; diff --git a/pxtlib/auth.ts b/pxtlib/auth.ts index 5e9987c01c29..9e34f509d115 100644 --- a/pxtlib/auth.ts +++ b/pxtlib/auth.ts @@ -73,7 +73,7 @@ namespace pxt.auth { export const DEFAULT_USER_PREFERENCES: () => UserPreferences = () => ({ language: pxt.appTarget.appTheme.defaultLocale, highContrast: false, - accessibleBlocks: false, + accessibleBlocks: undefined, // Defaulted at read time depending on flag colorThemeIds: {}, // Will lookup pxt.appTarget.appTheme.defaultColorTheme for active target reader: "", skillmap: { mapProgress: {}, completedTags: {} }, diff --git a/webapp/src/accessibility.tsx b/webapp/src/accessibility.tsx index 383e7fab3c07..04220036d351 100644 --- a/webapp/src/accessibility.tsx +++ b/webapp/src/accessibility.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import * as auth from "./auth"; +import * as core from "./core"; import * as data from "./data"; import * as sui from "./sui"; @@ -34,7 +35,7 @@ export class EditorAccessibilityMenu extends data.Component) { - this.props.parent.openBlocks(); + this.props.parent.openBlocks(core.isKeyboardControlsByDefault()); } openJavaScript() { diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index e340095a156a..2419b21718f8 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -720,9 +720,13 @@ export class ProjectView pxt.shell.setEditorLanguagePref("js"); } - openBlocks() { + openBlocks(showKeyboardControlsHint?: boolean) { if (this.updatingEditorFile) return; // already transitioning + if (showKeyboardControlsHint) { + this.blocksEditor.pendingKeyboardControlsHint = true; + } + if (this.isBlocksActive()) { if (this.state.embedSimView) this.setState({ embedSimView: false }); // This timeout prevents key events from being handled by Blockly's keyboard @@ -5309,6 +5313,9 @@ export class ProjectView } async toggleAccessibleBlocks(eventSource: string) { + if (core.isKeyboardControlsByDefault()) { + eventSource += "-on-by-default"; + } const nextEnabled = !this.getData(auth.ACCESSIBLE_BLOCKS); if (nextEnabled) { pxt.storage.setLocal("onboardAccessibleBlocks", "1") diff --git a/webapp/src/auth.ts b/webapp/src/auth.ts index 14110ed00de1..859cdaed4113 100644 --- a/webapp/src/auth.ts +++ b/webapp/src/auth.ts @@ -117,7 +117,11 @@ class AuthClient extends pxt.auth.AuthClient { // Identity not available, read from local storage switch (path) { case HIGHCONTRAST: return /^true$/i.test(pxt.storage.getLocal(HIGHCONTRAST)); - case ACCESSIBLE_BLOCKS: return /^true$/i.test(pxt.storage.getLocal(ACCESSIBLE_BLOCKS)); + case ACCESSIBLE_BLOCKS: { + const stored = pxt.storage.getLocal(ACCESSIBLE_BLOCKS); + if (stored == null) return core.isKeyboardControlsByDefault(); + return /^true$/i.test(stored); + } case COLOR_THEME_IDS: return pxt.U.jsonTryParse(pxt.storage.getLocal(COLOR_THEME_IDS)) as pxt.auth.ColorThemeIdsState; case LANGUAGE: return pxt.storage.getLocal(LANGUAGE); case READER: return pxt.storage.getLocal(READER); @@ -134,7 +138,7 @@ class AuthClient extends pxt.auth.AuthClient { switch (field) { case FIELD_USER_PREFERENCES: return { ...state.preferences }; case FIELD_HIGHCONTRAST: return state.preferences?.highContrast ?? pxt.auth.DEFAULT_USER_PREFERENCES().highContrast; - case FIELD_KEYBOARD_CONTROLS: return state.preferences?.accessibleBlocks ?? pxt.auth.DEFAULT_USER_PREFERENCES().accessibleBlocks; + case FIELD_KEYBOARD_CONTROLS: return state.preferences?.accessibleBlocks ?? core.isKeyboardControlsByDefault(); case FIELD_COLOR_THEME_IDS: return state.preferences?.colorThemeIds ?? pxt.auth.DEFAULT_USER_PREFERENCES().colorThemeIds; case FIELD_LANGUAGE: return state.preferences?.language ?? pxt.auth.DEFAULT_USER_PREFERENCES().language; case FIELD_READER: return state.preferences?.reader ?? pxt.auth.DEFAULT_USER_PREFERENCES().reader; diff --git a/webapp/src/blocks.tsx b/webapp/src/blocks.tsx index 8cf82f6dddac..330c8d43b5b3 100644 --- a/webapp/src/blocks.tsx +++ b/webapp/src/blocks.tsx @@ -60,6 +60,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { loadingXmlPromise: Promise; compilationResult: pxtblockly.BlockCompilationResult; shouldFocusWorkspace = false; + pendingKeyboardControlsHint = false; functionsDialog: CreateFunctionDialog = null; showCategories: boolean = true; @@ -1019,9 +1020,22 @@ export class Editor extends toolboxeditor.ToolboxEditor { if (accessibleBlocksEnabled) { (this.editor.getSvgGroup() as SVGElement).focus(); Blockly.hideChaff(); + + if (this.pendingKeyboardControlsHint) { + this.pendingKeyboardControlsHint = false; + this.showKeyboardControlsHint(); + } } } + showKeyboardControlsHint() { + if (!this.editor || !Blockly.Msg["HELP_PROMPT"]) return; + const isMac = navigator.platform.startsWith("Mac"); + const shortcut = isMac ? "⌘ /" : lf("Ctrl") + " + /"; + const message = Blockly.Msg["HELP_PROMPT"].replace("%1", shortcut); + Blockly.Toast.show(this.editor, { message, id: "helpHint", oncePerSession: true }); + } + hasUndo() { const undoStack = this.editor?.getUndoStack(); const redoStack = this.editor?.getRedoStack(); diff --git a/webapp/src/core.ts b/webapp/src/core.ts index ca2afb6b34f1..6c5fe17155ec 100644 --- a/webapp/src/core.ts +++ b/webapp/src/core.ts @@ -343,6 +343,13 @@ export function getHighContrastOnce(): boolean { return ThemeManager.isCurrentThemeHighContrast(); } +/** + * Returns true if keyboard controls should be enabled by default. + */ +export function isKeyboardControlsByDefault(): boolean { + return /keyboardcontrols=1/i.test(window.location.href); +} + export async function toggleAccessibleBlocks(eventSource: string) { await setAccessibleBlocks(!data.getData(auth.ACCESSIBLE_BLOCKS), eventSource); } From f9239cbad9268bf4f20afb9638dae94202c9ea09 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 12 Mar 2026 16:32:38 +0000 Subject: [PATCH 2/2] keyboardcontrols=1 flag tweaks based on PR feedback: - use defaultOn as a telemetry dimension - use BrowserUtils.isMac() --- webapp/src/auth.ts | 1 + webapp/src/blocks.tsx | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/auth.ts b/webapp/src/auth.ts index 859cdaed4113..cd7f810641c5 100644 --- a/webapp/src/auth.ts +++ b/webapp/src/auth.ts @@ -254,6 +254,7 @@ export async function setAccessibleBlocksPrefAsync(accessibleBlocks: boolean, ev "auth.setAccessibleBlocks", { enabling: accessibleBlocks ? "true" : "false", + defaultOn: core.isKeyboardControlsByDefault() ? "true" : "false", eventSource: eventSource, local: !cli ? "true" : "false" } diff --git a/webapp/src/blocks.tsx b/webapp/src/blocks.tsx index 330c8d43b5b3..14501930ec0a 100644 --- a/webapp/src/blocks.tsx +++ b/webapp/src/blocks.tsx @@ -1030,8 +1030,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { showKeyboardControlsHint() { if (!this.editor || !Blockly.Msg["HELP_PROMPT"]) return; - const isMac = navigator.platform.startsWith("Mac"); - const shortcut = isMac ? "⌘ /" : lf("Ctrl") + " + /"; + const shortcut = pxt.BrowserUtils.isMac() ? "⌘ /" : lf("Ctrl") + " + /"; const message = Blockly.Msg["HELP_PROMPT"].replace("%1", shortcut); Blockly.Toast.show(this.editor, { message, id: "helpHint", oncePerSession: true }); }