feat: use custom labels for block parent input labels (#9867)

This commit is contained in:
Maribeth Moffatt
2026-05-13 16:46:17 -04:00
committed by GitHub
parent 057356fe10
commit c8e0700fcd
2 changed files with 120 additions and 20 deletions
+49 -18
View File
@@ -154,10 +154,24 @@ export function computeFieldRowLabel(
}
/**
* Returns a description of the parent statement input a block is attached to.
* When a block is connected to a statement input, the input's field row label
* will be prepended to the block's description to indicate that the block
* begins a clause in its parent block.
* Returns a description of the parent input a block is attached to.
* When a block is connected to an input, the input's label will sometimes
* be prepended to the block's description.
*
* If an input has a custom label, the custom label will be prepended
* to the first child block connected to that input.
*
* If an input does not have a custom label, the input's fallback
* label determined from the field row will be prepended to the
* child block's label only if the following are true:
* - the parent block has at least one statement input
* - the child block in question is not attached to the first
* statement input of the parent block (in this case, the label
* would be redundant with the parent block's label)
*
* For statement inputs, the resolved label (whether custom or fallback) is
* wrapped in the "Begin %1" prefix so the readout indicates that the child
* block starts the body of the statement input.
*
* @internal
* @param block The block to generate a parent input label for.
@@ -168,24 +182,41 @@ function getParentInputLabel(block: BlockSvg) {
const parentInput = (
block.outputConnection ?? block.previousConnection
)?.targetConnection?.getParentInput();
const parentBlock = parentInput?.getSourceBlock();
if (!parentInput) return undefined;
if (parentBlock?.isInsertionMarker()) return undefined;
if (!parentBlock?.statementInputCount) return undefined;
const parentBlock = parentInput.getSourceBlock();
if (parentBlock.isInsertionMarker()) return undefined;
const firstStatementInput = parentBlock.inputList.find(
(i) => i.type === inputTypes.STATEMENT,
);
// The first statement input in a block has no field row label as it would
// be duplicative of the block's label.
if (!parentInput || parentInput === firstStatementInput) {
return undefined;
// parentInput is only non-null when this block is directly attached to the
// input (i.e. it is the first child block in that input). A custom label
// is always prepended for the first child; a fallback label from the field
// row is only used in select circumstances.
let inputLabel: string | string[];
const customLabel = parentInput.getAriaLabelText();
if (customLabel) {
inputLabel = customLabel;
} else {
if (!parentBlock.statementInputCount) return undefined;
const firstStatementInput = parentBlock.inputList.find(
(i) => i.type === inputTypes.STATEMENT,
);
// The first statement input in a block has no field row label as it would
// be duplicative of the block's label.
if (parentInput === firstStatementInput) {
return undefined;
}
inputLabel = computeFieldRowLabel(parentInput, true);
}
const parentInputLabel = computeFieldRowLabel(parentInput, true);
return parentInput.type === inputTypes.STATEMENT
? Msg['BLOCK_LABEL_BEGIN_PREFIX'].replace('%1', parentInputLabel.join(' '))
: parentInputLabel;
if (parentInput.type === inputTypes.STATEMENT) {
const labelText = Array.isArray(inputLabel)
? inputLabel.join(' ')
: inputLabel;
return Msg['BLOCK_LABEL_BEGIN_PREFIX'].replace('%1', labelText);
}
return inputLabel;
}
/**
+71 -2
View File
@@ -371,7 +371,7 @@ suite('ARIA', function () {
assert.notInclude(label, 'Begin stack');
});
test('Nested statement blocks in first statement input do not include their parent input in their label', function () {
test('Statement blocks in first statement input do not include their parent input in their label', function () {
const ifBlock = this.makeBlock('controls_ifelse');
const printBlock = this.makeBlock('text_print');
ifBlock.getInput('IF0').connection.connect(printBlock.previousConnection);
@@ -382,7 +382,7 @@ suite('ARIA', function () {
assert.isFalse(label.startsWith('Begin do'));
});
test('Nested statement blocks in subsequent statement inputs include their parent input in their label', function () {
test('Statement blocks in subsequent statement inputs include their parent input in their label', function () {
const ifBlock = this.makeBlock('controls_ifelse');
const printBlock = this.makeBlock('text_print');
ifBlock
@@ -395,6 +395,75 @@ suite('ARIA', function () {
assert.isTrue(label.startsWith('Begin else'));
});
test('A custom statement input label is wrapped in the "Begin" prefix', function () {
const ifBlock = this.makeBlock('controls_ifelse');
ifBlock.getInput('ELSE').setAriaLabelProvider('otherwise do');
const printBlock = this.makeBlock('text_print');
ifBlock
.getInput('ELSE')
.connection.connect(printBlock.previousConnection);
const label = Blockly.utils.aria.getState(
printBlock.getFocusableElement(),
Blockly.utils.aria.State.LABEL,
);
assert.include(label, 'Begin otherwise do');
});
test('A custom label on the first statement input is prepended to its child block label', function () {
const ifBlock = this.makeBlock('controls_ifelse');
ifBlock.getInput('DO0').setAriaLabelProvider('then do');
const printBlock = this.makeBlock('text_print');
ifBlock.getInput('DO0').connection.connect(printBlock.previousConnection);
const label = Blockly.utils.aria.getState(
printBlock.getFocusableElement(),
Blockly.utils.aria.State.LABEL,
);
assert.include(label, 'Begin then do');
});
test('A custom input label is only used for the first child block in a statement input stack', function () {
const ifBlock = this.makeBlock('controls_ifelse');
ifBlock.getInput('ELSE').setAriaLabelProvider('otherwise do');
const firstPrintBlock = this.makeBlock('text_print');
ifBlock
.getInput('ELSE')
.connection.connect(firstPrintBlock.previousConnection);
const secondPrintBlock = this.makeBlock('text_print');
firstPrintBlock.nextConnection.connect(
secondPrintBlock.previousConnection,
);
const subsequentLabel = Blockly.utils.aria.getState(
secondPrintBlock.getFocusableElement(),
Blockly.utils.aria.State.LABEL,
);
assert.notInclude(subsequentLabel, 'otherwise do');
});
test('A custom input label is prepended to the child block of a value input', function () {
const ifBlock = this.makeBlock('controls_ifelse');
ifBlock.getInput('IF0').setAriaLabelProvider('condition');
const boolBlock = this.makeBlock('logic_boolean');
ifBlock.getInput('IF0').connection.connect(boolBlock.outputConnection);
const label = Blockly.utils.aria.getState(
boolBlock.getFocusableElement(),
Blockly.utils.aria.State.LABEL,
);
assert.include(label, 'condition');
});
test('A block connected to a value input without a custom label does not include the input label', function () {
const negateBlock = this.makeBlock('logic_negate');
const boolBlock = this.makeBlock('logic_boolean');
negateBlock
.getInput('BOOL')
.connection.connect(boolBlock.outputConnection);
const label = Blockly.utils.aria.getState(
boolBlock.getFocusableElement(),
Blockly.utils.aria.State.LABEL,
);
assert.notInclude(label, 'not');
});
test('Disabled blocks indicate that in their label', function () {
const block = this.makeBlock('text_print');
let label = Blockly.utils.aria.getState(