Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/blockly/core/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {Coordinate} from './utils/coordinate.js';
import * as deprecation from './utils/deprecation.js';
import * as idGenerator from './utils/idgenerator.js';
import * as parsing from './utils/parsing.js';
import {replaceMessageReferences} from './utils/parsing.js';
import {Size} from './utils/size.js';
import type {Workspace} from './workspace.js';

Expand Down Expand Up @@ -243,6 +244,9 @@ export class Block {
inputsInlineDefault?: boolean;
workspace: Workspace;

/** A custom provider for generating the aria role description for this block. */
private ariaRoleDescriptionProvider: string | (() => string) | undefined;

/**
* @param workspace The block's workspace.
* @param prototypeName Name of the language object containing type-specific
Expand Down Expand Up @@ -1548,6 +1552,32 @@ export class Block {
}
}

/**
* Set a custom aria role description provider for this block. If not set,
* uses a default provider based on the block's properties (e.g. whether it has
* inputs, outputs, etc.).
*
* @param description The description or function to provide the description.
* If a string, we'll replace message references in the string, e.g.
* `%{BKY_CUSTOM_MESSAGE}` will be replaced with the value of
* `Blockly.Msg['CUSTOM_MESSAGE']`.}'
*/
setAriaRoleDescriptionProvider(description: string | (() => string)) {
this.ariaRoleDescriptionProvider = description;
}

/**
* @returns The custom string to use as the role description for this block,
* or undefined if no custom description is set.
*/
getAriaRoleDescription(): string | undefined {
if (!this.ariaRoleDescriptionProvider) return undefined;
if (typeof this.ariaRoleDescriptionProvider === 'function') {
return this.ariaRoleDescriptionProvider();
}
return replaceMessageReferences(this.ariaRoleDescriptionProvider);
}

/**
* Create a human-readable text representation of this block and any children.
*
Expand Down Expand Up @@ -1802,6 +1832,11 @@ export class Block {
const localizedValue = parsing.replaceMessageReferences(rawValue);
this.setHelpUrl(localizedValue);
}

if (json['ariaRoleDescription'] !== undefined) {
this.setAriaRoleDescriptionProvider(json['ariaRoleDescription']);
}

if (typeof json['extensions'] === 'string') {
console.warn(
warningPrefix +
Expand Down
10 changes: 7 additions & 3 deletions packages/blockly/core/block_aria_composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,17 @@ export function configureAriaRole(block: BlockSvg) {
setRole(focusableElement, Role.FIGURE);
}

let roleDescription = Msg['BLOCK_LABEL_STATEMENT'];
if (block.statementInputCount) {
let roleDescription;
const customDescription = block.getAriaRoleDescription();
if (customDescription) {
roleDescription = customDescription;
} else if (block.statementInputCount) {
roleDescription = Msg['BLOCK_LABEL_CONTAINER'];
} else if (block.outputConnection) {
roleDescription = Msg['BLOCK_LABEL_VALUE'];
} else {
roleDescription = Msg['BLOCK_LABEL_STATEMENT'];
}

setState(focusableElement, State.ROLEDESCRIPTION, roleDescription);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface JsonBlockDefinition {
inputsInline?: boolean;
tooltip?: string;
helpUrl?: string;
ariaRoleDescription?: string;
extensions?: string[];
mutator?: string;
enableContextMenu?: boolean;
Expand Down
4 changes: 2 additions & 2 deletions packages/blockly/msg/json/en.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"@metadata": {
"author": "Ellen Spertus <ellen.spertus@gmail.com>",
"lastupdated": "2026-04-29 12:42:30.774691",
"lastupdated": "2026-04-29 16:09:43.926632",
"locale": "en",
"messagedocumentation" : "qqq"
},
Expand Down Expand Up @@ -474,7 +474,7 @@
"BLOCK_LABEL_REPLACEABLE": "replaceable",
"BLOCK_LABEL_HAS_INPUT": "has input",
"BLOCK_LABEL_HAS_INPUTS": "has inputs",
"BLOCK_LABEL_STATEMENT": "statement",
"BLOCK_LABEL_STATEMENT": "command",
"BLOCK_LABEL_CONTAINER": "container",
"BLOCK_LABEL_VALUE": "value",
"BLOCK_LABEL_STACK_BLOCKS": "%1 stack blocks",
Expand Down
4 changes: 2 additions & 2 deletions packages/blockly/msg/json/qqq.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"@metadata": {
"@metadata": {
"authors": [
"Ajeje Brazorf",
"Amire80",
Expand Down Expand Up @@ -482,7 +482,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_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.",
"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.",
"BLOCK_LABEL_STACK_BLOCKS": "Accessibility label for a block that indicates it is a stack of two or more blocks.",
Expand Down
4 changes: 3 additions & 1 deletion packages/blockly/msg/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -1880,7 +1880,9 @@ Blockly.Msg.BLOCK_LABEL_HAS_INPUTS = 'has inputs';
/** @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.
Blockly.Msg.BLOCK_LABEL_STATEMENT = 'statement';
/// "command" here is used in the sense of a computer command, or a
/// command block in Scratch.
Blockly.Msg.BLOCK_LABEL_STATEMENT = 'command';
/** @type {string} */
/// 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.
Expand Down
2 changes: 1 addition & 1 deletion packages/blockly/tests/mocha/aria_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ suite('ARIA', function () {
block.getFocusableElement(),
Blockly.utils.aria.State.ROLEDESCRIPTION,
);
assert.equal(roleDescription, 'statement');
assert.equal(roleDescription, 'command');
});

test('Value blocks have correct role description', function () {
Expand Down
32 changes: 32 additions & 0 deletions packages/blockly/tests/mocha/block_json_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -774,4 +774,36 @@ suite('Block JSON initialization', function () {
});
});
});
suite('blockFromJson', function () {
test('Custom aria role description', function () {
const testBlockDefinition = {
'type': 'test_block',
'ariaRoleDescription': 'Custom aria description',
};
Blockly.common.defineBlocksWithJsonArray([testBlockDefinition]);
const block = this.workspace.newBlock('test_block');
assert.equal(
block.getAriaRoleDescription(),
'Custom aria description',
'Expected getAriaRoleDescription to return the custom description.',
);
delete Blockly.Blocks['test_block'];
});
test('Custom aria role description with message reference', function () {
const testBlockDefinition = {
'type': 'test_block',
'ariaRoleDescription': '%{BKY_CUSTOM_ROLE_DESCRIPTION}',
};
Blockly.Msg['CUSTOM_ROLE_DESCRIPTION'] = 'Custom aria description';
Blockly.common.defineBlocksWithJsonArray([testBlockDefinition]);
const block = this.workspace.newBlock('test_block');
assert.equal(
block.getAriaRoleDescription(),
'Custom aria description',
'Expected getAriaRoleDescription to return the custom description.',
);
delete Blockly.Blocks['test_block'];
delete Blockly.Msg['CUSTOM_ROLE_DESCRIPTION'];
});
});
});