fix: labels for multi-statement blocks (#9868)

* fix: labels for multi-statement blocks

* chore: re-add message after merge conflict
This commit is contained in:
Michael Harvey
2026-05-14 09:11:31 -04:00
committed by GitHub
parent 6513d08aeb
commit 463f95eaac
6 changed files with 121 additions and 14 deletions
+35 -11
View File
@@ -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:
+2 -1
View File
@@ -1,7 +1,7 @@
{
"@metadata": {
"author": "Ellen Spertus <ellen.spertus@gmail.com>",
"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",
+15
View File
@@ -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.",
+4
View File
@@ -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
+28
View File
@@ -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 () {
@@ -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 () {