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..07493286ad28 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 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); }