From c7c04ec9ecd84127e760a630a67599ccff51073e Mon Sep 17 00:00:00 2001 From: Mike Harvey <43474485+mikeharv@users.noreply.github.com> Date: Wed, 13 May 2026 16:17:22 -0400 Subject: [PATCH 1/2] fix: labels for multi-statement blocks --- packages/blockly/core/block_aria_composer.ts | 46 ++++++++++++++----- packages/blockly/msg/json/en.json | 3 +- packages/blockly/msg/json/qqq.json | 15 ++++++ packages/blockly/msg/messages.js | 4 ++ packages/blockly/tests/mocha/aria_test.js | 28 +++++++++++ .../tests/mocha/keyboard_movement_test.js | 39 +++++++++++++++- 6 files changed, 121 insertions(+), 14 deletions(-) diff --git a/packages/blockly/core/block_aria_composer.ts b/packages/blockly/core/block_aria_composer.ts index d99e1b33b8c..c29f8be7ce7 100644 --- a/packages/blockly/core/block_aria_composer.ts +++ b/packages/blockly/core/block_aria_composer.ts @@ -229,12 +229,28 @@ export function getInputLabels( verbosity = Verbosity.STANDARD, useCustomLabels = true, ): string[] { - return block.inputList - .filter((input) => input.isVisible()) - .map((input) => { - const customLabel = useCustomLabels ? input.getAriaLabelText() : null; - return customLabel ?? input.getLabel(verbosity); - }); + const visibleInputs = block.inputList.filter((input) => input.isVisible()); + let inputsToLabel = visibleInputs; + + // For terse and standard verbosity levels, if there are multiple statement inputs, + // only include labels up to the first one. + if (verbosity <= Verbosity.STANDARD) { + const statementInputs = visibleInputs.filter( + (input) => input.type === inputTypes.STATEMENT, + ); + + if (statementInputs.length > 1) { + inputsToLabel = visibleInputs.slice( + 0, + visibleInputs.indexOf(statementInputs[0]) + 1, + ); + } + } + + return inputsToLabel.map((input) => { + const customLabel = useCustomLabels ? input.getAriaLabelText() : null; + return customLabel ?? input.getLabel(verbosity); + }); } /** @@ -451,9 +467,7 @@ export function computeMoveLabel( let blockLabel = isMoveStart ? local.getSourceBlock().getStackBlocksCountLabel() : ''; - let neighbourLabel = (neighbour.getSourceBlock() as BlockSvg).getAriaLabel( - Verbosity.TERSE, - ); + let neighbourLabel = neighbour.getSourceBlock().getAriaLabel(Verbosity.TERSE); if (includeLocalContext) { blockLabel = computeMoveConnectionLabel(local, blockLabel); @@ -540,7 +554,17 @@ function getShadowBlockLabel(block: BlockSvg) { * otherwise undefined. */ function getInputCountLabel(block: BlockSvg) { - const inputCount = block.inputList.reduce((totalSum, input) => { + const branchCount = block.inputList.filter( + (input) => input.type === inputTypes.STATEMENT, + ).length; + + if (branchCount > 1) { + return Msg['BLOCK_LABEL_HAS_BRANCHES'].replace( + '%1', + branchCount.toString(), + ); + } + const valueInputCount = block.inputList.reduce((totalSum, input) => { return ( input.fieldRow.reduce((fieldCount, field) => { return field.EDITABLE && !field.isFullBlockField() @@ -551,7 +575,7 @@ function getInputCountLabel(block: BlockSvg) { ); }, 0); - switch (inputCount) { + switch (valueInputCount) { case 0: return undefined; case 1: diff --git a/packages/blockly/msg/json/en.json b/packages/blockly/msg/json/en.json index a643fd8f9b7..2d93b1a3094 100644 --- a/packages/blockly/msg/json/en.json +++ b/packages/blockly/msg/json/en.json @@ -1,7 +1,7 @@ { "@metadata": { "author": "Ellen Spertus ", - "lastupdated": "2026-05-13 09:26:53.422108", + "lastupdated": "2026-05-13 15:00:02.394960", "locale": "en", "messagedocumentation" : "qqq" }, @@ -475,6 +475,7 @@ "BLOCK_LABEL_REPLACEABLE": "replaceable", "BLOCK_LABEL_HAS_INPUT": "has input", "BLOCK_LABEL_HAS_INPUTS": "has inputs", + "BLOCK_LABEL_HAS_BRANCHES": "has %1 branches", "BLOCK_LABEL_STATEMENT": "command", "BLOCK_LABEL_CONTAINER": "container", "BLOCK_LABEL_VALUE": "value", diff --git a/packages/blockly/msg/json/qqq.json b/packages/blockly/msg/json/qqq.json index bdb9ce1952b..268146044f6 100644 --- a/packages/blockly/msg/json/qqq.json +++ b/packages/blockly/msg/json/qqq.json @@ -1,4 +1,18 @@ { + "@metadata": { + "authors": [ + "Ajeje Brazorf", + "Amire80", + "Espertus", + "Liuxinyu970226", + "McDutchie", + "Metalhead64", + "Nike", + "Robby", + "Shirayuki", + "YaronSh" + ] + }, "VARIABLES_DEFAULT_NAME": "default name - A simple, general default name for a variable, preferably short. For more context, see [[Translating:Blockly#infrequent_message_types]].\n{{Identical|Item}}", "UNNAMED_KEY": "default name - A simple, default name for an unnamed function or variable. Preferably indicates that the item is unnamed.", "TODAY": "button text - Button that sets a calendar to today's date.\n{{Identical|Today}}", @@ -469,6 +483,7 @@ "BLOCK_LABEL_REPLACEABLE": "Part of an accessibility label for a block that indicates that it is replaceable, i.e. that it is a shadow block.", "BLOCK_LABEL_HAS_INPUT": "Part of an accessibility label for a block that indicates that it has a single input.", "BLOCK_LABEL_HAS_INPUTS": "Part of an accessibility label for a block that indicates that it has more than one input.", + "BLOCK_LABEL_HAS_BRANCHES": "Part of an accessibility label for a block that indicates that it has more than one statement input, such as branches of an if-else block.", "BLOCK_LABEL_STATEMENT": "Part of an accessibility label for a block that indicates that it is a statement block, i.e. that it has a next or previous connection. 'command' here is used in the sense of a computer command, or a command block in Scratch.", "BLOCK_LABEL_CONTAINER": "Part of an accessibility label for a block that indicates that it is a container block, i.e. that it has one or more statement inputs.", "BLOCK_LABEL_VALUE": "Part of an accessibility label for a block that indicates that it is a value block, i.e. that it has an output connection.", diff --git a/packages/blockly/msg/messages.js b/packages/blockly/msg/messages.js index 943750ff4ee..08182dd0f3c 100644 --- a/packages/blockly/msg/messages.js +++ b/packages/blockly/msg/messages.js @@ -1881,6 +1881,10 @@ Blockly.Msg.BLOCK_LABEL_HAS_INPUT = 'has input'; /// than one input. Blockly.Msg.BLOCK_LABEL_HAS_INPUTS = 'has inputs'; /** @type {string} */ +/// Part of an accessibility label for a block that indicates that it has more +/// than one statement input, such as branches of an if-else block. +Blockly.Msg.BLOCK_LABEL_HAS_BRANCHES = 'has %1 branches'; +/** @type {string} */ /// Part of an accessibility label for a block that indicates that it is /// a statement block, i.e. that it has a next or previous connection. /// "command" here is used in the sense of a computer command, or a diff --git a/packages/blockly/tests/mocha/aria_test.js b/packages/blockly/tests/mocha/aria_test.js index 39d540d0f3f..5e29423eb91 100644 --- a/packages/blockly/tests/mocha/aria_test.js +++ b/packages/blockly/tests/mocha/aria_test.js @@ -451,5 +451,33 @@ suite('ARIA', function () { ); assert.isTrue(label.endsWith('has inputs')); }); + test('Blocks with multiple statement inputs are properly labeled', function () { + const json = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'controls_if', + 'id': 'ifBlock', + 'x': 0, + 'y': 100, + 'extraState': { + 'elseIfCount': 2, + 'hasElse': true, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(json, this.workspace); + const block = this.workspace.getBlockById('ifBlock'); + const label = Blockly.utils.aria.getState( + block.getFocusableElement(), + Blockly.utils.aria.State.LABEL, + ); + assert.isFalse(label.includes('else if, do')); + assert.isFalse(label.includes('else,')); + assert.isTrue(label.endsWith('has 4 branches')); + }); }); }); diff --git a/packages/blockly/tests/mocha/keyboard_movement_test.js b/packages/blockly/tests/mocha/keyboard_movement_test.js index a26749e8edd..2c919dbd676 100644 --- a/packages/blockly/tests/mocha/keyboard_movement_test.js +++ b/packages/blockly/tests/mocha/keyboard_movement_test.js @@ -1263,14 +1263,49 @@ suite('Keyboard-driven movement', function () { this.moveAndAssert( moveRight, ['Moving', 'else if, do', 'around', 'draw', '❤️'], - [this.getBlockLabel(ifBlock)], + ['of'], ); this.moveAndAssert( moveRight, ['Moving', 'if, do', 'around', 'draw', '❤️'], - [this.getBlockLabel(ifBlock)], + ['of'], ); + cancelMove(this.workspace); + }); + test("doesn't announce full block labels for multi-statement target blocks", function () { + const json = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'draw_emoji', + 'id': 'drawBlock', + 'x': 0, + 'y': 0, + }, + { + 'type': 'controls_if', + 'id': 'ifBlock', + 'x': 0, + 'y': 100, + 'extraState': { + 'elseIfCount': 2, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(json, this.workspace); + const drawBlock = this.workspace.getBlockById('drawBlock'); + const ifBlock = this.workspace.getBlockById('ifBlock'); + Blockly.getFocusManager().focusNode(drawBlock); + startMove(this.workspace); // on workspace + this.moveAndAssert( + moveRight, + ['Moving', 'before', ifBlock.getAriaLabel(0)], + [ifBlock.getAriaLabel(1), ifBlock.getAriaLabel(2)], + ); cancelMove(this.workspace); }); test('disambiguates with custom input labels around blocks', function () { From d61c79028631e857f654f3f23755f46958af1a53 Mon Sep 17 00:00:00 2001 From: Mike Harvey <43474485+mikeharv@users.noreply.github.com> Date: Thu, 14 May 2026 08:06:20 -0400 Subject: [PATCH 2/2] chore: re-add message after merge conflict --- packages/blockly/msg/json/en.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/blockly/msg/json/en.json b/packages/blockly/msg/json/en.json index 04a146a4a61..22cafc3f392 100644 --- a/packages/blockly/msg/json/en.json +++ b/packages/blockly/msg/json/en.json @@ -1,7 +1,7 @@ { "@metadata": { "author": "Ellen Spertus ", - "lastupdated": "2026-05-12 16:03:06.800029", + "lastupdated": "2026-05-14 08:05:42.601410", "locale": "en", "messagedocumentation" : "qqq" }, @@ -475,6 +475,7 @@ "BLOCK_LABEL_REPLACEABLE": "replaceable", "BLOCK_LABEL_HAS_INPUT": "has input", "BLOCK_LABEL_HAS_INPUTS": "has inputs", + "BLOCK_LABEL_HAS_BRANCHES": "has %1 branches", "BLOCK_LABEL_STATEMENT": "command", "BLOCK_LABEL_CONTAINER": "container", "BLOCK_LABEL_VALUE": "value",