diff --git a/packages/blockly/core/block_aria_composer.ts b/packages/blockly/core/block_aria_composer.ts index 85f9de68751..21df886ff6f 100644 --- a/packages/blockly/core/block_aria_composer.ts +++ b/packages/blockly/core/block_aria_composer.ts @@ -260,12 +260,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); + }); } /** @@ -482,9 +498,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); @@ -571,7 +585,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() @@ -582,7 +606,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 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", diff --git a/packages/blockly/msg/json/qqq.json b/packages/blockly/msg/json/qqq.json index 68312cd952e..1867b58f159 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 c6cb9cf7fed..4a71e140ae0 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 f03ba068814..bd5ee02cfec 100644 --- a/packages/blockly/tests/mocha/aria_test.js +++ b/packages/blockly/tests/mocha/aria_test.js @@ -521,6 +521,34 @@ 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')); + }); }); suite('Rendered connection highlight ARIA', function () { 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 () {