From f7ad4199ce596235e7c5e620dcd2e3a04b8c9d26 Mon Sep 17 00:00:00 2001 From: Mike Harvey <43474485+mikeharv@users.noreply.github.com> Date: Mon, 11 May 2026 10:07:10 -0400 Subject: [PATCH 1/5] fix: insert custom input label before children --- packages/blockly/core/block_aria_composer.ts | 26 ++------ packages/blockly/core/inputs/input.ts | 24 ++++++-- packages/blockly/tests/mocha/input_test.js | 6 +- .../tests/mocha/keyboard_movement_test.js | 61 +++++++++++++++++-- 4 files changed, 85 insertions(+), 32 deletions(-) diff --git a/packages/blockly/core/block_aria_composer.ts b/packages/blockly/core/block_aria_composer.ts index 38fd2ee0bae..852d2ed4c2c 100644 --- a/packages/blockly/core/block_aria_composer.ts +++ b/packages/blockly/core/block_aria_composer.ts @@ -205,7 +205,7 @@ export function getInputLabels( ): string[] { return block.inputList .filter((input) => input.isVisible()) - .map((input) => input.getAriaLabelText() ?? input.getLabel(verbosity)); + .map((input) => input.getLabel(verbosity, input.getAriaLabelText())); } /** @@ -224,11 +224,7 @@ export function getInputLabels( * @param input The input that defines the end of the subset. * @returns A list of field/input labels for the given block. */ -export function getInputLabelsSubset( - block: BlockSvg, - input: Input, - verbosity = Verbosity.STANDARD, -): string[] { +function getInputLabelsSubset(block: BlockSvg, input: Input): string[] { const inputIndex = block.inputList.indexOf(input); if (inputIndex === -1) { throw new Error( @@ -246,7 +242,7 @@ export function getInputLabelsSubset( .filter((input) => input.isVisible()) .map( (input) => - input.getLabel(verbosity) || + input.getLabel(Verbosity.TERSE, input.getAriaLabelText()) || Msg['INPUT_LABEL_INDEX'].replace( '%1', (input.getIndex() + 1).toString(), @@ -374,20 +370,10 @@ function computeMoveConnectionLabel( const input = conn.getParentInput(); if (!input) return baseLabel; - let inputLabel = input.getAriaLabelText(); + const labels = getInputLabelsSubset(conn.getSourceBlock(), input); + if (!labels.length) return baseLabel; - // If the input doesn't have a custom ARIA label, compute one using the labels from - // nearby fields. - if (!inputLabel) { - const labels = getInputLabelsSubset( - conn.getSourceBlock(), - input, - Verbosity.TERSE, - ); - if (!labels.length) return baseLabel; - - inputLabel = labels.join(', '); - } + const inputLabel = labels.join(', '); return baseLabel ? Msg['ANNOUNCE_MOVE_OF'].replace('%1', inputLabel).replace('%2', baseLabel) diff --git a/packages/blockly/core/inputs/input.ts b/packages/blockly/core/inputs/input.ts index f85516d7bd7..86a55eb1f75 100644 --- a/packages/blockly/core/inputs/input.ts +++ b/packages/blockly/core/inputs/input.ts @@ -401,11 +401,22 @@ export class Input { * * @internal */ - getLabel(verbosity = Verbosity.STANDARD): string { + getLabel( + verbosity = Verbosity.STANDARD, + ariaLabelText: string | null, + ): string { if (!this.isVisible()) return ''; const labels = computeFieldRowLabel(this, false, verbosity); + // A block's custom ARIA label for this input is inserted between the field + // row label and the connected block labels, since it's meant to describe the + // connection itself, which is conceptually between the field row and any + // connected blocks. + if (ariaLabelText) { + labels.push(ariaLabelText); + } + if (this.connection?.type === ConnectionType.INPUT_VALUE) { const childBlock = this.connection.targetBlock(); if (childBlock && !childBlock.isInsertionMarker()) { @@ -418,12 +429,17 @@ export class Input { } /** - * Returns the index of this input on its source block. + * Returns the index of this input, excluding inputs without connections, on its + * source block. * * @internal */ getIndex(): number { - const inputs = this.getSourceBlock().inputList; - return inputs.indexOf(this); + const noConnectionInputTypes = [inputTypes.DUMMY, inputTypes.END_ROW]; + const allInputs = this.getSourceBlock().inputList; + const allConnectionInputs = allInputs.filter( + (input) => !noConnectionInputTypes.includes(input.type), + ); + return allConnectionInputs.indexOf(this); } } diff --git a/packages/blockly/tests/mocha/input_test.js b/packages/blockly/tests/mocha/input_test.js index 075ea548d3d..cdf7aeb2819 100644 --- a/packages/blockly/tests/mocha/input_test.js +++ b/packages/blockly/tests/mocha/input_test.js @@ -315,13 +315,13 @@ suite('Inputs', function () { // Using a text input as it will return a default ARIA label this.block .appendValueInput('NAME') - .appendField(new Blockly.FieldTextInput('text'), 'NAME') + .appendField(new Blockly.FieldTextInput('test'), 'NAME') .setAriaLabelProvider((input) => customLabel); const label = this.block.getAriaLabel(); - assert.include(label, customLabel); - assert.notInclude(label, 'text'); + assert.include(label, 'text: test'); // Computed label from fields + assert.include(label, customLabel); // Custom ARIA label from provider }); test('Set input ARIA Label Provider from JSON', function () { const customLabel = 'custom ARIA label'; diff --git a/packages/blockly/tests/mocha/keyboard_movement_test.js b/packages/blockly/tests/mocha/keyboard_movement_test.js index e8c7222a32e..e7c3ee4acf5 100644 --- a/packages/blockly/tests/mocha/keyboard_movement_test.js +++ b/packages/blockly/tests/mocha/keyboard_movement_test.js @@ -1280,7 +1280,7 @@ suite('Keyboard-driven movement', function () { ifBlock.elseCount_ = 1; ifBlock.updateShape_(); ifBlock.render(); - ifBlock.getInput('DO1').setAriaLabelProvider('custom else if branch'); + ifBlock.getInput('DO1').setAriaLabelProvider('custom branch label'); this.workspace.cleanUp(); Blockly.getFocusManager().focusNode(ifBlock); @@ -1289,16 +1289,23 @@ suite('Keyboard-driven movement', function () { this.clock.tick(10); this.moveAndAssert( moveRight, - ['Moving', 'custom else if branch', 'around', 'draw', '❤️'], - ['else if, do'], + [ + 'Moving', + 'else if, do', + 'custom branch label', + 'around', + 'draw', + '❤️', + ], + [], ); cancelMove(this.workspace); Blockly.getFocusManager().focusNode(this.block1); this.clock.tick(10); this.moveAndAssert( startMove, - ['Moving', 'inside', 'custom else if branch'], - ['else if, do'], + ['Moving', 'inside', 'else if, do', 'custom branch label'], + [], ); cancelMove(this.workspace); }); @@ -1353,6 +1360,50 @@ suite('Keyboard-driven movement', function () { cancelMove(this.workspace); }); + test('ignores dummy inputs when disambiguating', function () { + const subListBlock = this.workspace.newBlock('lists_getSublist'); + subListBlock.initSvg(); + subListBlock.render(); + const mathBlock = this.workspace.newBlock('math_number'); + mathBlock.initSvg(); + mathBlock.render(); + + Blockly.getFocusManager().focusNode(mathBlock); + startMove(this.workspace); + this.clock.tick(10); + this.moveAndAssert( + moveRight, + ['Moving', 'to', 'list, get sub-list from', 'input 2'], + ['input 3'], + ); + this.moveAndAssert( + moveRight, + ['Moving', 'to', 'list, get sub-list from', 'input 3'], + ['input 4'], + ); + + cancelMove(this.workspace); + }); + test('ignores end row inputs when disambiguating', function () { + const compare = this.workspace.newBlock('logic_compare'); + compare.appendDummyInput('END_ROW'); + compare.moveInputBefore('END_ROW', 'A'); + compare.initSvg(); + compare.render(); + const boolean = this.workspace.newBlock('logic_boolean'); + boolean.initSvg(); + boolean.render(); + + Blockly.getFocusManager().focusNode(boolean); + startMove(this.workspace); + this.clock.tick(10); + this.moveAndAssert( + moveRight, + ['Moving', 'to', 'input 1', '='], + [this.getBlockLabel(boolean)], + ); + cancelMove(this.workspace); + }); }); }); From 49776ef2e4b84da5e4d8d045f262f56ff23c2c71 Mon Sep 17 00:00:00 2001 From: Mike Harvey <43474485+mikeharv@users.noreply.github.com> Date: Mon, 11 May 2026 10:34:30 -0400 Subject: [PATCH 2/5] fix: do not add empty field labels to block label --- packages/blockly/core/block_aria_composer.ts | 6 +++- packages/blockly/core/field_label.ts | 36 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/blockly/core/block_aria_composer.ts b/packages/blockly/core/block_aria_composer.ts index 852d2ed4c2c..3f243cbceb0 100644 --- a/packages/blockly/core/block_aria_composer.ts +++ b/packages/blockly/core/block_aria_composer.ts @@ -112,6 +112,10 @@ export function configureAriaRole(block: BlockSvg) { * `lookback` attribute is specified, all of the fields on the row immediately * above the Input will be used instead. * + * Empty field labels are excluded because they don't provide useful context. + * Fields should generally have a helpful label, but there are exceptions, such + * as when empty label fields are used to control the layout of a block. + * * @internal * @param input The Input to compute a description/context label for. * @param lookback If true, will use labels for fields on the previous row if @@ -135,7 +139,7 @@ export function computeFieldRowLabel( return computeFieldRowLabel(inputs[index - 1], lookback, verbosity); } } - return fieldRowLabel; + return fieldRowLabel.filter((label) => !!label); } /** diff --git a/packages/blockly/core/field_label.ts b/packages/blockly/core/field_label.ts index 16745d3f926..ffa122075ee 100644 --- a/packages/blockly/core/field_label.ts +++ b/packages/blockly/core/field_label.ts @@ -81,6 +81,42 @@ export class FieldLabel extends Field { } } + /** + * Computes a descriptive ARIA label to represent this field with configurable + * verbosity. + * + * A 'verbose' label includes type information, if available, whereas a + * non-verbose label only contains the field's value. + * + * Note that this will always return the latest representation of the field's + * label which may differ from any previously set ARIA label for the field + * itself. Implementations are largely responsible for ensuring that the + * field's ARIA label is set correctly at relevant moments in the field's + * lifecycle (such as when its value changes). + * + * Finally, it is never guaranteed that implementations use the label returned + * by this method for their actual ARIA label. Some implementations may rely + * on other contexts to convey information like the field's value. Example: + * checkboxes represent their checked/non-checked status (i.e. value) through + * a separate ARIA property. + * + * Unlike other built-in fields, FieldLabel does return an empty string when its + * value is empty. This is because empty labels are sometimes used for layout + * purposes. + * + * @param includeTypeInfo Whether to include the field's type information in + * the returned label, if available. + */ + computeAriaLabel(includeTypeInfo: boolean = true): string { + const ariaTypeName = includeTypeInfo ? this.getAriaTypeName() : null; + const ariaValue = this.getAriaValue() ?? ''; + + if (ariaTypeName) { + return `${ariaTypeName}: ${ariaValue}`; + } + return ariaValue; + } + /** * Ensure that the input value casts to a valid string. * From 0a9372aa7a0892bbeff65add63cff6a598a0af6d Mon Sep 17 00:00:00 2001 From: Mike Harvey <43474485+mikeharv@users.noreply.github.com> Date: Mon, 11 May 2026 11:53:27 -0400 Subject: [PATCH 3/5] fix: revert ariaLabelProvider changes --- packages/blockly/core/block_aria_composer.ts | 16 +++++++++++----- packages/blockly/core/inputs/input.ts | 13 +------------ packages/blockly/tests/mocha/input_test.js | 6 +++--- .../tests/mocha/keyboard_movement_test.js | 15 ++++----------- 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/packages/blockly/core/block_aria_composer.ts b/packages/blockly/core/block_aria_composer.ts index 3f243cbceb0..60b3a438bd8 100644 --- a/packages/blockly/core/block_aria_composer.ts +++ b/packages/blockly/core/block_aria_composer.ts @@ -209,7 +209,7 @@ export function getInputLabels( ): string[] { return block.inputList .filter((input) => input.isVisible()) - .map((input) => input.getLabel(verbosity, input.getAriaLabelText())); + .map((input) => input.getAriaLabelText() ?? input.getLabel(verbosity)); } /** @@ -246,7 +246,7 @@ function getInputLabelsSubset(block: BlockSvg, input: Input): string[] { .filter((input) => input.isVisible()) .map( (input) => - input.getLabel(Verbosity.TERSE, input.getAriaLabelText()) || + input.getLabel(Verbosity.TERSE) || Msg['INPUT_LABEL_INDEX'].replace( '%1', (input.getIndex() + 1).toString(), @@ -374,10 +374,16 @@ function computeMoveConnectionLabel( const input = conn.getParentInput(); if (!input) return baseLabel; - const labels = getInputLabelsSubset(conn.getSourceBlock(), input); - if (!labels.length) return baseLabel; + let inputLabel = input.getAriaLabelText(); - const inputLabel = labels.join(', '); + // If the input doesn't have a custom ARIA label, compute one using the labels from + // nearby fields. + if (!inputLabel) { + const labels = getInputLabelsSubset(conn.getSourceBlock(), input); + if (!labels.length) return baseLabel; + + inputLabel = labels.join(', '); + } return baseLabel ? Msg['ANNOUNCE_MOVE_OF'].replace('%1', inputLabel).replace('%2', baseLabel) diff --git a/packages/blockly/core/inputs/input.ts b/packages/blockly/core/inputs/input.ts index 86a55eb1f75..88b760dda15 100644 --- a/packages/blockly/core/inputs/input.ts +++ b/packages/blockly/core/inputs/input.ts @@ -401,22 +401,11 @@ export class Input { * * @internal */ - getLabel( - verbosity = Verbosity.STANDARD, - ariaLabelText: string | null, - ): string { + getLabel(verbosity = Verbosity.STANDARD): string { if (!this.isVisible()) return ''; const labels = computeFieldRowLabel(this, false, verbosity); - // A block's custom ARIA label for this input is inserted between the field - // row label and the connected block labels, since it's meant to describe the - // connection itself, which is conceptually between the field row and any - // connected blocks. - if (ariaLabelText) { - labels.push(ariaLabelText); - } - if (this.connection?.type === ConnectionType.INPUT_VALUE) { const childBlock = this.connection.targetBlock(); if (childBlock && !childBlock.isInsertionMarker()) { diff --git a/packages/blockly/tests/mocha/input_test.js b/packages/blockly/tests/mocha/input_test.js index cdf7aeb2819..075ea548d3d 100644 --- a/packages/blockly/tests/mocha/input_test.js +++ b/packages/blockly/tests/mocha/input_test.js @@ -315,13 +315,13 @@ suite('Inputs', function () { // Using a text input as it will return a default ARIA label this.block .appendValueInput('NAME') - .appendField(new Blockly.FieldTextInput('test'), 'NAME') + .appendField(new Blockly.FieldTextInput('text'), 'NAME') .setAriaLabelProvider((input) => customLabel); const label = this.block.getAriaLabel(); - assert.include(label, 'text: test'); // Computed label from fields - assert.include(label, customLabel); // Custom ARIA label from provider + assert.include(label, customLabel); + assert.notInclude(label, 'text'); }); test('Set input ARIA Label Provider from JSON', function () { const customLabel = 'custom ARIA label'; diff --git a/packages/blockly/tests/mocha/keyboard_movement_test.js b/packages/blockly/tests/mocha/keyboard_movement_test.js index e7c3ee4acf5..e2b039acbd3 100644 --- a/packages/blockly/tests/mocha/keyboard_movement_test.js +++ b/packages/blockly/tests/mocha/keyboard_movement_test.js @@ -1289,23 +1289,16 @@ suite('Keyboard-driven movement', function () { this.clock.tick(10); this.moveAndAssert( moveRight, - [ - 'Moving', - 'else if, do', - 'custom branch label', - 'around', - 'draw', - '❤️', - ], - [], + ['Moving', 'custom branch label', 'around', 'draw', '❤️'], + ['else if, do'], ); cancelMove(this.workspace); Blockly.getFocusManager().focusNode(this.block1); this.clock.tick(10); this.moveAndAssert( startMove, - ['Moving', 'inside', 'else if, do', 'custom branch label'], - [], + ['Moving', 'inside', 'custom branch label'], + ['else if, do'], ); cancelMove(this.workspace); }); From 5e3370c4f4bd796d32c6779b47c5b15de1885dc5 Mon Sep 17 00:00:00 2001 From: Mike Harvey <43474485+mikeharv@users.noreply.github.com> Date: Mon, 11 May 2026 16:39:53 -0400 Subject: [PATCH 4/5] fix: add override designation --- packages/blockly/core/field_label.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blockly/core/field_label.ts b/packages/blockly/core/field_label.ts index ffa122075ee..49f0583d216 100644 --- a/packages/blockly/core/field_label.ts +++ b/packages/blockly/core/field_label.ts @@ -107,7 +107,7 @@ export class FieldLabel extends Field { * @param includeTypeInfo Whether to include the field's type information in * the returned label, if available. */ - computeAriaLabel(includeTypeInfo: boolean = true): string { + override computeAriaLabel(includeTypeInfo: boolean = true): string { const ariaTypeName = includeTypeInfo ? this.getAriaTypeName() : null; const ariaValue = this.getAriaValue() ?? ''; From c05e33f42b1d5ebee8674367d2163723a6440fe9 Mon Sep 17 00:00:00 2001 From: Mike Harvey <43474485+mikeharv@users.noreply.github.com> Date: Mon, 11 May 2026 17:10:21 -0400 Subject: [PATCH 5/5] fix: update test after merge conflict --- packages/blockly/tests/mocha/keyboard_movement_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blockly/tests/mocha/keyboard_movement_test.js b/packages/blockly/tests/mocha/keyboard_movement_test.js index cbd2717822e..a26749e8edd 100644 --- a/packages/blockly/tests/mocha/keyboard_movement_test.js +++ b/packages/blockly/tests/mocha/keyboard_movement_test.js @@ -1306,7 +1306,7 @@ suite('Keyboard-driven movement', function () { this.clock.tick(10); this.moveAndAssert( moveRight, - ['Moving', 'custom branch label', 'around', 'draw', '❤️'], + ['Moving', 'custom else if branch', 'around', 'draw', '❤️'], ['else if, do'], ); cancelMove(this.workspace);