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
1 change: 1 addition & 0 deletions packages/blockly/core/bubbles/bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,7 @@ export abstract class Bubble
*/
setAriaLabelProvider(provider: AriaLabelProvider | null): void {
this.ariaLabelProvider = provider;
this.recomputeAriaContext();
}

/**
Expand Down
12 changes: 12 additions & 0 deletions packages/blockly/core/bubbles/textinput_bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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. */
Expand Down Expand Up @@ -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(`
Expand Down
20 changes: 20 additions & 0 deletions packages/blockly/core/icons/comment_icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -336,6 +337,9 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
'comment',
),
);
if (this.svgRoot) {
this.recomputeAriaContext();
}
}

/** See IHasBubble.getBubble. */
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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. */
Expand Down
27 changes: 27 additions & 0 deletions packages/blockly/core/icons/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
}
18 changes: 18 additions & 0 deletions packages/blockly/core/icons/mutator_icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -202,6 +206,7 @@ export class MutatorIcon extends Icon implements IHasBubble {
'mutator',
),
);
this.recomputeAriaContext();
}

/** See IHasBubble.getBubble. */
Expand Down Expand Up @@ -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'];
}
}
18 changes: 18 additions & 0 deletions packages/blockly/core/icons/warning_icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -197,6 +201,7 @@ export class WarningIcon extends Icon implements IHasBubble {
'warning',
),
);
this.recomputeAriaContext();
}

/** See IHasBubble.getBubble. */
Expand Down Expand Up @@ -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'];
}
}
13 changes: 11 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-30 15:41:41.211465",
"lastupdated": "2026-05-01 14:09:40.345417",
"locale": "en",
"messagedocumentation" : "qqq"
},
Expand Down Expand Up @@ -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"
}
11 changes: 10 additions & 1 deletion packages/blockly/msg/json/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <button> elements in your language.",
"ARIA_LABEL_HEADING": "Part of an aria label for an element that indicates it is a heading, but for technial reasons cannot be given a role of heading. Ideally, this would match the localized name for what screenreaders announce for <h1> elements in your language.",
"BUBBLE_LABEL_DEFAULT": "Default label for bubbles. This is only used if a bubble is created without a label provider."
"BUBBLE_LABEL_DEFAULT": "Default label for bubbles. This is only used if a bubble is created without a label provider.",
"BUBBLE_LABEL_COMMENT": "Label for a comment bubble. Placeholder corresponds to the content of the comment. \n\nParameters:\n* %1 - the content of the comment \n\nExamples:\n* 'Comment: This block does something important.'",
"BUBBLE_LABEL_WARNING": "Label for a warning bubble. Placeholder corresponds to the content of the warning. \n\nParameters:\n* %1 - the content of the warning \n\nExamples:\n* 'Warning: Something went wrong with this block.'",
"ICON_LABEL_DEFAULT": "Label for an icon, used by screen readers to identify it.",
"ICON_LABEL_COMMENT_CLOSED": "Label for an icon, used by screen readers to identify a closed comment. Clicking on the icon opens the comment's bubble, which allows the user to read the comment.",
"ICON_LABEL_COMMENT_OPEN": "Label for an icon, used by screen readers to identify an open comment. Clicking on the icon closes the comment's bubble.",
"ICON_LABEL_MUTATOR_CLOSED": "Label for an icon, used by screen readers to identify a closed mutator. Clicking on the icon opens the mutator's bubble, which allows the user to edit the block's structure.",
"ICON_LABEL_MUTATOR_OPEN": "Label for an icon, used by screen readers to identify an open mutator. Clicking on the icon closes the mutator's bubble.",
"ICON_LABEL_WARNING_CLOSED": "Label for an icon, used by screen readers to identify a closed warning. Clicking on the icon opens the warning's bubble, which allows the user read the warning.",
"ICON_LABEL_WARNING_OPEN": "Label for an icon, used by screen readers to identify an open warning. Clicking on the icon closes the warning's bubble."
}
31 changes: 31 additions & 0 deletions packages/blockly/msg/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2007,3 +2007,34 @@ Blockly.Msg.ARIA_LABEL_HEADING = 'heading';
/** @type {string} */
/// Default label for bubbles. This is only used if a bubble is created without a label provider.
Blockly.Msg.BUBBLE_LABEL_DEFAULT = 'Bubble';
/** @type {string} */
/// Label for a comment bubble. Placeholder corresponds to the content of the comment.
/// \n\nParameters:\n* %1 - the content of the comment
/// \n\nExamples:\n* "Comment: This block does something important."
Blockly.Msg.BUBBLE_LABEL_COMMENT = 'Comment: %1';
/** @type {string} */
/// Label for a warning bubble. Placeholder corresponds to the content of the warning.
/// \n\nParameters:\n* %1 - the content of the warning
/// \n\nExamples:\n* "Warning: Something went wrong with this block."
Blockly.Msg.BUBBLE_LABEL_WARNING = 'Warning: %1';
/** @type {string} */
/// Label for an icon, used by screen readers to identify it.
Blockly.Msg.ICON_LABEL_DEFAULT = 'Icon';
/** @type {string} */
/// Label for an icon, used by screen readers to identify a closed comment. Clicking on the icon opens the comment's bubble, which allows the user to read the comment.
Blockly.Msg.ICON_LABEL_COMMENT_CLOSED = 'Open Comment';
/** @type {string} */
/// Label for an icon, used by screen readers to identify an open comment. Clicking on the icon closes the comment's bubble.
Blockly.Msg.ICON_LABEL_COMMENT_OPEN = 'Close Comment';
/** @type {string} */
/// Label for an icon, used by screen readers to identify a closed mutator. Clicking on the icon opens the mutator's bubble, which allows the user to edit the block's structure.
Blockly.Msg.ICON_LABEL_MUTATOR_CLOSED = 'Edit this block';
/** @type {string} */
/// Label for an icon, used by screen readers to identify an open mutator. Clicking on the icon closes the mutator's bubble.
Blockly.Msg.ICON_LABEL_MUTATOR_OPEN = 'Close block editor';
/** @type {string} */
/// Label for an icon, used by screen readers to identify a closed warning. Clicking on the icon opens the warning's bubble, which allows the user read the warning.
Blockly.Msg.ICON_LABEL_WARNING_CLOSED = 'Open Warning';
/** @type {string} */
/// Label for an icon, used by screen readers to identify an open warning. Clicking on the icon closes the warning's bubble.
Blockly.Msg.ICON_LABEL_WARNING_OPEN = 'Close Warning';
40 changes: 35 additions & 5 deletions packages/blockly/tests/mocha/block_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1897,26 +1897,56 @@ suite('Blocks', function () {

suite('ARIA', function () {
setup(async function () {
this.block.setWarningText('Warning Text');
this.block.setWarningText('Something went wrong');
this.block.initSvg();
this.block.render();
const icon = this.block.getIcon(Blockly.icons.WarningIcon.TYPE);
icon.performAction();
await Blockly.renderManagement.finishQueuedRenders();
this.icon = this.block.getIcon(Blockly.icons.WarningIcon.TYPE);
await this.icon.setBubbleVisible(true);

this.bubble = icon.getBubble();
this.bubble = this.icon.getBubble();
});
function getFocusableAriaLabel(iFocusable) {
return iFocusable.getFocusableElement().getAttribute('aria-label');
}
test('Bubble has ARIA label', async function () {
assert.isTrue(
this.bubble.focusableElement.hasAttribute('aria-label'),
);
});
test('Bubble has working ARIA label provider', function () {
const label = getFocusableAriaLabel(this.bubble);
assert.include(label, 'Warning');
assert.include(label, 'Something went wrong');
});
test('Bubble has ARIA role of group', async function () {
assert.equal(
this.bubble.focusableElement.getAttribute('role'),
'group',
);
});
test('Bubble uses function provider ARIA label when provided', function () {
this.bubble.setAriaLabelProvider(() => 'Custom warning label');
const label = getFocusableAriaLabel(this.bubble);
assert.equal(label, 'Custom warning label');
});
test('Bubble uses string provider ARIA label when provided', function () {
this.bubble.setAriaLabelProvider('Custom warning label');
const label = getFocusableAriaLabel(this.bubble);
assert.equal(label, 'Custom warning label');
});
test('Mutator icon label changes when bubble is opened', async function () {
const openLabel = getFocusableAriaLabel(this.icon);
assert.equal(openLabel, 'Close Warning');
await this.icon.setBubbleVisible(false);

const closedLabel = getFocusableAriaLabel(this.icon);
assert.equal(closedLabel, 'Open Warning');
});
test('Bubble uses default ARIA label when no provider is set', function () {
this.bubble.setAriaLabelProvider(null);
const label = getFocusableAriaLabel(this.bubble);
assert.equal(label, 'Bubble');
});
});
});

Expand Down
Loading