mirror of
https://github.com/google/blockly.git
synced 2026-05-20 19:10:13 +02:00
feat: Icon ARIA (#9805)
* feat: Icon ARIA * fix: code review * fix: remove same listener object
This commit is contained in:
@@ -809,6 +809,7 @@ export abstract class Bubble
|
||||
*/
|
||||
setAriaLabelProvider(provider: AriaLabelProvider | null): void {
|
||||
this.ariaLabelProvider = provider;
|
||||
this.recomputeAriaContext();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -216,31 +216,54 @@ suite('Comments', function () {
|
||||
setup(async function () {
|
||||
const block = this.workspace.newBlock('empty_block');
|
||||
block.setCommentText('test text');
|
||||
const commentIcon = block.getIcon(Blockly.icons.IconType.COMMENT);
|
||||
await commentIcon.setBubbleVisible(true);
|
||||
this.bubble = commentIcon.getBubble();
|
||||
this.icon = block.getIcon(Blockly.icons.IconType.COMMENT);
|
||||
await this.icon.setBubbleVisible(true);
|
||||
this.bubble = this.icon.getBubble();
|
||||
});
|
||||
function getFocusableAriaLabel(iFocusable) {
|
||||
return iFocusable.getFocusableElement().getAttribute('aria-label');
|
||||
}
|
||||
test('Bubble has ARIA label', 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, 'Comment');
|
||||
assert.include(label, 'test text');
|
||||
});
|
||||
test('Bubble has ARIA role of group', function () {
|
||||
assert.equal(this.bubble.focusableElement.getAttribute('role'), 'group');
|
||||
});
|
||||
test('Bubble can use AriaLabelProvider function', function () {
|
||||
this.bubble.setAriaLabelProvider(() => 'comment aria label');
|
||||
this.bubble.recomputeAriaContext();
|
||||
assert.equal(
|
||||
this.bubble.focusableElement.getAttribute('aria-label'),
|
||||
'comment aria label',
|
||||
);
|
||||
assert.equal(getFocusableAriaLabel(this.bubble), 'comment aria label');
|
||||
});
|
||||
test('Bubble can use AriaLabelProvider string', function () {
|
||||
this.bubble.setAriaLabelProvider('comment aria label');
|
||||
this.bubble.recomputeAriaContext();
|
||||
assert.equal(
|
||||
this.bubble.focusableElement.getAttribute('aria-label'),
|
||||
'comment aria label',
|
||||
);
|
||||
assert.equal(getFocusableAriaLabel(this.bubble), 'comment aria label');
|
||||
});
|
||||
test('Icon label changes when bubble is opened', async function () {
|
||||
const openLabel = getFocusableAriaLabel(this.icon);
|
||||
assert.equal(openLabel, 'Close Comment');
|
||||
await this.icon.setBubbleVisible(false);
|
||||
|
||||
const closedLabel = getFocusableAriaLabel(this.icon);
|
||||
assert.equal(closedLabel, 'Open Comment');
|
||||
});
|
||||
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');
|
||||
});
|
||||
test('Bubble ARIA label updates when comment text changes', function () {
|
||||
const initialLabel = getFocusableAriaLabel(this.bubble);
|
||||
assert.include(initialLabel, 'test text');
|
||||
|
||||
this.bubble.editor.setText('updated text');
|
||||
const updatedLabel = getFocusableAriaLabel(this.bubble);
|
||||
assert.include(updatedLabel, 'updated text');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -428,4 +428,21 @@ suite('Icon', function () {
|
||||
}
|
||||
});
|
||||
});
|
||||
suite('ARIA', function () {
|
||||
setup(function () {
|
||||
const workspace = createWorkspaceSvg();
|
||||
const block = createInitializedBlock(workspace);
|
||||
const icon = new TestIcon(block);
|
||||
block.addIcon(icon);
|
||||
this.element = icon.getFocusableElement();
|
||||
});
|
||||
test('Generic icons use button role', function () {
|
||||
const role = this.element.getAttribute('role');
|
||||
assert.equal(role, 'button');
|
||||
});
|
||||
test('Generic icons default to "Icon" ARIA label', function () {
|
||||
const label = this.element.getAttribute('aria-label');
|
||||
assert.equal(label, 'Icon');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,20 +88,51 @@ suite('Mutator', function () {
|
||||
setup(async function () {
|
||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||
const block = createRenderedBlock(this.workspace, 'controls_if');
|
||||
const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE);
|
||||
await icon.setBubbleVisible(true);
|
||||
this.bubble = icon.getBubble();
|
||||
this.icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE);
|
||||
await this.icon.setBubbleVisible(true);
|
||||
this.bubble = this.icon.getBubble();
|
||||
});
|
||||
|
||||
function getFocusableAriaLabel(iFocusable) {
|
||||
return iFocusable.getFocusableElement().getAttribute('aria-label');
|
||||
}
|
||||
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
});
|
||||
|
||||
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.equal(label, 'Block editor workspace');
|
||||
});
|
||||
test('Bubble has ARIA role of group', async function () {
|
||||
assert.equal(this.bubble.focusableElement.getAttribute('role'), 'group');
|
||||
test('Bubble has ARIA role of group', function () {
|
||||
assert.equal(
|
||||
this.bubble.getFocusableElement().getAttribute('role'),
|
||||
'group',
|
||||
);
|
||||
});
|
||||
test('Bubble uses function provider ARIA label when provided', function () {
|
||||
this.bubble.setAriaLabelProvider(() => 'Custom mutator label');
|
||||
const label = getFocusableAriaLabel(this.bubble);
|
||||
assert.equal(label, 'Custom mutator label');
|
||||
});
|
||||
test('Bubble uses string provider ARIA label when provided', function () {
|
||||
this.bubble.setAriaLabelProvider('Custom mutator label');
|
||||
const label = getFocusableAriaLabel(this.bubble);
|
||||
assert.equal(label, 'Custom mutator label');
|
||||
});
|
||||
test('Mutator icon label changes when bubble is opened', async function () {
|
||||
const openLabel = getFocusableAriaLabel(this.icon);
|
||||
assert.equal(openLabel, 'Close block editor');
|
||||
await this.icon.setBubbleVisible(false);
|
||||
|
||||
const closedLabel = getFocusableAriaLabel(this.icon);
|
||||
assert.equal(closedLabel, 'Edit this block');
|
||||
});
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user