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
16 changes: 16 additions & 0 deletions packages/blockly/core/comments/collapse_comment_bar_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import * as browserEvents from '../browser_events.js';
import {Msg} from '../msg.js';
import * as touch from '../touch.js';
import * as dom from '../utils/dom.js';
import {Svg} from '../utils/svg.js';
Expand Down Expand Up @@ -56,6 +57,7 @@ export class CollapseCommentBarButton extends CommentBarButton {
},
this.container,
);
this.recomputeAriaContext();
this.bindId = browserEvents.conditionalBind(
this.icon,
'pointerdown',
Expand Down Expand Up @@ -95,8 +97,22 @@ export class CollapseCommentBarButton extends CommentBarButton {
}

this.getCommentView().setCollapsed(!this.getCommentView().isCollapsed());
this.recomputeAriaContext();
this.workspace.hideChaff();

e?.stopPropagation();
}

/**
* Returns the ARIA label to use for this button (defaults to null). Note that this
* method will only be called and apply when recomputeAriaContext is called.
*
* @returns The ARIA label to use for this button, or null to use a default.
*/
protected getAriaLabel(): string {
const isCollapsed = this.getCommentView().isCollapsed();
return isCollapsed
? Msg['ARIA_LABEL_COMMENT_EXPAND']
: Msg['ARIA_LABEL_COMMENT_COLLAPSE'];
}
}
31 changes: 31 additions & 0 deletions packages/blockly/core/comments/comment_bar_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*/

import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import {Msg} from '../msg.js';
import * as aria from '../utils/aria.js';
import {Rect} from '../utils/rect.js';
import type {WorkspaceSvg} from '../workspace_svg.js';
import type {CommentView} from './comment_view.js';
Expand Down Expand Up @@ -102,4 +104,33 @@ export abstract class CommentBarButton implements IFocusableNode {
canBeFocused() {
return this.isVisible();
}

/**
* Recomputes the ARIA label and role for this button. Note that this is not
* automatically called during initialization and must be called once a button's
* focusable element (icon) is initialized. Implementations may also find it useful
* to call this if the button's label should be changed.
*/
protected recomputeAriaContext(): void {
if (!this.icon) return;

aria.setRole(this.icon, aria.Role.BUTTON);

const label = this.getAriaLabel();
aria.setState(
this.icon,
aria.State.LABEL,
label || Msg['ARIA_LABEL_BUTTON'],
);
}

/**
* Returns the ARIA label to use for this button (defaults to null). Note that this
* method will only be called and apply when recomputeAriaContext is called.
*
* @returns The ARIA label to use for this button, or null to use a default.
*/
protected getAriaLabel(): string | null {
return null;
}
}
24 changes: 24 additions & 0 deletions packages/blockly/core/comments/comment_editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {BlockSvg} from '../block_svg.js';
import * as browserEvents from '../browser_events.js';
import {getFocusManager} from '../focus_manager.js';
import {IFocusableNode} from '../interfaces/i_focusable_node.js';
import {IFocusableTree} from '../interfaces/i_focusable_tree.js';
import {Msg} from '../msg.js';
import * as touch from '../touch.js';
import * as aria from '../utils/aria.js';
import * as dom from '../utils/dom.js';
import {Rect} from '../utils/rect.js';
import {Size} from '../utils/size.js';
import {Svg} from '../utils/svg.js';
import * as svgMath from '../utils/svg_math.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import {RenderedWorkspaceComment} from './rendered_workspace_comment.js';

/**
* String added to the ID of a workspace comment to identify
Expand All @@ -40,6 +43,9 @@ export class CommentEditor implements IFocusableNode {
/** The current text of the comment. Updates on text area change. */
private text: string = '';

/** The parent object that owns this comment editor. */
private parent?: BlockSvg | RenderedWorkspaceComment;

constructor(
public workspace: WorkspaceSvg,
commentId: string,
Expand All @@ -57,6 +63,7 @@ export class CommentEditor implements IFocusableNode {
) as HTMLTextAreaElement;
this.textArea.setAttribute('tabindex', '-1');
this.textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR');
aria.setRole(this.textArea, aria.Role.TEXTBOX);
this.textArea.setAttribute(
'placeholder',
Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'],
Expand Down Expand Up @@ -129,6 +136,23 @@ export class CommentEditor implements IFocusableNode {
this.onTextChange();
}

/**
* Sets the parent object that owns this comment editor.
*
* @param newParent The parent of this comment editor.
* @internal
*/
setParent(newParent: BlockSvg | RenderedWorkspaceComment): void {
this.parent = newParent;
}

/**
* Returns the parent object that owns this comment editor, if any.
*/
getParent(): BlockSvg | RenderedWorkspaceComment | undefined {
return this.parent;
}

/**
* Triggers listeners when the text of the comment changes, either
* programmatically or manually by the user.
Expand Down
16 changes: 14 additions & 2 deletions packages/blockly/core/comments/comment_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

import * as browserEvents from '../browser_events.js';
import * as css from '../css.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node';
import {IRenderedElement} from '../interfaces/i_rendered_element.js';
import * as layers from '../layers.js';
import {Msg} from '../msg.js';
import * as touch from '../touch.js';
import * as aria from '../utils/aria.js';
import {Coordinate} from '../utils/coordinate.js';
import * as dom from '../utils/dom.js';
import * as drag from '../utils/drag.js';
Expand Down Expand Up @@ -107,6 +108,12 @@ export class CommentView implements IRenderedElement {
this.svgRoot = dom.createSvgElement(Svg.G, {
'class': 'blocklyComment blocklyEditable blocklyDraggable',
});
aria.setRole(this.svgRoot, aria.Role.BUTTON);
aria.setState(
this.svgRoot,
aria.State.ROLEDESCRIPTION,
Msg['ARIA_LABEL_COMMENT'],
);

this.highlightRect = this.createHighlightRect(this.svgRoot);

Expand All @@ -120,6 +127,11 @@ export class CommentView implements IRenderedElement {
} = this.createTopBar(this.svgRoot));

this.commentEditor = this.createTextArea();
aria.setState(
this.svgRoot,
aria.State.LABELLEDBY,
this.commentEditor.getFocusableElement().id,
);

this.resizeHandle = this.createResizeHandle(this.svgRoot, workspace);

Expand Down Expand Up @@ -235,7 +247,7 @@ export class CommentView implements IRenderedElement {
*
* @returns The FocusableNode representing the editor portion of this comment.
*/
getEditorFocusableNode(): IFocusableNode {
getEditorFocusableNode(): CommentEditor {
return this.commentEditor;
}

Expand Down
12 changes: 12 additions & 0 deletions packages/blockly/core/comments/delete_comment_bar_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import * as browserEvents from '../browser_events.js';
import {getFocusManager} from '../focus_manager.js';
import {Msg} from '../msg.js';
import * as touch from '../touch.js';
import * as dom from '../utils/dom.js';
import {Svg} from '../utils/svg.js';
Expand Down Expand Up @@ -56,6 +57,7 @@ export class DeleteCommentBarButton extends CommentBarButton {
},
container,
);
this.recomputeAriaContext();
this.bindId = browserEvents.conditionalBind(
this.icon,
'pointerdown',
Expand Down Expand Up @@ -104,4 +106,14 @@ export class DeleteCommentBarButton extends CommentBarButton {
getFocusManager().focusNode(this.workspace);
this.workspace.getAudioManager().play('delete');
}

/**
* Returns the ARIA label to use for this button (defaults to null). Note that this
* method will only be called and apply when recomputeAriaContext is called.
*
* @returns The ARIA label to use for this button, or null to use a default.
*/
protected getAriaLabel(): string {
return Msg['REMOVE_COMMENT'];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class RenderedWorkspaceComment
this.workspace = workspace;

this.view = new CommentView(workspace, this.id);
this.view.getEditorFocusableNode().setParent(this);
// Set the size to the default size as defined in the superclass.
this.view.setSize(this.getSize());
this.view.setEditable(this.isEditable());
Expand Down
1 change: 1 addition & 0 deletions packages/blockly/core/icons/comment_icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
this.getBubbleOwnerRect(),
this,
);
this.textInputBubble.getEditor().setParent(this.sourceBlock as BlockSvg);
this.textInputBubble.setText(this.getText());
this.textInputBubble.setSize(this.bubbleSize, true);
if (this.bubbleLocation) {
Expand Down
7 changes: 5 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-05-06 14:12:59.060731",
"lastupdated": "2026-05-07 12:03:23.440539",
"locale": "en",
"messagedocumentation" : "qqq"
},
Expand Down Expand Up @@ -514,5 +514,8 @@
"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"
"ICON_LABEL_WARNING_OPEN": "Close Warning",
"ARIA_LABEL_COMMENT": "Comment",
"ARIA_LABEL_COMMENT_COLLAPSE": "Collapse Comment",
"ARIA_LABEL_COMMENT_EXPAND": "Expand Comment"
}
5 changes: 4 additions & 1 deletion packages/blockly/msg/json/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -522,5 +522,8 @@
"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."
"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.",
"ARIA_LABEL_COMMENT": "ARIA label for a comment.",
"ARIA_LABEL_COMMENT_COLLAPSE": "ARIA label for an expanded comment's collapse button.",
"ARIA_LABEL_COMMENT_EXPAND": "ARIA label for a collapsed comment's expand button."
}
11 changes: 10 additions & 1 deletion packages/blockly/msg/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2037,4 +2037,13 @@ Blockly.Msg.ICON_LABEL_MUTATOR_OPEN = 'Close block editor';
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';
Blockly.Msg.ICON_LABEL_WARNING_OPEN = 'Close Warning';
/** @type {string} */
/// ARIA label for a comment.
Blockly.Msg.ARIA_LABEL_COMMENT = 'Comment';
/** @type {string} */
/// ARIA label for an expanded comment's collapse button.
Blockly.Msg.ARIA_LABEL_COMMENT_COLLAPSE = 'Collapse Comment';
/** @type {string} */
/// ARIA label for a collapsed comment's expand button.
Blockly.Msg.ARIA_LABEL_COMMENT_EXPAND = 'Expand Comment';
88 changes: 88 additions & 0 deletions packages/blockly/tests/mocha/comment_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ suite('Comments', function () {
function getFocusableAriaLabel(iFocusable) {
return iFocusable.getFocusableElement().getAttribute('aria-label');
}
function getFocusableAriaRole(iFocusable) {
return iFocusable.getFocusableElement().getAttribute('role');
}
function getFocusableAriaDescription(iFocusable) {
return iFocusable
.getFocusableElement()
.getAttribute('aria-roledescription');
}
test('Bubble has ARIA label', function () {
assert.isTrue(this.bubble.focusableElement.hasAttribute('aria-label'));
});
Expand Down Expand Up @@ -265,5 +273,85 @@ suite('Comments', function () {
const updatedLabel = getFocusableAriaLabel(this.bubble);
assert.include(updatedLabel, 'updated text');
});
suite('Comment Editor', function () {
test('Has ARIA role textbox', function () {
const editor = this.bubble.editor;
assert.equal(
editor.getFocusableElement().getAttribute('role'),
'textbox',
);
});
test('Parent is initialized', function () {
const editor = this.bubble.getEditor();
assert.exists(editor.getParent());
});
});
suite('Comment View', function () {
setup(function () {
// Create workspace comment to test comment view ARIA attributes, since block comments use bubbles.
this.workspaceComment = this.workspace.newComment();
this.workspaceComment.setText('workspace comment');
});
test('Has ARIA role button', function () {
const view = this.workspaceComment.view;
assert.equal(view.svgRoot.getAttribute('role'), 'button');
});
test('Has ARIA roledescription of comment', function () {
const view = this.workspaceComment.view;
assert.equal(
view.svgRoot.getAttribute('aria-roledescription'),
'Comment',
);
});
test('Comment view is labelled by comment editor', function () {
const view = this.workspaceComment.view;
const ownerId = view.commentEditor.getFocusableElement().id;
assert.equal(view.svgRoot.getAttribute('aria-labelledby'), ownerId);
});
});
suite('Comment Bar Buttons', function () {
setup(function () {
this.comment = this.workspace.newComment();
this.comment.setText('test comment');

this.view = this.comment.view;
});
function getButtonLabel(button) {
return button.getFocusableElement().getAttribute('aria-label');
}
test('Buttons have ARIA role button', function () {
for (const button of this.view.getCommentBarButtons()) {
assert.equal(
button.getFocusableElement().getAttribute('role'),
'button',
);
}
});
test('Delete button has correct ARIA label', function () {
assert.equal(getButtonLabel(this.view.deleteButton), 'Remove Comment');
});
test('Collapse button has initial ARIA label', function () {
assert.include(
getButtonLabel(this.view.foldoutButton),
'Collapse Comment',
);
});
test('Collapse button updates ARIA label when toggled', function () {
const initial = getButtonLabel(this.view.foldoutButton);
assert.include(initial, 'Collapse Comment');

this.view.foldoutButton.performAction();

const updated = getButtonLabel(this.view.foldoutButton);
assert.include(updated, 'Expand Comment');
});
test('Buttons recompute ARIA context after creation', function () {
for (const button of this.view.getCommentBarButtons()) {
assert.isNotNull(
button.getFocusableElement().getAttribute('aria-label'),
);
}
});
});
});
});