From a6ea088bff4d69bcf9b2705ed00f6f2a1f35d962 Mon Sep 17 00:00:00 2001 From: lizschwab Date: Thu, 30 Apr 2026 09:03:54 -0700 Subject: [PATCH 1/3] feat: Added support for custom ARIA labels --- packages/blockly/core/block.ts | 3 ++ packages/blockly/core/block_aria_composer.ts | 6 ++- packages/blockly/core/inputs/input.ts | 43 ++++++++++++++++++++ packages/blockly/tests/mocha/input_test.js | 15 +++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/blockly/core/block.ts b/packages/blockly/core/block.ts index a47eb1e5430..4690229d439 100644 --- a/packages/blockly/core/block.ts +++ b/packages/blockly/core/block.ts @@ -2165,6 +2165,9 @@ export class Block { input.setAlign(alignment); } } + if (element['ariaLabelText']) { + input.setAriaLabelProvider(element['ariaLabelText']); + } return input; } diff --git a/packages/blockly/core/block_aria_composer.ts b/packages/blockly/core/block_aria_composer.ts index 5e02f3fc9c4..aa9cb6db799 100644 --- a/packages/blockly/core/block_aria_composer.ts +++ b/packages/blockly/core/block_aria_composer.ts @@ -204,7 +204,11 @@ export function getInputLabels( ): string[] { return block.inputList .filter((input) => input.isVisible()) - .map((input) => input.getLabel(verbosity)); + .map((input) => + input.getAriaLabelText() !== null + ? input.getAriaLabelText()! + : input.getLabel(verbosity), + ); } /** diff --git a/packages/blockly/core/inputs/input.ts b/packages/blockly/core/inputs/input.ts index 86171316e4d..96589274496 100644 --- a/packages/blockly/core/inputs/input.ts +++ b/packages/blockly/core/inputs/input.ts @@ -23,9 +23,17 @@ import type {Field} from '../field.js'; import * as fieldRegistry from '../field_registry.js'; import {RenderedConnection} from '../rendered_connection.js'; import {Verbosity} from '../utils/aria.js'; +import * as parsing from '../utils/parsing.js'; import {Align} from './align.js'; import {inputTypes} from './input_types.js'; +/** + * Represents a string or a function that returns a string which can be used as a + * custom ARIA string to represent an Input, or null if the default fallback should + * be used. See setAriaLabelProvider for more context. + */ +export type AriaLabelProvider = ((input: Input) => string | null) | string; + /** Class for an input with optional fields. */ export class Input { fieldRow: Field[] = []; @@ -35,6 +43,9 @@ export class Input { /** Is the input visible? */ private visible = true; + /** The AriaLabelProvider */ + private ariaLabelProvider: AriaLabelProvider | null = null; + public readonly type: inputTypes = inputTypes.CUSTOM; public connection: Connection | null = null; @@ -273,6 +284,38 @@ export class Input { } } + /** + * Sets a custom ARIA label provider for this input, or null if it should be reset + * to use the default method. + * + * Inputs do not compute ARIA contexts directly, so the set provider will be used + * in select cases when the Input needs to be represented (such as for parts of a + * block label or for connections). Note that overriding this provider will not + * recompute any already constructed ARIA labels, and it cannot be assumed that the + * provider will be called any particular number of times during label + * recomputation. As such, implementations should make sure to provide a + * deterministic and idempotent ARIA representation each time the provider is + * called for a given input. It's also fine to reuse providers across multiple + * Input implementations. + */ + setAriaLabelProvider(provider: AriaLabelProvider | null) { + this.ariaLabelProvider = provider; + } + + /** + * Returns the string from the custom ARIA label provider set, or null if the default label (from the field row) should + * be used. See setAriaLabelProvider for more context. + */ + getAriaLabelText(): string | null { + if (!this.ariaLabelProvider) { + return null; + } else if (typeof this.ariaLabelProvider === 'string') { + return parsing.replaceMessageReferences(this.ariaLabelProvider); + } else { + return this.ariaLabelProvider(this); + } + } + /** * Initializes the fields on this input for a headless block. * diff --git a/packages/blockly/tests/mocha/input_test.js b/packages/blockly/tests/mocha/input_test.js index dfa30858e0e..d110137543d 100644 --- a/packages/blockly/tests/mocha/input_test.js +++ b/packages/blockly/tests/mocha/input_test.js @@ -293,4 +293,19 @@ suite('Inputs', function () { assert.deepEqual(this.dummy.fieldRow, [this.b, this.c]); }); }); + suite('ARIA', function () { + test('Set ARIA Label Provider', function () { + const customLabel = 'custom ARIA label'; + this.block + .appendValueInput('NAME') + .setAriaLabelProvider((input) => customLabel); + + const label = this.block.getAriaLabel(); + + assert.include(label, customLabel); + }); + test('Set ARIA Label Provider to null', function () { + // TODO: need a way to test that the getLabel() returns appropriately + }); + }); }); From 4cc57797dc0803501faff2aab04f690e2d5a7233 Mon Sep 17 00:00:00 2001 From: lizschwab Date: Thu, 30 Apr 2026 16:07:15 -0700 Subject: [PATCH 2/3] added tests --- packages/blockly/tests/mocha/block_test.js | 100 +++++++++++++++++---- packages/blockly/tests/mocha/input_test.js | 15 ---- 2 files changed, 81 insertions(+), 34 deletions(-) diff --git a/packages/blockly/tests/mocha/block_test.js b/packages/blockly/tests/mocha/block_test.js index 377b248e05e..6bbff085c2c 100644 --- a/packages/blockly/tests/mocha/block_test.js +++ b/packages/blockly/tests/mocha/block_test.js @@ -1896,26 +1896,88 @@ suite('Blocks', function () { }); suite('ARIA', function () { - setup(async function () { - this.block.setWarningText('Warning Text'); - this.block.initSvg(); - this.block.render(); - const icon = this.block.getIcon(Blockly.icons.WarningIcon.TYPE); - icon.performAction(); - await Blockly.renderManagement.finishQueuedRenders(); - - this.bubble = icon.getBubble(); - }); - test('Bubble has ARIA label', async function () { - assert.isTrue( - this.bubble.focusableElement.hasAttribute('aria-label'), - ); + suite('Bubble', function () { + setup(async function () { + this.block.setWarningText('Warning Text'); + this.block.initSvg(); + this.block.render(); + const icon = this.block.getIcon(Blockly.icons.WarningIcon.TYPE); + icon.performAction(); + await Blockly.renderManagement.finishQueuedRenders(); + + this.bubble = icon.getBubble(); + }); + test('Bubble has ARIA label', async function () { + assert.isTrue( + this.bubble.focusableElement.hasAttribute('aria-label'), + ); + }); + test('Bubble has ARIA role of group', async function () { + assert.equal( + this.bubble.focusableElement.getAttribute('role'), + 'group', + ); + }); }); - test('Bubble has ARIA role of group', async function () { - assert.equal( - this.bubble.focusableElement.getAttribute('role'), - 'group', - ); + suite('Input', function () { + test('Set input ARIA Label Provider', function () { + const customLabel = 'custom ARIA label'; + // Using a text input as it will return a default ARIA label + this.block + .appendValueInput('NAME') + .appendField(new Blockly.FieldTextInput('text'), 'NAME') + .setAriaLabelProvider((input) => customLabel); + + const label = this.block.getAriaLabel(); + + assert.include(label, customLabel); + assert.notInclude(label, 'text'); + }); + test('Set input ARIA Label Provider from JSON', function () { + const customLabel = 'custom ARIA label'; + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'input_aria_block', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'text', + }, + { + 'type': 'input_value', + 'name': 'NAME', + 'ariaLabelText': customLabel, + }, + ], + }, + ]); + + this.block = this.workspace.newBlock('input_aria_block'); + const label = this.block.getAriaLabel(); + + assert.include(label, customLabel); + }); + test('Set input ARIA Label Provider to null', function () { + const blockA = createRenderedBlock(this.workspace, 'row_block'); + const blockB = createRenderedBlock(this.workspace, 'row_block'); + + blockA + .appendValueInput('NAME') + .appendField(new Blockly.FieldTextInput('text'), 'NAME') + .setAriaLabelProvider(null); + blockB + .appendValueInput('NAME') + .appendField(new Blockly.FieldTextInput('text'), 'NAME'); + + const labelA = blockA.getAriaLabel(); + const labelB = blockB.getAriaLabel(); + + // The label should be the same between a block created with a null + // AriaLabelProvider and without setting the provider (the default label) + assert.equal(labelA, labelB); + }); }); }); }); diff --git a/packages/blockly/tests/mocha/input_test.js b/packages/blockly/tests/mocha/input_test.js index d110137543d..dfa30858e0e 100644 --- a/packages/blockly/tests/mocha/input_test.js +++ b/packages/blockly/tests/mocha/input_test.js @@ -293,19 +293,4 @@ suite('Inputs', function () { assert.deepEqual(this.dummy.fieldRow, [this.b, this.c]); }); }); - suite('ARIA', function () { - test('Set ARIA Label Provider', function () { - const customLabel = 'custom ARIA label'; - this.block - .appendValueInput('NAME') - .setAriaLabelProvider((input) => customLabel); - - const label = this.block.getAriaLabel(); - - assert.include(label, customLabel); - }); - test('Set ARIA Label Provider to null', function () { - // TODO: need a way to test that the getLabel() returns appropriately - }); - }); }); From db6a5b714161976fa2783b972ba5a5d7e6318754 Mon Sep 17 00:00:00 2001 From: lizschwab Date: Fri, 1 May 2026 11:48:40 -0700 Subject: [PATCH 3/3] moved tests to input test file --- packages/blockly/tests/mocha/block_test.js | 100 ++++----------------- packages/blockly/tests/mocha/input_test.js | 76 ++++++++++++++++ 2 files changed, 95 insertions(+), 81 deletions(-) diff --git a/packages/blockly/tests/mocha/block_test.js b/packages/blockly/tests/mocha/block_test.js index 6bbff085c2c..377b248e05e 100644 --- a/packages/blockly/tests/mocha/block_test.js +++ b/packages/blockly/tests/mocha/block_test.js @@ -1896,88 +1896,26 @@ suite('Blocks', function () { }); suite('ARIA', function () { - suite('Bubble', function () { - setup(async function () { - this.block.setWarningText('Warning Text'); - this.block.initSvg(); - this.block.render(); - const icon = this.block.getIcon(Blockly.icons.WarningIcon.TYPE); - icon.performAction(); - await Blockly.renderManagement.finishQueuedRenders(); - - this.bubble = icon.getBubble(); - }); - test('Bubble has ARIA label', async function () { - assert.isTrue( - this.bubble.focusableElement.hasAttribute('aria-label'), - ); - }); - test('Bubble has ARIA role of group', async function () { - assert.equal( - this.bubble.focusableElement.getAttribute('role'), - 'group', - ); - }); - }); - suite('Input', function () { - test('Set input ARIA Label Provider', function () { - const customLabel = 'custom ARIA label'; - // Using a text input as it will return a default ARIA label - this.block - .appendValueInput('NAME') - .appendField(new Blockly.FieldTextInput('text'), 'NAME') - .setAriaLabelProvider((input) => customLabel); - - const label = this.block.getAriaLabel(); - - assert.include(label, customLabel); - assert.notInclude(label, 'text'); - }); - test('Set input ARIA Label Provider from JSON', function () { - const customLabel = 'custom ARIA label'; - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'input_aria_block', - 'message0': '%1 %2', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'text', - }, - { - 'type': 'input_value', - 'name': 'NAME', - 'ariaLabelText': customLabel, - }, - ], - }, - ]); - - this.block = this.workspace.newBlock('input_aria_block'); - const label = this.block.getAriaLabel(); + setup(async function () { + this.block.setWarningText('Warning Text'); + this.block.initSvg(); + this.block.render(); + const icon = this.block.getIcon(Blockly.icons.WarningIcon.TYPE); + icon.performAction(); + await Blockly.renderManagement.finishQueuedRenders(); - assert.include(label, customLabel); - }); - test('Set input ARIA Label Provider to null', function () { - const blockA = createRenderedBlock(this.workspace, 'row_block'); - const blockB = createRenderedBlock(this.workspace, 'row_block'); - - blockA - .appendValueInput('NAME') - .appendField(new Blockly.FieldTextInput('text'), 'NAME') - .setAriaLabelProvider(null); - blockB - .appendValueInput('NAME') - .appendField(new Blockly.FieldTextInput('text'), 'NAME'); - - const labelA = blockA.getAriaLabel(); - const labelB = blockB.getAriaLabel(); - - // The label should be the same between a block created with a null - // AriaLabelProvider and without setting the provider (the default label) - assert.equal(labelA, labelB); - }); + this.bubble = icon.getBubble(); + }); + test('Bubble has ARIA label', async function () { + assert.isTrue( + this.bubble.focusableElement.hasAttribute('aria-label'), + ); + }); + test('Bubble has ARIA role of group', async function () { + assert.equal( + this.bubble.focusableElement.getAttribute('role'), + 'group', + ); }); }); }); diff --git a/packages/blockly/tests/mocha/input_test.js b/packages/blockly/tests/mocha/input_test.js index dfa30858e0e..d06444ec263 100644 --- a/packages/blockly/tests/mocha/input_test.js +++ b/packages/blockly/tests/mocha/input_test.js @@ -5,6 +5,7 @@ */ import {assert} from '../../node_modules/chai/index.js'; +import {createRenderedBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, sharedTestTeardown, @@ -293,4 +294,79 @@ suite('Inputs', function () { assert.deepEqual(this.dummy.fieldRow, [this.b, this.c]); }); }); + suite('ARIA', function () { + setup(function () { + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'row_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'input_value', + 'name': 'INPUT', + }, + ], + 'output': null, + }, + ]); + }); + test('Set input ARIA Label Provider', function () { + const customLabel = 'custom ARIA label'; + // Using a text input as it will return a default ARIA label + this.block + .appendValueInput('NAME') + .appendField(new Blockly.FieldTextInput('text'), 'NAME') + .setAriaLabelProvider((input) => customLabel); + + const label = this.block.getAriaLabel(); + + assert.include(label, customLabel); + assert.notInclude(label, 'text'); + }); + test('Set input ARIA Label Provider from JSON', function () { + const customLabel = 'custom ARIA label'; + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'input_aria_block', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'text', + }, + { + 'type': 'input_value', + 'name': 'NAME', + 'ariaLabelText': customLabel, + }, + ], + }, + ]); + + this.block = this.workspace.newBlock('input_aria_block'); + const label = this.block.getAriaLabel(); + + assert.include(label, customLabel); + }); + test('Set input ARIA Label Provider to null', function () { + const blockA = createRenderedBlock(this.workspace, 'row_block'); + const blockB = createRenderedBlock(this.workspace, 'row_block'); + + blockA + .appendValueInput('NAME') + .appendField(new Blockly.FieldTextInput('text'), 'NAME') + .setAriaLabelProvider(null); + blockB + .appendValueInput('NAME') + .appendField(new Blockly.FieldTextInput('text'), 'NAME'); + + const labelA = blockA.getAriaLabel(); + const labelB = blockB.getAriaLabel(); + + // The label should be the same between a block created with a null + // AriaLabelProvider and without setting the provider (the default label) + assert.equal(labelA, labelB); + }); + }); });