diff --git a/packages/blockly/core/bubbles/bubble.ts b/packages/blockly/core/bubbles/bubble.ts index df42fa62051..f56d95f007d 100644 --- a/packages/blockly/core/bubbles/bubble.ts +++ b/packages/blockly/core/bubbles/bubble.ts @@ -809,6 +809,7 @@ export abstract class Bubble */ setAriaLabelProvider(provider: AriaLabelProvider | null): void { this.ariaLabelProvider = provider; + this.recomputeAriaContext(); } /** diff --git a/packages/blockly/core/bubbles/textinput_bubble.ts b/packages/blockly/core/bubbles/textinput_bubble.ts index 3ede92b7497..ca2685e44d0 100644 --- a/packages/blockly/core/bubbles/textinput_bubble.ts +++ b/packages/blockly/core/bubbles/textinput_bubble.ts @@ -63,6 +63,9 @@ export class TextInputBubble extends Bubble { /** View responsible for supporting text editing. */ private editor: CommentEditor; + private readonly textChangeListener = () => { + this.recomputeAriaContext(); + }; /** * @param workspace The workspace this bubble belongs to. * @param anchor The anchor location of the thing this bubble is attached to. @@ -85,6 +88,7 @@ export class TextInputBubble extends Bubble { this.contentContainer.appendChild(this.editor.getDom()); this.resizeGroup = this.createResizeHandle(this.svgRoot, workspace); this.setSize(this.DEFAULT_SIZE, true); + this.addTextChangeListener(this.textChangeListener); } /** @returns the text of this bubble. */ @@ -287,6 +291,14 @@ export class TextInputBubble extends Bubble { performAction() { getFocusManager().focusNode(this.getEditor()); } + + /** + * Dispose of this bubble. + */ + dispose() { + super.dispose(); + this.editor.removeTextChangeListener(this.textChangeListener); + } } Css.register(` diff --git a/packages/blockly/core/icons/comment_icon.ts b/packages/blockly/core/icons/comment_icon.ts index 8f5a82c0d15..eb485b062a2 100644 --- a/packages/blockly/core/icons/comment_icon.ts +++ b/packages/blockly/core/icons/comment_icon.ts @@ -13,6 +13,7 @@ import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import type {ISerializable} from '../interfaces/i_serializable.js'; +import {Msg} from '../msg.js'; import * as renderManagement from '../render_management.js'; import {Coordinate} from '../utils.js'; import * as dom from '../utils/dom.js'; @@ -336,6 +337,9 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { 'comment', ), ); + if (this.svgRoot) { + this.recomputeAriaContext(); + } } /** See IHasBubble.getBubble. */ @@ -376,6 +380,9 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { this.textInputBubble.addLocationChangeListener(() => this.onBubbleLocationChange(), ); + this.textInputBubble.setAriaLabelProvider(() => + Msg['BUBBLE_LABEL_COMMENT'].replace('%1', this.getText()), + ); } /** Hides any open bubbles owned by this comment. */ @@ -403,6 +410,19 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { private getBubbleOwnerRect(): Rect { return (this.sourceBlock as BlockSvg).getBoundingRectangleWithoutChildren(); } + + /** + * Returns the ARIA label to use for this icon (defaults to null). Note that this + * method will only be called during initialization by default, so dynamic changes + * to the icon's ARIA label need to be applied by calling recomputeAriaContext. + * + * @returns The ARIA label to use for this icon, or null to use a default. + */ + protected override getAriaLabel(): string | null { + return this.bubbleIsVisible() + ? Msg['ICON_LABEL_COMMENT_OPEN'] + : Msg['ICON_LABEL_COMMENT_CLOSED']; + } } /** The save state format for a comment icon. */ diff --git a/packages/blockly/core/icons/icon.ts b/packages/blockly/core/icons/icon.ts index 30d11543c8d..fefd320b334 100644 --- a/packages/blockly/core/icons/icon.ts +++ b/packages/blockly/core/icons/icon.ts @@ -12,8 +12,10 @@ import type {IContextMenu} from '../interfaces/i_contextmenu.js'; import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import {hasBubble} from '../interfaces/i_has_bubble.js'; import type {IIcon} from '../interfaces/i_icon.js'; +import {Msg} from '../msg.js'; import * as renderManagement from '../render_management.js'; import * as tooltip from '../tooltip.js'; +import {aria} from '../utils.js'; import {Coordinate} from '../utils/coordinate.js'; import * as dom from '../utils/dom.js'; import * as idGenerator from '../utils/idgenerator.js'; @@ -75,6 +77,7 @@ export abstract class Icon implements IIcon, IContextMenu { ); (this.svgRoot as any).tooltip = this; tooltip.bindMouseEvents(this.svgRoot); + this.recomputeAriaContext(); } dispose(): void { @@ -219,4 +222,28 @@ export abstract class Icon implements IIcon, IContextMenu { showContextMenu(e: PointerEvent) { (this.getSourceBlock() as BlockSvg).showContextMenu(e); } + + /** + * Recomputes the ARIA label and role for this icon. This is automatically called + * during initialization, but implementations may find it useful to call this if + * the icon's label should be changed. + */ + protected recomputeAriaContext(): void { + const element = this.getFocusableElement(); + if (!element) return; + aria.setRole(element, aria.Role.BUTTON); + const label = this.getAriaLabel() ?? Msg['ICON_LABEL_DEFAULT']; + aria.setState(element, aria.State.LABEL, label); + } + + /** + * Returns the ARIA label to use for this icon (defaults to null). Note that this + * method will only be called during initialization by default, so dynamic changes + * to the icon's ARIA label need to be applied by calling recomputeAriaContext. + * + * @returns The ARIA label to use for this icon, or null to use a default. + */ + protected getAriaLabel(): string | null { + return null; + } } diff --git a/packages/blockly/core/icons/mutator_icon.ts b/packages/blockly/core/icons/mutator_icon.ts index 7d001def9ec..b6c2e6a975e 100644 --- a/packages/blockly/core/icons/mutator_icon.ts +++ b/packages/blockly/core/icons/mutator_icon.ts @@ -15,6 +15,7 @@ import {isBlockChange, isBlockCreate} from '../events/predicates.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; import type {IHasBubble} from '../interfaces/i_has_bubble.js'; +import {Msg} from '../msg.js'; import * as renderManagement from '../render_management.js'; import {Coordinate} from '../utils/coordinate.js'; import * as dom from '../utils/dom.js'; @@ -184,6 +185,9 @@ export class MutatorIcon extends Icon implements IHasBubble { this.miniWorkspaceBubble?.addWorkspaceChangeListener( this.createMiniWorkspaceChangeListener(), ); + this.miniWorkspaceBubble.setAriaLabelProvider( + Msg['WORKSPACE_LABEL_MUTATOR_WORKSPACE'], + ); } else { this.miniWorkspaceBubble?.dispose(); this.miniWorkspaceBubble = null; @@ -202,6 +206,7 @@ export class MutatorIcon extends Icon implements IHasBubble { 'mutator', ), ); + this.recomputeAriaContext(); } /** See IHasBubble.getBubble. */ @@ -358,4 +363,17 @@ export class MutatorIcon extends Icon implements IHasBubble { getWorkspace(): WorkspaceSvg | undefined { return this.miniWorkspaceBubble?.getWorkspace(); } + + /** + * Returns the ARIA label to use for this icon (defaults to null). Note that this + * method will only be called during initialization by default, so dynamic changes + * to the icon's ARIA label need to be applied by calling recomputeAriaContext. + * + * @returns The ARIA label to use for this icon, or null to use a default. + */ + protected override getAriaLabel(): string | null { + return this.bubbleIsVisible() + ? Msg['ICON_LABEL_MUTATOR_OPEN'] + : Msg['ICON_LABEL_MUTATOR_CLOSED']; + } } diff --git a/packages/blockly/core/icons/warning_icon.ts b/packages/blockly/core/icons/warning_icon.ts index 2e00c10d74f..d0cc9e25369 100644 --- a/packages/blockly/core/icons/warning_icon.ts +++ b/packages/blockly/core/icons/warning_icon.ts @@ -12,6 +12,7 @@ import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; import type {IBubble} from '../interfaces/i_bubble.js'; import type {IHasBubble} from '../interfaces/i_has_bubble.js'; +import {Msg} from '../msg.js'; import * as renderManagement from '../render_management.js'; import {Size} from '../utils.js'; import {Coordinate} from '../utils/coordinate.js'; @@ -185,6 +186,9 @@ export class WarningIcon extends Icon implements IHasBubble { this, ); this.applyColour(); + this.textBubble.setAriaLabelProvider(() => + Msg['BUBBLE_LABEL_WARNING'].replace('%1', this.getText()), + ); } else { this.textBubble?.dispose(); this.textBubble = null; @@ -197,6 +201,7 @@ export class WarningIcon extends Icon implements IHasBubble { 'warning', ), ); + this.recomputeAriaContext(); } /** See IHasBubble.getBubble. */ @@ -224,4 +229,17 @@ export class WarningIcon extends Icon implements IHasBubble { const bbox = this.sourceBlock.getSvgRoot().getBBox(); return new Rect(bbox.y, bbox.y + bbox.height, bbox.x, bbox.x + bbox.width); } + + /** + * Returns the ARIA label to use for this icon (defaults to null). Note that this + * method will only be called during initialization by default, so dynamic changes + * to the icon's ARIA label need to be applied by calling recomputeAriaContext. + * + * @returns The ARIA label to use for this icon, or null to use a default. + */ + protected override getAriaLabel(): string | null { + return this.bubbleIsVisible() + ? Msg['ICON_LABEL_WARNING_OPEN'] + : Msg['ICON_LABEL_WARNING_CLOSED']; + } } diff --git a/packages/blockly/msg/json/en.json b/packages/blockly/msg/json/en.json index 1e10a41814d..79a6fb5d7b7 100644 --- a/packages/blockly/msg/json/en.json +++ b/packages/blockly/msg/json/en.json @@ -1,7 +1,7 @@ { "@metadata": { "author": "Ellen Spertus ", - "lastupdated": "2026-04-30 15:41:41.211465", + "lastupdated": "2026-05-01 14:09:40.345417", "locale": "en", "messagedocumentation" : "qqq" }, @@ -504,5 +504,14 @@ "FIELD_LABEL_VARIABLE": "Variable '%1'", "ARIA_LABEL_BUTTON": "button", "ARIA_LABEL_HEADING": "heading", - "BUBBLE_LABEL_DEFAULT": "Bubble" + "BUBBLE_LABEL_DEFAULT": "Bubble", + "BUBBLE_LABEL_COMMENT": "Comment: %1", + "BUBBLE_LABEL_WARNING": "Warning: %1", + "ICON_LABEL_DEFAULT": "Icon", + "ICON_LABEL_COMMENT_CLOSED": "Open Comment", + "ICON_LABEL_COMMENT_OPEN": "Close Comment", + "ICON_LABEL_MUTATOR_CLOSED": "Edit this block", + "ICON_LABEL_MUTATOR_OPEN": "Close block editor", + "ICON_LABEL_WARNING_CLOSED": "Open Warning", + "ICON_LABEL_WARNING_OPEN": "Close Warning" } diff --git a/packages/blockly/msg/json/qqq.json b/packages/blockly/msg/json/qqq.json index d88945f5c0a..f8bfd7132d1 100644 --- a/packages/blockly/msg/json/qqq.json +++ b/packages/blockly/msg/json/qqq.json @@ -512,5 +512,14 @@ "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''", "ARIA_LABEL_BUTTON": "Part of an aria label for an element that indicates it is a button, but for technical reasons cannot be give a role of button. Ideally, this would match the localized name for what screenreaders announce for