Skip to content
19 changes: 11 additions & 8 deletions blocks/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'BOOL',
'ariaTypeName': 'Boolean',
'options': [
['%{BKY_LOGIC_BOOLEAN_TRUE}', 'TRUE'],
['%{BKY_LOGIC_BOOLEAN_FALSE}', 'FALSE'],
Expand Down Expand Up @@ -117,13 +118,14 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'OP',
'ariaTypeName': 'Operator',
'options': [
['=', 'EQ'],
['\u2260', 'NEQ'],
['\u200F<', 'LT'],
['\u200F\u2264', 'LTE'],
['\u200F>', 'GT'],
['\u200F\u2265', 'GTE'],
['=', 'EQ', 'Equals'],
['\u2260', 'NEQ', 'Does not equal'],
['\u200F<', 'LT', 'Less than'],
['\u200F\u2264', 'LTE', 'Less than or equal to'],
['\u200F>', 'GT', 'Greater than'],
['\u200F\u2265', 'GTE', 'Greater than or equal to'],
],
},
{
Expand All @@ -150,9 +152,10 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'OP',
'ariaTypeName': 'Boolean operator',
'options': [
['%{BKY_LOGIC_OPERATION_AND}', 'AND'],
['%{BKY_LOGIC_OPERATION_OR}', 'OR'],
['%{BKY_LOGIC_OPERATION_AND}', 'AND', 'And'],
['%{BKY_LOGIC_OPERATION_OR}', 'OR', 'Or'],
],
},
{
Expand Down
18 changes: 14 additions & 4 deletions blocks/loops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'MODE',
'ariaTypeName': 'Repeat type',
'options': [
['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}', 'WHILE'],
['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}', 'UNTIL'],
['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}', 'WHILE', 'While'],
['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}', 'UNTIL', 'Until'],
],
},
{
Expand Down Expand Up @@ -199,9 +200,18 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'FLOW',
'ariaTypeName': 'Continue type',
'options': [
['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}', 'BREAK'],
['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}', 'CONTINUE'],
[
'%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}',
'BREAK',
'Break loop',
],
[
'%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}',
'CONTINUE',
'Continue loop',
],
],
},
],
Expand Down
88 changes: 49 additions & 39 deletions blocks/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([
'type': 'field_number',
'name': 'NUM',
'value': 0,
'ariaName': 'Number',
'ariaTypeName': 'Number',
},
],
'output': 'Number',
Expand All @@ -55,7 +55,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'OP',
'ariaName': 'Arithmetic operation',
'ariaTypeName': 'Operator',
'options': [
['%{BKY_MATH_ADDITION_SYMBOL}', 'ADD', 'Plus'],
['%{BKY_MATH_SUBTRACTION_SYMBOL}', 'MINUS', 'Minus'],
Expand Down Expand Up @@ -85,14 +85,15 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'OP',
'ariaTypeName': 'Function',
'options': [
['%{BKY_MATH_SINGLE_OP_ROOT}', 'ROOT'],
['%{BKY_MATH_SINGLE_OP_ABSOLUTE}', 'ABS'],
['-', 'NEG'],
['ln', 'LN'],
['log10', 'LOG10'],
['e^', 'EXP'],
['10^', 'POW10'],
['%{BKY_MATH_SINGLE_OP_ROOT}', 'ROOT', 'Square root of '],
['%{BKY_MATH_SINGLE_OP_ABSOLUTE}', 'ABS', 'Absolute value of'],
['-', 'NEG', 'Negative value of'],
['ln', 'LN', 'Natural logarithm of'],
['log10', 'LOG10', 'Logarithm base 10 of'],
['e^', 'EXP', 'e to the power of'],
['10^', 'POW10', '10 to the power of'],
],
},
{
Expand All @@ -115,13 +116,14 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'OP',
'ariaTypeName': 'Trigonometry function',
'options': [
['%{BKY_MATH_TRIG_SIN}', 'SIN'],
['%{BKY_MATH_TRIG_COS}', 'COS'],
['%{BKY_MATH_TRIG_TAN}', 'TAN'],
['%{BKY_MATH_TRIG_ASIN}', 'ASIN'],
['%{BKY_MATH_TRIG_ACOS}', 'ACOS'],
['%{BKY_MATH_TRIG_ATAN}', 'ATAN'],
['%{BKY_MATH_TRIG_SIN}', 'SIN', 'Sine of'],
['%{BKY_MATH_TRIG_COS}', 'COS', 'Cosine of'],
['%{BKY_MATH_TRIG_TAN}', 'TAN', 'Tangent of'],
['%{BKY_MATH_TRIG_ASIN}', 'ASIN', 'Arc sine of'],
['%{BKY_MATH_TRIG_ACOS}', 'ACOS', 'Arc cosine of'],
['%{BKY_MATH_TRIG_ATAN}', 'ATAN', 'Arc tangent of'],
],
},
{
Expand All @@ -144,13 +146,14 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'CONSTANT',
'ariaTypeName': 'Math constant',
'options': [
['\u03c0', 'PI'],
['e', 'E'],
['\u03c6', 'GOLDEN_RATIO'],
['sqrt(2)', 'SQRT2'],
['sqrt(\u00bd)', 'SQRT1_2'],
['\u221e', 'INFINITY'],
['\u03c0', 'PI', 'Pi'],
['e', 'E', "Euler's number"],
['\u03c6', 'GOLDEN_RATIO', 'Golden ratio'],
['sqrt(2)', 'SQRT2', 'Square root of 2'],
['sqrt(\u00bd)', 'SQRT1_2', 'Square root of one half'],
['\u221e', 'INFINITY', 'Infinity'],
],
},
],
Expand All @@ -174,14 +177,15 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'PROPERTY',
'ariaTypeName': 'Number comparison',
'options': [
['%{BKY_MATH_IS_EVEN}', 'EVEN'],
['%{BKY_MATH_IS_ODD}', 'ODD'],
['%{BKY_MATH_IS_PRIME}', 'PRIME'],
['%{BKY_MATH_IS_WHOLE}', 'WHOLE'],
['%{BKY_MATH_IS_POSITIVE}', 'POSITIVE'],
['%{BKY_MATH_IS_NEGATIVE}', 'NEGATIVE'],
['%{BKY_MATH_IS_DIVISIBLE_BY}', 'DIVISIBLE_BY'],
['%{BKY_MATH_IS_EVEN}', 'EVEN', 'Is even'],
['%{BKY_MATH_IS_ODD}', 'ODD', 'Is odd'],
['%{BKY_MATH_IS_PRIME}', 'PRIME', 'Is prime'],
['%{BKY_MATH_IS_WHOLE}', 'WHOLE', 'Is whole number'],
['%{BKY_MATH_IS_POSITIVE}', 'POSITIVE', 'Is positive'],
['%{BKY_MATH_IS_NEGATIVE}', 'NEGATIVE', 'Is negative'],
['%{BKY_MATH_IS_DIVISIBLE_BY}', 'DIVISIBLE_BY', 'Is divisible by'],
],
},
],
Expand Down Expand Up @@ -223,10 +227,11 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'OP',
'ariaTypeName': 'Rounding operation',
'options': [
['%{BKY_MATH_ROUND_OPERATOR_ROUND}', 'ROUND'],
['%{BKY_MATH_ROUND_OPERATOR_ROUNDUP}', 'ROUNDUP'],
['%{BKY_MATH_ROUND_OPERATOR_ROUNDDOWN}', 'ROUNDDOWN'],
['%{BKY_MATH_ROUND_OPERATOR_ROUND}', 'ROUND', 'Round to nearest'],
['%{BKY_MATH_ROUND_OPERATOR_ROUNDUP}', 'ROUNDUP', 'Round up'],
['%{BKY_MATH_ROUND_OPERATOR_ROUNDDOWN}', 'ROUNDDOWN', 'Round down'],
],
},
{
Expand All @@ -250,15 +255,20 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'OP',
'ariaTypeName': 'List operation',
'options': [
['%{BKY_MATH_ONLIST_OPERATOR_SUM}', 'SUM'],
['%{BKY_MATH_ONLIST_OPERATOR_MIN}', 'MIN'],
['%{BKY_MATH_ONLIST_OPERATOR_MAX}', 'MAX'],
['%{BKY_MATH_ONLIST_OPERATOR_AVERAGE}', 'AVERAGE'],
['%{BKY_MATH_ONLIST_OPERATOR_MEDIAN}', 'MEDIAN'],
['%{BKY_MATH_ONLIST_OPERATOR_MODE}', 'MODE'],
['%{BKY_MATH_ONLIST_OPERATOR_STD_DEV}', 'STD_DEV'],
['%{BKY_MATH_ONLIST_OPERATOR_RANDOM}', 'RANDOM'],
['%{BKY_MATH_ONLIST_OPERATOR_SUM}', 'SUM', 'Sum'],
['%{BKY_MATH_ONLIST_OPERATOR_MIN}', 'MIN', 'Minimum'],
['%{BKY_MATH_ONLIST_OPERATOR_MAX}', 'MAX', 'Maximum'],
['%{BKY_MATH_ONLIST_OPERATOR_AVERAGE}', 'AVERAGE', 'Average'],
['%{BKY_MATH_ONLIST_OPERATOR_MEDIAN}', 'MEDIAN', 'Median'],
['%{BKY_MATH_ONLIST_OPERATOR_MODE}', 'MODE', 'Mode'],
[
'%{BKY_MATH_ONLIST_OPERATOR_STD_DEV}',
'STD_DEV',
'Standard deviation',
],
['%{BKY_MATH_ONLIST_OPERATOR_RANDOM}', 'RANDOM', 'Random value'],
],
},
{
Expand Down
16 changes: 9 additions & 7 deletions blocks/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,10 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'END',
'ariaTypeName': 'Search direction',
'options': [
['%{BKY_TEXT_INDEXOF_OPERATOR_FIRST}', 'FIRST'],
['%{BKY_TEXT_INDEXOF_OPERATOR_LAST}', 'LAST'],
['%{BKY_TEXT_INDEXOF_OPERATOR_FIRST}', 'FIRST', 'First'],
['%{BKY_TEXT_INDEXOF_OPERATOR_LAST}', 'LAST', 'Last'],
],
},
{
Expand All @@ -171,12 +172,13 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_dropdown',
'name': 'WHERE',
'ariaTypeName': 'Search operation',
'options': [
['%{BKY_TEXT_CHARAT_FROM_START}', 'FROM_START'],
['%{BKY_TEXT_CHARAT_FROM_END}', 'FROM_END'],
['%{BKY_TEXT_CHARAT_FIRST}', 'FIRST'],
['%{BKY_TEXT_CHARAT_LAST}', 'LAST'],
['%{BKY_TEXT_CHARAT_RANDOM}', 'RANDOM'],
['%{BKY_TEXT_CHARAT_FROM_START}', 'FROM_START', 'From string start'],
['%{BKY_TEXT_CHARAT_FROM_END}', 'FROM_END', 'From string end'],
['%{BKY_TEXT_CHARAT_FIRST}', 'FIRST', 'First character'],
['%{BKY_TEXT_CHARAT_LAST}', 'LAST', 'Last character'],
['%{BKY_TEXT_CHARAT_RANDOM}', 'RANDOM', 'Random character'],
],
},
],
Expand Down
8 changes: 4 additions & 4 deletions core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,8 @@ export class BlockSvg
);
}

private computeAriaLabel(): string {
const {blockSummary, inputCount} = buildBlockSummary(this);
computeAriaLabel(verbose: boolean = false): string {
const {blockSummary, inputCount} = buildBlockSummary(this, verbose);
const inputSummary = inputCount
? ` ${inputCount} ${inputCount > 1 ? 'inputs' : 'input'}`
: '';
Expand Down Expand Up @@ -2051,7 +2051,7 @@ interface BlockSummary {
inputCount: number;
}

function buildBlockSummary(block: BlockSvg): BlockSummary {
function buildBlockSummary(block: BlockSvg, verbose: boolean): BlockSummary {
let inputCount = 0;
function recursiveInputSummary(
block: BlockSvg,
Expand All @@ -2066,7 +2066,7 @@ function buildBlockSummary(block: BlockSvg): BlockSummary {
if (field.EDITABLE && !field.isFullBlockField() && !isNestedInput) {
inputCount++;
}
return [field.getText() ?? field.getValue()];
return field.computeAriaLabel(verbose);
});
if (
input.isVisible() &&
Expand Down
49 changes: 46 additions & 3 deletions core/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,51 @@ export abstract class Field<T = any>
}
}

getAriaName(): string | null {
return this.config?.ariaName ?? null;
/**
* Gets a an ARIA-friendly label representation of this field's type.
*
* @returns An ARIA representation of the field's type or null if it is
* unspecified.
*/
getAriaTypeName(): string | null {
return this.config?.ariaTypeName ?? null;
}

/**
* Gets a an ARIA-friendly label representation of this field's value.
*
* @returns An ARIA representation of the field's value.
*/
abstract getAriaValue(): string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this not be abstract and have a default implementation that returned an empty string (or null)? I found myself going through and returning empty strings for quite a few custom fields in order to get up and running with this change, and having a default implementation would reduce the overhead.

Copy link
Collaborator Author

@BenHenning BenHenning Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was on the fence of going with a default implementation but this gives strong incentive to. Within core Blockly we more or less need to explicitly decide what to provide here in all cases so I didn't have enough context to decide on a default value.

Could you maybe provide some context on why so many of your custom fields have an empty string? I'd expect that to be atypical in general since I expect that basically all fields have a non-default value (except maybe text input when nothing has yet been inputted).


/**
* 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 context to convey information like the field's value. Example:
* checkboxes represent their checked/non-checked status (i.e. value) through
* a separate ARIA property.
*
* @param verbose Whether to include the field's type information in the
* returned label, if available.
*/
computeAriaLabel(verbose: boolean = false): string {
const components: Array<string | null> = [this.getAriaValue()];
if (verbose) {
components.push(this.getAriaTypeName());
}
return components.filter((item) => item !== null).join(', ');
}

/**
Expand Down Expand Up @@ -1429,7 +1472,7 @@ export interface FieldConfig {
type: string;
name?: string;
tooltip?: string;
ariaName?: string;
ariaTypeName?: string;
}

/**
Expand Down
10 changes: 9 additions & 1 deletion core/field_checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,18 @@ export class FieldCheckbox extends Field<CheckboxBool> {
this.recomputeAria();
}

override getAriaValue(): string {
return this.value_ ? 'checked' : 'not checked';
}

private recomputeAria() {
const element = this.getFocusableElement();
aria.setRole(element, aria.Role.CHECKBOX);
aria.setState(element, aria.State.LABEL, this.getAriaName() ?? 'Checkbox');
aria.setState(
element,
aria.State.LABEL,
this.getAriaTypeName() ?? 'Checkbox',
);
aria.setState(element, aria.State.CHECKED, !!this.value_);
}

Expand Down
13 changes: 5 additions & 8 deletions core/field_dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ export class FieldDropdown extends Field<string> {
this.recomputeAria();
}

override getAriaValue(): string {
return this.computeLabelForOption(this.selectedOption);
}

protected recomputeAria() {
if (!this.fieldGroup_) return; // There's no element to set currently.
const element = this.getFocusableElement();
Expand All @@ -214,14 +218,7 @@ export class FieldDropdown extends Field<string> {
aria.clearState(element, aria.State.CONTROLS);
}

const label = [
this.computeLabelForOption(this.selectedOption),
this.getAriaName(),
]
.filter((item) => !!item)
.join(', ');

aria.setState(element, aria.State.LABEL, label);
aria.setState(element, aria.State.LABEL, super.computeAriaLabel(true));
}

/**
Expand Down
Loading
Loading