From 5ec85090d347ae49a0a678357b81d509ff32c245 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 11 Jun 2026 10:06:00 +0100 Subject: [PATCH 1/2] Update Blockly to v13.0.0-beta.9 Remove the workaround for keyboard disconnect on shadows now that's in Blockly proper. Various screen reader and keyboard navigation improvements. One important change is that the collapse icon on blocks is now reachable via the keyboard. --- package.json | 6 +++--- webapp/src/blocks.tsx | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 85b3363cf9f2..862f548d4968 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@zip.js/zip.js": "2.4.20", "adm-zip": "^0.5.12", "axios": "^1.12.2", - "blockly": "13.0.0-beta.8", + "blockly": "13.0.0-beta.9", "browserify": "17.0.0", "chai": "^3.5.0", "chalk": "^4.1.2", @@ -151,10 +151,10 @@ }, "overrides": { "@blockly/field-grid-dropdown": { - "blockly": "13.0.0-beta.8" + "blockly": "13.0.0-beta.9" }, "@blockly/plugin-workspace-search": { - "blockly": "13.0.0-beta.8" + "blockly": "13.0.0-beta.9" }, "combine-source-map": { "source-map": "0.4.4" diff --git a/webapp/src/blocks.tsx b/webapp/src/blocks.tsx index a66955cfde8f..e464b5d1bf6f 100644 --- a/webapp/src/blocks.tsx +++ b/webapp/src/blocks.tsx @@ -705,11 +705,6 @@ export class Editor extends toolboxeditor.ToolboxEditor { if (focused instanceof Blockly.BlockSvg && shouldDuplicateOnDrag(focused)) { return !workspace.isReadOnly() && !workspace.isDragging(); } - // Workaround: https://github.com/RaspberryPiFoundation/blockly/issues/9963 - if (focused instanceof Blockly.BlockSvg && focused.isShadow()) { - workspace.getAudioManager().playErrorBeep(); - return false; - } return disconnectShortcut.preconditionFn!(workspace, scope); }, callback: (workspace, e, shortcut, scope) => { From b79091e20f5babddb69e9c441a46f04d461b3929 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 11 Jun 2026 11:51:14 +0100 Subject: [PATCH 2/2] Focus the block when expanding a collapsed block via the expand icon The custom expand icon's callback moved focus to the function collapse button, which only exists on function definitions. For other blocks focus was lost when the expand icon re-rendered away. Fall back to focusing the block itself. Rename maybeFocusMutatorButton to maybeMoveFocusFromButton now that it also focuses blocks and inputs, not just mutator buttons. --- pxtblocks/builtins/functions.ts | 4 ++-- pxtblocks/composableMutations.ts | 8 ++++---- pxtblocks/monkeyPatches/blockSvg.ts | 6 ++++-- pxtblocks/plugins/arrays/createList.ts | 4 ++-- .../plugins/functions/blocks/functionDefinitionBlock.ts | 4 ++-- pxtblocks/plugins/logic/ifElse.ts | 8 ++++---- pxtblocks/plugins/text/join.ts | 4 ++-- pxtblocks/utils.ts | 8 ++++++-- 8 files changed, 26 insertions(+), 20 deletions(-) diff --git a/pxtblocks/builtins/functions.ts b/pxtblocks/builtins/functions.ts index 3a24c63a10ea..a24b54bf6d9c 100644 --- a/pxtblocks/builtins/functions.ts +++ b/pxtblocks/builtins/functions.ts @@ -7,7 +7,7 @@ import { FieldProcedure } from "../fields"; import { cachedBlockInfo, setOutputCheck } from "../loader"; import { domToWorkspaceNoEvents } from "../importer"; import { FieldImageNoText } from "../fields/field_imagenotext"; -import { maybeFocusMutatorButton } from "../utils"; +import { maybeMoveFocusFromButton } from "../utils"; export function initFunctions() { const msg = Blockly.Msg; @@ -356,7 +356,7 @@ function initReturnStatement(b: Blockly.Block) { b.setInputsInline(true); if (userTriggered) { - maybeFocusMutatorButton(buttonToFocus?.fieldRow[0]); + maybeMoveFocusFromButton(buttonToFocus?.fieldRow[0]); } } diff --git a/pxtblocks/composableMutations.ts b/pxtblocks/composableMutations.ts index 5706ad690765..43176773ad21 100644 --- a/pxtblocks/composableMutations.ts +++ b/pxtblocks/composableMutations.ts @@ -9,7 +9,7 @@ import { DRAGGABLE_PARAM_INPUT_PREFIX, getBlocklyCheckForType, setVarFieldValue import { UpdateBeforeRenderMixin } from "./plugins/renderer"; import { FieldImageNoText } from "./fields/field_imagenotext"; import { setDuplicateOnDrag } from "./plugins/duplicateOnDrag"; -import { maybeFocusMutatorButton } from "./utils"; +import { maybeMoveFocusFromButton } from "./utils"; export interface ComposableMutation { // Set to save mutations. Should return an XML element @@ -69,7 +69,7 @@ export function initVariableArgsBlock(b: Blockly.Block, handlerArgs: pxt.blocks. if (currentlyVisible >= handlerArgs.length) { i.removeField("_HANDLER_ADD"); if (userTriggered) { - maybeFocusMutatorButton(fieldToFocus); + maybeMoveFocusFromButton(fieldToFocus); } } else if (actuallyVisible >= handlerArgs.length) { @@ -269,7 +269,7 @@ export function initExpandableBlock(info: pxtc.BlocksInfo, b: Blockly.Block, def if (buttonToFocus.fieldRow.length > 1) { field = delta < 0 ? field : buttonToFocus.fieldRow[1]; } - maybeFocusMutatorButton(field); + maybeMoveFocusFromButton(field); buttonToFocus = null; } }); @@ -503,7 +503,7 @@ export function initVariableReporterArgs(b: Blockly.Block, handlerArgs: pxt.bloc setTimeout(() => { populateArguments(); if (userTriggered && handlerArgs.length === state.getNumber(numVisibleAttr)) { - maybeFocusMutatorButton(inputToFocus.connection?.targetBlock() as Blockly.BlockSvg) + maybeMoveFocusFromButton(inputToFocus.connection?.targetBlock() as Blockly.BlockSvg) } }, 0); } diff --git a/pxtblocks/monkeyPatches/blockSvg.ts b/pxtblocks/monkeyPatches/blockSvg.ts index ef6815928288..76aacacd2c3d 100644 --- a/pxtblocks/monkeyPatches/blockSvg.ts +++ b/pxtblocks/monkeyPatches/blockSvg.ts @@ -1,7 +1,7 @@ import * as Blockly from "blockly"; import { FieldImageNoText } from "../fields/field_imagenotext"; import { ConstantProvider } from "../plugins/renderer/constants"; -import { maybeFocusMutatorButton } from "../utils"; +import { maybeMoveFocusFromButton } from "../utils"; export function monkeyPatchBlockSvg() { const oldSetCollapsed = Blockly.BlockSvg.prototype.setCollapsed; @@ -17,8 +17,10 @@ export function monkeyPatchBlockSvg() { if (image) { input.appendField(new FieldImageNoText(image, 24, 24, lf("Expand block"), () => { this.setCollapsed(false); + // Functions keep focus on their collapse button; other blocks + // have no such button so focus the block itself instead. const collapseBtn = this.inputList.find(i => i.name === "function_collapse")?.fieldRow[0]; - maybeFocusMutatorButton(collapseBtn); + maybeMoveFocusFromButton(collapseBtn ?? this); }, false)); } } diff --git a/pxtblocks/plugins/arrays/createList.ts b/pxtblocks/plugins/arrays/createList.ts index 92c15b51b711..bee9fbb7d823 100644 --- a/pxtblocks/plugins/arrays/createList.ts +++ b/pxtblocks/plugins/arrays/createList.ts @@ -3,7 +3,7 @@ import { FUNCTION_CALL_OUTPUT_BLOCK_TYPE } from "../functions/constants"; import { CommonFunctionBlock } from "../functions/commonFunctionMixin"; import { InlineSvgsExtensionBlock } from "../functions"; import { FieldImageNoText } from "../../fields/field_imagenotext"; -import { maybeFocusMutatorButton } from "../../utils"; +import { maybeMoveFocusFromButton } from "../../utils"; type ListCreateMixinType = typeof LIST_CREATE_MIXIN; @@ -188,7 +188,7 @@ const LIST_CREATE_MIXIN = { if (this.buttons.fieldRow.length > 1) { field = this.delta < 0 ? field : this.buttons.fieldRow[1]; } - maybeFocusMutatorButton(field); + maybeMoveFocusFromButton(field); this.buttons = null; this.delta = 0; } diff --git a/pxtblocks/plugins/functions/blocks/functionDefinitionBlock.ts b/pxtblocks/plugins/functions/blocks/functionDefinitionBlock.ts index 9d567ac333ae..5f1d2ddb8192 100644 --- a/pxtblocks/plugins/functions/blocks/functionDefinitionBlock.ts +++ b/pxtblocks/plugins/functions/blocks/functionDefinitionBlock.ts @@ -23,7 +23,7 @@ import { COLLAPSE_IMAGE_DATAURI } from "../svgs"; import { ArgumentReporterBlock } from "./argumentReporterBlocks"; import { setDuplicateOnDrag } from "../../duplicateOnDrag"; import { FieldImageNoText } from "../../../fields/field_imagenotext"; -import { maybeFocusMutatorButton } from "../../../utils"; +import { maybeMoveFocusFromButton } from "../../../utils"; interface FunctionDefinitionMixin extends CommonFunctionMixin { createArgumentReporter_(arg: FunctionArgument): ArgumentReporterBlock; @@ -203,7 +203,7 @@ Blockly.Blocks[FUNCTION_DEFINITION_BLOCK_TYPE] = { () => { this.setCollapsed(true); const expandBtn = this.inputList.find(i => i.name === Blockly.constants.COLLAPSED_INPUT_NAME)?.fieldRow[1]; - maybeFocusMutatorButton(expandBtn); + maybeMoveFocusFromButton(expandBtn); }, false ) diff --git a/pxtblocks/plugins/logic/ifElse.ts b/pxtblocks/plugins/logic/ifElse.ts index a2cb8420fa4c..fad58b3fd6ff 100644 --- a/pxtblocks/plugins/logic/ifElse.ts +++ b/pxtblocks/plugins/logic/ifElse.ts @@ -1,7 +1,7 @@ import * as Blockly from "blockly"; import { InlineSvgsExtensionBlock } from "../functions"; import { FieldImageNoText } from "../../fields/field_imagenotext"; -import { maybeFocusMutatorButton } from "../../utils"; +import { maybeMoveFocusFromButton } from "../../utils"; type IfElseMixinType = typeof IF_ELSE_MIXIN; @@ -88,7 +88,7 @@ const IF_ELSE_MIXIN = { // Focus the condition of the last elseif branch, fallback to the condition of the if. const focusIndex = this.elseifCount_; const inputName = 'IF' + focusIndex; - maybeFocusMutatorButton(this.getInput(inputName)?.connection?.targetBlock() as Blockly.BlockSvg); + maybeMoveFocusFromButton(this.getInput(inputName)?.connection?.targetBlock() as Blockly.BlockSvg); }, addElseIf_: function (this: IfElseBlock) { Blockly.utils.aria.announceDynamicAriaState(pxt.Util.lf("Else if branch added.")); @@ -106,7 +106,7 @@ const IF_ELSE_MIXIN = { // Focus the condition of the branch before the one just removed. const prevIndex = arg - 1; const inputName = 'IF' + prevIndex; - maybeFocusMutatorButton(this.getInput(inputName)?.connection?.targetBlock() as Blockly.BlockSvg); + maybeMoveFocusFromButton(this.getInput(inputName)?.connection?.targetBlock() as Blockly.BlockSvg); }, update_: function (this: IfElseBlock, update: () => void, arg?: number) { Blockly.Events.setGroup(true); @@ -196,7 +196,7 @@ const IF_ELSE_MIXIN = { if (that.elseCount_ == 0) { that.addElse_(); // Focus the else 'remove branch' button (keyboard users only). - maybeFocusMutatorButton(that.getInput('ELSEBUTTONS')?.fieldRow[0]); + maybeMoveFocusFromButton(that.getInput('ELSEBUTTONS')?.fieldRow[0]); } else { if (!that.elseifCount_) that.elseifCount_ = 0; that.addElseIf_(); diff --git a/pxtblocks/plugins/text/join.ts b/pxtblocks/plugins/text/join.ts index 061657bf020b..d1b4e8ade1db 100644 --- a/pxtblocks/plugins/text/join.ts +++ b/pxtblocks/plugins/text/join.ts @@ -1,7 +1,7 @@ import * as Blockly from "blockly"; import { InlineSvgsExtensionBlock } from "../functions"; import { FieldImageNoText } from "../../fields/field_imagenotext"; -import { maybeFocusMutatorButton } from "../../utils"; +import { maybeMoveFocusFromButton } from "../../utils"; type TextJoinMixinType = typeof TEXT_JOIN_MUTATOR_MIXIN; @@ -108,7 +108,7 @@ const TEXT_JOIN_MUTATOR_MIXIN = { if (this.buttons.fieldRow.length > 1) { field = this.delta < 0 ? field : this.buttons.fieldRow[1]; } - maybeFocusMutatorButton(field); + maybeMoveFocusFromButton(field); this.buttons = null; this.delta = 0; } diff --git a/pxtblocks/utils.ts b/pxtblocks/utils.ts index 328b69909601..dc99c3f786c0 100644 --- a/pxtblocks/utils.ts +++ b/pxtblocks/utils.ts @@ -1,7 +1,11 @@ import * as Blockly from "blockly"; import { FieldImageNoText } from "./fields/field_imagenotext"; -export const maybeFocusMutatorButton = (node: Blockly.IFocusableNode | undefined): void => { +// Moves focus to the given node, but only when focus currently sits on one of +// our FieldImageNoText buttons (the +/-/expand/collapse icons) or nowhere. This +// keeps focus sensible after such a button re-renders away the element that had +// focus, without stealing focus if it has moved elsewhere. +export const maybeMoveFocusFromButton = (node: Blockly.IFocusableNode | undefined): void => { const focusManager = Blockly.getFocusManager(); const currentlyFocusedNode = focusManager.getFocusedNode(); if ( @@ -10,4 +14,4 @@ export const maybeFocusMutatorButton = (node: Blockly.IFocusableNode | undefined ) { focusManager.focusNode(node); } -} \ No newline at end of file +}