diff --git a/packages/blockly/core/block_aria_composer.ts b/packages/blockly/core/block_aria_composer.ts index e39de955127..f8ffd68d27f 100644 --- a/packages/blockly/core/block_aria_composer.ts +++ b/packages/blockly/core/block_aria_composer.ts @@ -63,13 +63,6 @@ export function computeAriaLabel( block: BlockSvg, verbosity = Verbosity.STANDARD, ) { - if (block.isSimpleReporter()) { - // special case for full-block field blocks. - const field = block.getFullBlockField(); - if (field) { - return field.computeAriaLabel(verbosity >= Verbosity.STANDARD); - } - } return [ verbosity >= Verbosity.STANDARD && getBeginStackLabel(block), getParentInputLabel(block), @@ -271,7 +264,7 @@ function getParentInputLabel(block: BlockSvg) { * @returns Text indicating that the block begins a stack, or undefined if it * does not. */ -function getBeginStackLabel(block: BlockSvg) { +export function getBeginStackLabel(block: BlockSvg) { // Don't include the "begin stack" label for blocks that are moving // or blocks in the flyout if (block.isInFlyout || block.isDragging()) return undefined; diff --git a/packages/blockly/core/field_dropdown.ts b/packages/blockly/core/field_dropdown.ts index 11e3f6282a8..48fa9abd117 100644 --- a/packages/blockly/core/field_dropdown.ts +++ b/packages/blockly/core/field_dropdown.ts @@ -13,6 +13,7 @@ */ // Former goog.module ID: Blockly.FieldDropdown +import {computeAriaLabel} from './block_aria_composer.js'; import type {BlockSvg} from './block_svg.js'; import * as dropDownDiv from './dropdowndiv.js'; import { @@ -940,7 +941,14 @@ export class FieldDropdown extends Field { if (!shouldCustomize) return false; const focusableElement = this.getFocusableElement(); - const label = this.computeAriaLabel(true); + let label = this.computeAriaLabel(true); + if (this.isFullBlockField()) { + // Full block fields get a more detailed label that includes the block's label + label = computeAriaLabel(this.getSourceBlock() as BlockSvg).replace( + this.computeAriaLabel(false), + label, + ); + } aria.setState(focusableElement, aria.State.LABEL, label); aria.setState(focusableElement, aria.State.HASPOPUP, 'listbox'); diff --git a/packages/blockly/core/field_input.ts b/packages/blockly/core/field_input.ts index 0da8371c015..cfa1003ae60 100644 --- a/packages/blockly/core/field_input.ts +++ b/packages/blockly/core/field_input.ts @@ -14,6 +14,7 @@ // Unused import preserved for side-effects. Remove if unneeded. import './events/events_block_change.js'; +import {computeAriaLabel, getBeginStackLabel} from './block_aria_composer.js'; import {BlockSvg} from './block_svg.js'; import * as browserEvents from './browser_events.js'; import * as bumpObjects from './bump_objects.js'; @@ -855,8 +856,35 @@ export abstract class FieldInput extends Field< const focusableElement = this.getFocusableElement(); let label = this.computeAriaLabel(true); - if (this.isCurrentlyEditable() && !this.getSourceBlock()?.isInFlyout) { - label = Msg['FIELD_LABEL_EDIT_PREFIX'].replace('%1', label); + const requiresEditableLabel = + this.isCurrentlyEditable() && !this.getSourceBlock()?.isInFlyout; + + if (!this.isFullBlockField()) { + if (requiresEditableLabel) { + label = Msg['FIELD_LABEL_EDIT_PREFIX'].replace('%1', label); + } + } else { + // Full block fields get a more detailed label that includes the block's label + const fullBlockLabel = computeAriaLabel( + this.getSourceBlock() as BlockSvg, + ).replace(this.computeAriaLabel(false), label); + if (requiresEditableLabel) { + const labels = fullBlockLabel.split(', '); + const beginStackLabel = getBeginStackLabel( + this.getSourceBlock() as BlockSvg, + ); + + // Insert "Edit" after "Begin stack" if found, otherwise at start. + const beginStackLabelIndex = + beginStackLabel === undefined ? -1 : labels.indexOf(beginStackLabel); + const insertIndex = + beginStackLabelIndex === -1 ? 0 : beginStackLabelIndex + 1; + labels[insertIndex] = Msg['FIELD_LABEL_EDIT_PREFIX'].replace( + '%1', + labels[insertIndex] ?? '', + ); + label = labels.join(', '); + } } aria.setState(focusableElement, aria.State.LABEL, label); return true; diff --git a/packages/blockly/tests/mocha/field_dropdown_test.js b/packages/blockly/tests/mocha/field_dropdown_test.js index cae3fb4d0fb..2ee51844bf5 100644 --- a/packages/blockly/tests/mocha/field_dropdown_test.js +++ b/packages/blockly/tests/mocha/field_dropdown_test.js @@ -580,5 +580,58 @@ suite('Dropdown Fields', function () { assert.include(label, 'Option 5'); }); }); + suite('Full block fields', function () { + setup(function () { + this.workspace = Blockly.inject('blocklyDiv', { + renderer: 'zelos', + }); + this.block = this.workspace.newBlock('variables_get'); + this.block.initSvg(); + this.block.render(); + this.field = this.block.getField('VAR'); + }); + + test('Top block ARIA label includes "Begin stack" label before dropdown field label', function () { + const labels = this.block + .getFocusableElement() + .getAttribute('aria-label') + .split(', '); + + const expectedBeginStackLabel = 'Begin stack'; + const expectedFieldLabel = "dropdown: Variable 'item'"; + assert.include(labels, expectedBeginStackLabel); + assert.include(labels, expectedFieldLabel); + assert.isTrue( + labels.indexOf(expectedBeginStackLabel) < + labels.indexOf(expectedFieldLabel), + ); + }); + + test('Child block ARIA label includes parent input custom label before dropdown field label', function () { + const parentBlock = this.workspace.newBlock('controls_repeat_ext'); + parentBlock.initSvg(); + parentBlock.render(); + + this.block.outputConnection.connect( + parentBlock.getInput('TIMES').connection, + ); + + this.block.getField('VAR').recomputeAriaContext(); + + const labels = this.block + .getFocusableElement() + .getAttribute('aria-label') + .split(', '); + + const expectedInputLabel = 'number of times to repeat'; + const expectedFieldLabel = "dropdown: Variable 'item'"; + assert.include(labels, expectedInputLabel); + assert.include(labels, expectedFieldLabel); + assert.isTrue( + labels.indexOf(expectedInputLabel) < + labels.indexOf(expectedFieldLabel), + ); + }); + }); }); }); diff --git a/packages/blockly/tests/mocha/field_number_test.js b/packages/blockly/tests/mocha/field_number_test.js index 59d82b4b141..616fee56daa 100644 --- a/packages/blockly/tests/mocha/field_number_test.js +++ b/packages/blockly/tests/mocha/field_number_test.js @@ -551,5 +551,53 @@ suite('Number Fields', function () { const updatedLabel = this.focusableElement.getAttribute('aria-label'); assert.isTrue(updatedLabel.includes('1')); }); + suite('Full block fields', function () { + setup(function () { + this.workspace = Blockly.inject('blocklyDiv', { + renderer: 'zelos', + }); + this.block = this.workspace.newBlock('math_number'); + this.field = this.block.getField('NUM'); + this.block.initSvg(); + this.block.render(); + }); + test('Top block ARIA label includes "Begin stack" label before expected field label', function () { + const labels = this.block + .getFocusableElement() + .getAttribute('aria-label') + .split(', '); + + const expectedBeginStackLabel = 'Begin stack'; + const expectedFieldLabel = 'Edit number: 0'; + assert.include(labels, expectedBeginStackLabel); + assert.include(labels, expectedFieldLabel); + assert.isTrue( + labels.indexOf(expectedBeginStackLabel) < + labels.indexOf(expectedFieldLabel), + ); + }); + test('Child block ARIA label includes parent input custom label after "Edit" label and before field label', function () { + const parentBlock = this.workspace.newBlock('controls_repeat_ext'); + parentBlock.initSvg(); + parentBlock.render(); + this.block.outputConnection.connect( + parentBlock.getInput('TIMES').connection, + ); + this.block.getField('NUM').recomputeAriaContext(); + const labels = this.block + .getFocusableElement() + .getAttribute('aria-label') + .split(', '); + + const expectedInputLabel = 'Edit number of times to repeat'; + const expectedFieldLabel = 'number: 0'; + assert.include(labels, expectedInputLabel); + assert.include(labels, expectedFieldLabel); + assert.isTrue( + labels.indexOf(expectedInputLabel) < + labels.indexOf(expectedFieldLabel), + ); + }); + }); }); });