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
4 changes: 2 additions & 2 deletions packages/blockly/blocks/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
this.type === 'variables_get' ||
this.type === 'variables_get_reporter'
) {
const name = this.getField('VAR')!.getText();
const renameOption = {
text: Msg['RENAME_VARIABLE'],
text: Msg['RENAME_VARIABLE'].replace('%1', name),
enabled: true,
callback: renameOptionCallbackFactory(this),
};
const name = this.getField('VAR')!.getText();
const deleteOption = {
text: Msg['DELETE_VARIABLE'].replace('%1', name),
enabled: true,
Expand Down
4 changes: 2 additions & 2 deletions packages/blockly/blocks/variables_dynamic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
this.type === 'variables_get_dynamic' ||
this.type === 'variables_get_reporter_dynamic'
) {
const name = this.getField('VAR')!.getText();
const renameOption = {
text: Msg['RENAME_VARIABLE'],
text: Msg['RENAME_VARIABLE'].replace('%1', name),
enabled: true,
callback: renameOptionCallbackFactory(this),
};
const name = this.getField('VAR')!.getText();
const deleteOption = {
text: Msg['DELETE_VARIABLE'].replace('%1', name),
enabled: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/blockly/core/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ export abstract class Field<T = any>
* @param includeTypeInfo Whether to include the field's type information in
* the returned label, if available.
*/
computeAriaLabel(includeTypeInfo: boolean = false): string {
computeAriaLabel(includeTypeInfo: boolean = true): string {
const ariaTypeName = includeTypeInfo ? this.getAriaTypeName() : null;
let ariaValue = this.getAriaValue();
if (ariaValue === null || ariaValue === '') {
Expand Down
62 changes: 62 additions & 0 deletions packages/blockly/core/field_checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import './events/events_block_change.js';

import {Field, FieldConfig, FieldValidator} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {Msg} from './msg.js';
import {aria} from './utils.js';
import * as dom from './utils/dom.js';

type BoolString = 'TRUE' | 'FALSE';
Expand Down Expand Up @@ -111,6 +113,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
const textElement = this.getTextElement();
dom.addClass(this.fieldGroup_!, 'blocklyCheckboxField');
textElement.style.display = this.value_ ? 'block' : 'none';
this.recomputeAriaContext();
}

override render_() {
Expand Down Expand Up @@ -170,6 +173,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
if (this.textElement_) {
this.textElement_.style.display = this.value_ ? 'block' : 'none';
}
this.recomputeAriaContext();
}

/**
Expand Down Expand Up @@ -213,6 +217,39 @@ export class FieldCheckbox extends Field<CheckboxBool> {
return !!value;
}

/**
* Gets an ARIA-friendly label representation of this field's type.
*
* Implementations are responsible for, and encouraged to, return a localized
* version of the ARIA representation of the field's type.
*
* @returns An ARIA representation of the field's type or a default if it is
* unspecified.
*/
override getAriaTypeName(): string {
return this.ariaTypeName || Msg['ARIA_TYPE_FIELD_CHECKBOX'];
}

/**
* Gets an ARIA-friendly label representation of this field's value.
*
* Implementations are responsible for, and encouraged to, return a localized
* version of the ARIA representation of the field's value.
*
* The FieldCheckbox implementation is not used for the actual ARIA label of
* the field, since the checked state is already included in the ARIA checked
* state, but it is used for the ARIA label of its source block.
*
* @returns An ARIA representation of the field's text.
*/
override getAriaValue(): string | null {
// return null;
const checked = this.convertValueToBool(this.value_);
return checked
? Msg['FIELD_LABEL_CHECKBOX_CHECKED']
: Msg['FIELD_LABEL_CHECKBOX_UNCHECKED'];
}

/**
* Construct a FieldCheckbox from a JSON arg object.
*
Expand All @@ -228,6 +265,31 @@ export class FieldCheckbox extends Field<CheckboxBool> {
// 'override' the static fromJson method.
return new this(options.checked, undefined, options);
}

/**
* Recomputes the ARIA role and label for this field.
*/
protected recomputeAriaContext(): void {
const focusableElement = this.getClickTarget_();
if (!focusableElement) return;

if (this.getSourceBlock()?.isInFlyout) {
aria.setState(focusableElement, aria.State.HIDDEN, true);
return;
}

aria.setState(focusableElement, aria.State.HIDDEN, false);
aria.setRole(focusableElement, aria.Role.CHECKBOX);
const checked = this.convertValueToBool(this.value_);
aria.setState(focusableElement, aria.State.CHECKED, checked);

// Checkbox fields do not use this.computeAriaLabel(), because the
// included 'checked' or 'not checked' state in the ARIA label would
// be redundant with the ARIA checked state.
const label = this.getAriaTypeName();

aria.setState(focusableElement, aria.State.LABEL, label);
}
}

fieldRegistry.register('field_checkbox', FieldCheckbox);
Expand Down
6 changes: 3 additions & 3 deletions packages/blockly/core/field_dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ export class FieldDropdown extends Field<string> {
*
* @returns An ARIA representation of the field's text.
*/
override getAriaValue(): string | null {
override getAriaValue(): string {
// Note: This fallback is effectively unreachable since computeOptionAriaLabel
// always returns a non-empty string for non-separator options. It exists as a
// defensive safeguard.
Expand Down Expand Up @@ -920,7 +920,7 @@ export class FieldDropdown extends Field<string> {
/**
* Recomputes the ARIA role and label for this field.
*/
private recomputeAriaContext(): void {
protected recomputeAriaContext(): void {
const focusableElement = this.getFocusableElement();
if (!focusableElement) return;

Expand All @@ -934,7 +934,7 @@ export class FieldDropdown extends Field<string> {
// editing mode that can be activated.
aria.setRole(focusableElement, aria.Role.BUTTON);

const label = this.computeAriaLabel(false);
const label = this.computeAriaLabel(true);

aria.setState(focusableElement, aria.State.LABEL, label);
aria.setState(focusableElement, aria.State.HASPOPUP, 'listbox');
Expand Down
58 changes: 58 additions & 0 deletions packages/blockly/core/field_image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

import {Field, FieldConfig} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {Msg} from './msg.js';
import {aria} from './utils.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
import {Size} from './utils/size.js';
Expand Down Expand Up @@ -157,6 +159,8 @@ export class FieldImage extends Field<string> {
if (this.clickHandler) {
this.imageElement.style.cursor = 'pointer';
}

this.recomputeAriaContext();
}

override updateSize_() {}
Expand Down Expand Up @@ -186,6 +190,7 @@ export class FieldImage extends Field<string> {
if (this.imageElement) {
this.imageElement.setAttributeNS(dom.XLINK_NS, 'xlink:href', this.value_);
}
this.recomputeAriaContext();
}

/**
Expand Down Expand Up @@ -283,6 +288,59 @@ export class FieldImage extends Field<string> {
options,
);
}

/**
* Gets an ARIA-friendly label representation of this field's type.
*
* Implementations are responsible for, and encouraged to, return a localized
* version of the ARIA representation of the field's type.
*
* @returns An ARIA representation of the field's type or a default if it is
* unspecified.
*/
override getAriaTypeName(): string | null {
return this.ariaTypeName || Msg['ARIA_TYPE_FIELD_IMAGE'];
}

/**
* Gets an ARIA-friendly label representation of this field's value.
*
* Implementations are responsible for, and encouraged to, return a localized
* version of the ARIA representation of the field's value.
*
* @returns An ARIA representation of the field's text, or null if no text is
* currently defined or known for the field.
*/
override getAriaValue(): string | null {
return this.altText || null;
}

/**
* Recomputes the ARIA role and label for this field.
*/
protected recomputeAriaContext(): void {
const focusableElement = this.getClickTarget_();
if (!focusableElement) return;

const isInFlyout = this.getSourceBlock()?.isInFlyout;
if (isInFlyout) {
aria.setState(focusableElement, aria.State.HIDDEN, true);
return;
}

aria.setState(focusableElement, aria.State.HIDDEN, false);
// The button role is intended to indicate to users that the field has an
// editing mode that can be activated. The presentation role is used to
// prevent screen readers from reading the content or its descendants.
// Only clickable image fields are navigable.
aria.setRole(
focusableElement,
this.isClickable() ? aria.Role.BUTTON : aria.Role.PRESENTATION,
);

const label = this.computeAriaLabel(true);
aria.setState(focusableElement, aria.State.LABEL, label);
}
}

fieldRegistry.register('field_image', FieldImage);
Expand Down
4 changes: 2 additions & 2 deletions packages/blockly/core/field_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
/**
* Recomputes the ARIA role and label for this field.
*/
private recomputeAriaContext(): void {
protected recomputeAriaContext(): void {
const focusableElement = this.getClickTarget_();
if (!focusableElement) return;

Expand All @@ -853,7 +853,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
// editing mode that can be activated.
aria.setRole(focusableElement, aria.Role.BUTTON);

let label = this.computeAriaLabel(false);
let label = this.computeAriaLabel(true);

if (this.isCurrentlyEditable?.()) {
label = Msg['FIELD_LABEL_EDIT_PREFIX'].replace('%1', label);
Expand Down
2 changes: 2 additions & 0 deletions packages/blockly/core/field_label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import {Field, FieldConfig} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {aria} from './utils.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';

Expand Down Expand Up @@ -76,6 +77,7 @@ export class FieldLabel extends Field<string> {
}
if (this.fieldGroup_) {
dom.addClass(this.fieldGroup_, 'blocklyLabelField');
aria.setState(this.fieldGroup_, aria.State.HIDDEN, true);
}
}

Expand Down
3 changes: 0 additions & 3 deletions packages/blockly/core/field_number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
} from './field_input.js';
import * as fieldRegistry from './field_registry.js';
import {Msg} from './msg.js';
import * as aria from './utils/aria.js';
import * as dom from './utils/dom.js';

/**
Expand Down Expand Up @@ -300,11 +299,9 @@ export class FieldNumber extends FieldInput<number> {
// Set the accessibility state
if (this.min_ > -Infinity) {
htmlInput.min = `${this.min_}`;
aria.setState(htmlInput, aria.State.VALUEMIN, this.min_);
}
if (this.max_ < Infinity) {
htmlInput.max = `${this.max_}`;
aria.setState(htmlInput, aria.State.VALUEMAX, this.max_);
}
return htmlInput;
}
Expand Down
14 changes: 13 additions & 1 deletion packages/blockly/core/field_variable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ export class FieldVariable extends FieldDropdown {
];
}
options.push([
Msg['RENAME_VARIABLE'],
Msg['RENAME_VARIABLE'].replace('%1', name),
internalConstants.RENAME_VARIABLE_ID,
]);
if (Msg['DELETE_VARIABLE']) {
Expand All @@ -617,6 +617,18 @@ export class FieldVariable extends FieldDropdown {

return options;
}
/**
* Gets an ARIA-friendly label representation of this field's value.
*
* Implementations are responsible for, and encouraged to, return a localized
* version of the ARIA representation of the field's value.
*
* @returns An ARIA representation of the field's text.
*/
override getAriaValue(): string {
// Example: 'Variable "i"'
return Msg['FIELD_LABEL_VARIABLE'].replace('%1', super.getAriaValue());
}
}

fieldRegistry.register('field_variable', FieldVariable);
Expand Down
11 changes: 8 additions & 3 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-24 15:03:55.288228",
"lastupdated": "2026-04-29 08:57:47.670420",
"locale": "en",
"messagedocumentation" : "qqq"
},
Expand Down Expand Up @@ -29,7 +29,7 @@
"UNDO": "Undo",
"REDO": "Redo",
"CHANGE_VALUE_TITLE": "Change value:",
"RENAME_VARIABLE": "Rename variable...",
"RENAME_VARIABLE": "Rename the '%1' variable",
"RENAME_VARIABLE_TITLE": "Rename all '%1' variables to:",
"NEW_VARIABLE": "Create variable...",
"NEW_STRING_VARIABLE": "Create string variable...",
Expand Down Expand Up @@ -488,6 +488,11 @@
"ARIA_TYPE_FIELD_TEXT_INPUT": "text",
"ARIA_TYPE_FIELD_NUMBER": "number",
"ARIA_TYPE_FIELD_DROPDOWN": "dropdown",
"ARIA_TYPE_FIELD_IMAGE": "image",
"ARIA_TYPE_FIELD_CHECKBOX": "checkbox",
"FIELD_LABEL_EDIT_PREFIX": "Edit %1",
"FIELD_LABEL_OPTION_INDEX": "Option %1"
"FIELD_LABEL_OPTION_INDEX": "Option %1",
"FIELD_LABEL_CHECKBOX_CHECKED": "Checked",
"FIELD_LABEL_CHECKBOX_UNCHECKED": "Not checked",
"FIELD_LABEL_VARIABLE": "Variable '%1'"
}
7 changes: 6 additions & 1 deletion packages/blockly/msg/json/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,11 @@
"ARIA_TYPE_FIELD_TEXT_INPUT": "ARIA type name for a text input field, used by screen readers to identify the type of field.",
"ARIA_TYPE_FIELD_NUMBER": "ARIA type name for a number field, used by screen readers to identify the type of field.",
"ARIA_TYPE_FIELD_DROPDOWN": "ARIA type name for a dropdown field, used by screen readers to identify the type of field.",
"ARIA_TYPE_FIELD_IMAGE": "ARIA type name of an image field, used by screen readers to identify the type of field.",
"ARIA_TYPE_FIELD_CHECKBOX": "ARIA type name of an checkbox field, used by screen readers to identify the type of field.",
"FIELD_LABEL_EDIT_PREFIX": "Label for an editable field, used by screen readers to identify fields that can be edited by the user. Placeholder corresponds to the label of the field's value. \n\nParameters:\n* %1 - the label of the field's value \n\nExamples:\n* 'Edit 5'\n* 'Edit item'",
"FIELD_LABEL_OPTION_INDEX": "Label for an unlabeled dropdown field option, used by screen readers to identify options in a dropdown field. Placeholder corresponds to the index of the option in the dropdown, starting at 1. \n\nParameters:\n* %1 - the index of the option in the dropdown, starting at 1 \n\nExamples:\n* 'Option 1'\n* 'Option 2'"
"FIELD_LABEL_OPTION_INDEX": "Label for an unlabeled dropdown field option, used by screen readers to identify options in a dropdown field. Placeholder corresponds to the index of the option in the dropdown, starting at 1. \n\nParameters:\n* %1 - the index of the option in the dropdown, starting at 1 \n\nExamples:\n* 'Option 1'\n* 'Option 2'",
"FIELD_LABEL_CHECKBOX_CHECKED": "Label for a checked checkbox field, used by screen readers to identify the state of a checkbox field.",
"FIELD_LABEL_CHECKBOX_UNCHECKED": "Label for an unchecked checkbox field, used by screen readers to identify the state of a checkbox field.",
"FIELD_LABEL_VARIABLE": "Label for a variable field option, used by screen readers to identify the options in a variable dropdown field. \n\nParameters:\n* %1 - the name of the variable represented by the option \n\nExamples:\n* 'Variable 'item''\n* 'Variable 'x''"
}
Loading