From 0c6c86170ceaccdb92153c69b3d2fc170bf3f5aa Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 12 Mar 2026 17:27:12 +0000 Subject: [PATCH 1/2] Add keyboardcontrols=1 flag to enable testing "on by default" (#11185) * 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. * keyboardcontrols=1 flag tweaks based on PR feedback: - use defaultOn as a telemetry dimension - use BrowserUtils.isMac() --------- Co-authored-by: Richard Knoll --- localtypings/pxteditor.d.ts | 2 +- pxtlib/auth.ts | 2 +- webapp/src/accessibility.tsx | 3 ++- webapp/src/app.tsx | 9 ++++++++- webapp/src/auth.ts | 9 +++++++-- webapp/src/blocks.tsx | 13 +++++++++++++ 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 89849714e1fc..f4741c95c3f4 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -914,7 +914,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 20b3fed7bd34..a0c6321f8bc5 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -716,9 +716,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 @@ -5252,6 +5256,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..cd7f810641c5 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; @@ -250,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 47b1eb56ba02..1c1900ab5cfd 100644 --- a/webapp/src/blocks.tsx +++ b/webapp/src/blocks.tsx @@ -59,6 +59,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { loadingXmlPromise: Promise; compilationResult: pxtblockly.BlockCompilationResult; shouldFocusWorkspace = false; + pendingKeyboardControlsHint = false; functionsDialog: CreateFunctionDialog = null; showCategories: boolean = true; @@ -997,9 +998,21 @@ 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 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 }); + } + 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 0727867eaebb62fba603ee35e0cd084828ac4195 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 19 Mar 2026 19:23:40 +0000 Subject: [PATCH 2/2] Keyboard controls events: remove suffix now we added a dimension (#11198) This should have been part of #11185 to fully address the feedback (I added the dimension but didn't remove this alternative approach). Co-authored-by: Richard Knoll --- webapp/src/app.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index a0c6321f8bc5..07493286ad28 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -5256,9 +5256,6 @@ 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")