feat: allow setting custom role description for blocks (#9777)

* feat: allow setting custom role description for blocks

* fix: update test
This commit is contained in:
Maribeth Moffatt
2026-04-30 12:43:06 -04:00
committed by GitHub
parent 60f423bced
commit 771f9eabe1
8 changed files with 83 additions and 9 deletions
+35
View File
@@ -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';
@@ -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
@@ -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.
*
@@ -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 +
+7 -3
View File
@@ -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);
}
@@ -42,6 +42,7 @@ export interface JsonBlockDefinition {
inputsInline?: boolean;
tooltip?: string;
helpUrl?: string;
ariaRoleDescription?: string;
extensions?: string[];
mutator?: string;
enableContextMenu?: boolean;
+2 -2
View File
@@ -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"
},
@@ -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",
+2 -2
View File
@@ -1,5 +1,5 @@
{
"@metadata": {
"@metadata": {
"authors": [
"Ajeje Brazorf",
"Amire80",
@@ -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.",
+3 -1
View File
@@ -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.
+1 -1
View File
@@ -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 () {
@@ -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'];
});
});
});