mirror of
https://github.com/google/blockly.git
synced 2026-05-01 17:40:11 +02:00
feat: make flyouts and workspace labels properly accessible (#9774)
* feat: make flyouts and workspace labels properly accessible * chore: update tests * chore: small refactor
This commit is contained in:
@@ -81,7 +81,11 @@ export function computeAriaLabel(
|
||||
export function configureAriaRole(block: BlockSvg) {
|
||||
setRole(block.getSvgRoot(), Role.PRESENTATION);
|
||||
const focusableElement = block.getFocusableElement();
|
||||
setRole(focusableElement, block.isInFlyout ? Role.LISTITEM : Role.FIGURE);
|
||||
if (!block.isInFlyout) {
|
||||
// blocks in the flyout have their role set by the Flyout's block inflater
|
||||
// don't overwrite it here
|
||||
setRole(focusableElement, Role.FIGURE);
|
||||
}
|
||||
|
||||
let roleDescription = Msg['BLOCK_LABEL_STATEMENT'];
|
||||
if (block.statementInputCount) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {IFlyout} from './interfaces/i_flyout.js';
|
||||
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
|
||||
import * as registry from './registry.js';
|
||||
import * as blocks from './serialization/blocks.js';
|
||||
import {aria} from './utils.js';
|
||||
import type {BlockInfo} from './utils/toolbox.js';
|
||||
import * as utilsXml from './utils/xml.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
@@ -67,7 +68,19 @@ export class BlockFlyoutInflater implements IFlyoutInflater {
|
||||
// Mark blocks as being inside a flyout. This is used to detect and
|
||||
// prevent the closure of the flyout if the user right-clicks on such
|
||||
// a block.
|
||||
block.getDescendants(false).forEach((b) => (b.isInFlyout = true));
|
||||
block.getDescendants(false).forEach((b) => {
|
||||
b.isInFlyout = true;
|
||||
const focusableElement = b.getFocusableElement();
|
||||
// blocks can't be focused if they're in a flyout and not top-level
|
||||
// nonfocusable blocks should be hidden from the aria tree
|
||||
aria.setState(focusableElement, aria.State.HIDDEN, true);
|
||||
aria.setRole(focusableElement, aria.Role.PRESENTATION);
|
||||
});
|
||||
// Since getDescencdants includes the root block, we need
|
||||
// to correct the role and hidden state for it.
|
||||
const focusableElement = block.getFocusableElement();
|
||||
aria.clearState(focusableElement, aria.State.HIDDEN);
|
||||
aria.setRole(focusableElement, aria.Role.LISTITEM);
|
||||
this.addBlockListeners(block);
|
||||
|
||||
return new FlyoutItem(block, BLOCK_TYPE);
|
||||
|
||||
@@ -24,12 +24,15 @@ import {getFocusManager} from './focus_manager.js';
|
||||
import {IAutoHideable} from './interfaces/i_autohideable.js';
|
||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
|
||||
import {isSelectableToolboxItem} from './interfaces/i_selectable_toolbox_item.js';
|
||||
import {FlyoutNavigator} from './keyboard_nav/navigators/flyout_navigator.js';
|
||||
import {Msg} from './msg.js';
|
||||
import type {Options} from './options.js';
|
||||
import * as registry from './registry.js';
|
||||
import * as renderManagement from './render_management.js';
|
||||
import {ScrollbarPair} from './scrollbar_pair.js';
|
||||
import {SEPARATOR_TYPE} from './separator_flyout_inflater.js';
|
||||
import * as aria from './utils/aria.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import * as idGenerator from './utils/idgenerator.js';
|
||||
import {Svg} from './utils/svg.js';
|
||||
@@ -312,6 +315,7 @@ export abstract class Flyout
|
||||
init(targetWorkspace: WorkspaceSvg) {
|
||||
this.targetWorkspace = targetWorkspace;
|
||||
this.workspace_.targetWorkspace = targetWorkspace;
|
||||
this.workspace_.setInitialAriaContext();
|
||||
|
||||
this.workspace_.scrollbar = new ScrollbarPair(
|
||||
this.workspace_,
|
||||
@@ -632,6 +636,7 @@ export abstract class Flyout
|
||||
this.width_ = 0;
|
||||
}
|
||||
this.reflow();
|
||||
this.updateAriaContext();
|
||||
eventUtils.setRecordUndo(true);
|
||||
this.workspace_.setResizesEnabled(true);
|
||||
|
||||
@@ -650,6 +655,53 @@ export abstract class Flyout
|
||||
this.workspace_.addChangeListener(this.reflowWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the aria attributes for the entire flyout dom.
|
||||
* This needs to do two things:
|
||||
* 1. Set aria-owns on the flyout's workspace canvas to include the ids of all
|
||||
* focusable elements in the flyout.
|
||||
* 2. Update the aria attributes on the flyout's workspace. This can't be done at workspace
|
||||
* creation because the workspace may not have all required information until the flyout
|
||||
* is fully shown.
|
||||
*/
|
||||
protected updateAriaContext() {
|
||||
// Set aria-owns on the flyout's workspace canvas to include the ids of all focusable elements in the flyout.
|
||||
// This is probably not necessary if the listitems are all direct descendants of the canvas, but
|
||||
// we can't know the dom structure of the flyout contents, so it's best to be explicit.
|
||||
const focusableIds = this.getContents()
|
||||
.map((item) => item.getElement())
|
||||
.filter((item) => item.canBeFocused())
|
||||
.map((item) => item.getFocusableElement().id);
|
||||
aria.setState(
|
||||
this.getWorkspace().getCanvas(),
|
||||
aria.State.OWNS,
|
||||
focusableIds.join(' '),
|
||||
);
|
||||
|
||||
// Update aria attributes on the flyout's workspace.
|
||||
// Only call a flyout's workspace a region if it's not auto-closing and not a mutator
|
||||
if (!this.targetWorkspace.isMutator && !this.autoClose) {
|
||||
aria.setRole(this.getWorkspace().svgGroup_, aria.Role.REGION);
|
||||
} else {
|
||||
aria.setRole(this.getWorkspace().svgGroup_, aria.Role.PRESENTATION);
|
||||
}
|
||||
|
||||
// the label for a flyout includes the category name if it's available
|
||||
const selectedItem = this.targetWorkspace.getToolbox()?.getSelectedItem();
|
||||
const selectedItemName =
|
||||
selectedItem && isSelectableToolboxItem(selectedItem)
|
||||
? selectedItem.getName()
|
||||
: '';
|
||||
const ariaLabel = Msg['WORKSPACE_LABEL_FLYOUT_WORKSPACE']
|
||||
.replace('%1', selectedItemName)
|
||||
.trim();
|
||||
aria.setState(this.getWorkspace().getCanvas(), aria.State.LABEL, ariaLabel);
|
||||
|
||||
// The block canvas is a list. The list items must be direct descendants of the list,
|
||||
// and the flyout may or may not be a region, so we set the role on the block canvas rather than the svgGroup_.
|
||||
aria.setRole(this.getWorkspace().getCanvas(), aria.Role.LIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the contents array and gaps array necessary to create the layout for
|
||||
* the flyout.
|
||||
|
||||
@@ -17,7 +17,8 @@ import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
|
||||
import type {IRenderedElement} from './interfaces/i_rendered_element.js';
|
||||
import {idGenerator} from './utils.js';
|
||||
import {Msg} from './msg.js';
|
||||
import {aria, idGenerator} from './utils.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import * as parsing from './utils/parsing.js';
|
||||
@@ -134,6 +135,7 @@ export class FlyoutButton
|
||||
},
|
||||
this.svgGroup!,
|
||||
);
|
||||
aria.setRole(shadow, aria.Role.PRESENTATION);
|
||||
}
|
||||
// Background rectangle.
|
||||
const rect = dom.createSvgElement(
|
||||
@@ -147,6 +149,7 @@ export class FlyoutButton
|
||||
},
|
||||
this.svgGroup!,
|
||||
);
|
||||
aria.setRole(rect, aria.Role.PRESENTATION);
|
||||
|
||||
const svgText = dom.createSvgElement(
|
||||
Svg.TEXT,
|
||||
@@ -170,6 +173,13 @@ export class FlyoutButton
|
||||
.getThemeManager()
|
||||
.subscribe(this.svgText, 'flyoutForegroundColour', 'fill');
|
||||
}
|
||||
aria.setRole(svgText, aria.Role.PRESENTATION);
|
||||
|
||||
// We add the word "heading" or "button" to the label so that they give appropriate hints
|
||||
// we can't use the corresponding roles because that overwrites the context of it being a list item.
|
||||
const ariaLabel = `${text}, ${this.isFlyoutLabel ? Msg['ARIA_LABEL_HEADING'] : Msg['ARIA_LABEL_BUTTON']}`;
|
||||
aria.setState(this.getFocusableElement(), aria.State.LABEL, ariaLabel);
|
||||
aria.setRole(this.getFocusableElement(), aria.Role.LISTITEM);
|
||||
|
||||
const fontSize = style.getComputedStyle(svgText, 'fontSize');
|
||||
const fontWeight = style.getComputedStyle(svgText, 'fontWeight');
|
||||
|
||||
@@ -8,6 +8,15 @@ export interface IFlyoutInflater {
|
||||
* Note that this method's interface is identical to that in ISerializer, to
|
||||
* allow for code reuse.
|
||||
*
|
||||
* You must ensure that any item created by this method has the appropriate
|
||||
* ARIA markup:
|
||||
* - The role of the element's focusable element should be set to `listitem`.
|
||||
* - The focusable element must have an `id` attribute.
|
||||
* - Any DOM parents of the focusable element should set their role to
|
||||
* `presentation` to avoid interfering with flyout list navigation.
|
||||
* - If the element is not focusable, it must be hidden from the ARIA tree.
|
||||
* Only do this if the content should be inaccessible to screenreaders.
|
||||
*
|
||||
* @param state A JSON representation of an element to inflate on the flyout.
|
||||
* @param flyout The flyout on whose workspace the inflated element
|
||||
* should be created. If the inflated element is an `IRenderedElement` it
|
||||
|
||||
@@ -214,6 +214,13 @@ export enum State {
|
||||
* Value: a number representing the minimum allowed value for a range widget.
|
||||
*/
|
||||
VALUEMIN = 'valuemin',
|
||||
|
||||
/**
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-owns
|
||||
*
|
||||
* Value: a space-separated list of element IDs that are owned by the current element.
|
||||
*/
|
||||
OWNS = 'owns',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,7 +58,7 @@ export function createSvgElement<T extends SVGElement>(
|
||||
): T {
|
||||
const e = document.createElementNS(SVG_NS, `${name}`) as T;
|
||||
/**
|
||||
* For svg and group (g) elements, we set the role to generic so that they are ignored by assistive technologies.
|
||||
* For svg and group (g) elements, we set the role to presentation so that they are ignored by assistive technologies.
|
||||
*/
|
||||
if (
|
||||
name === Svg.SVG.toString() ||
|
||||
@@ -66,7 +66,7 @@ export function createSvgElement<T extends SVGElement>(
|
||||
e.tagName === Svg.SVG.toString() ||
|
||||
e.tagName === Svg.G.toString()
|
||||
) {
|
||||
aria.setRole(e, aria.Role.GENERIC);
|
||||
aria.setRole(e, aria.Role.PRESENTATION);
|
||||
}
|
||||
for (const key in attrs) {
|
||||
e.setAttribute(key, `${attrs[key]}`);
|
||||
|
||||
@@ -698,6 +698,53 @@ export class WorkspaceSvg
|
||||
this.resizeHandlerWrapper = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets Aria labels, roles, etc. for the workspace depending on the type of workspace it is.
|
||||
*/
|
||||
setInitialAriaContext() {
|
||||
if (!this.svgGroup_) {
|
||||
throw new Error(
|
||||
'Must initialize svgGroup_ by calling `createDom` before calling setAriaContext',
|
||||
);
|
||||
}
|
||||
if (this.isFlyout) {
|
||||
// Flyouts have their aria attributes set when the flyout is shown.
|
||||
return;
|
||||
}
|
||||
aria.setRole(this.svgGroup_, aria.Role.REGION);
|
||||
if (this.isMutator) {
|
||||
aria.setState(
|
||||
this.svgGroup_,
|
||||
aria.State.LABEL,
|
||||
Msg['WORKSPACE_LABEL_MUTATOR_WORKSPACE'],
|
||||
);
|
||||
} else {
|
||||
// Main workspaces get labelled with how many stacks of blocks they contain
|
||||
// This will be updated in a change listener, but set it here in case there are blocks in the initial state of the workspace
|
||||
this.updateAriaLabel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the label on the workspace to reflect the number of top-level stacks in the workspace.
|
||||
*/
|
||||
private updateAriaLabel() {
|
||||
const numStacks = this.getTopBlocks(false).length;
|
||||
if (numStacks == 1) {
|
||||
aria.setState(
|
||||
this.svgGroup_,
|
||||
aria.State.LABEL,
|
||||
Msg['WORKSPACE_LABEL_1_STACK'],
|
||||
);
|
||||
} else {
|
||||
aria.setState(
|
||||
this.svgGroup_,
|
||||
aria.State.LABEL,
|
||||
Msg['WORKSPACE_LABEL_MANY_STACKS'].replace('%1', String(numStacks)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the workspace DOM elements.
|
||||
*
|
||||
@@ -722,13 +769,6 @@ export class WorkspaceSvg
|
||||
'class': 'blocklyWorkspace',
|
||||
'id': this.id,
|
||||
});
|
||||
if (injectionDiv) {
|
||||
aria.setState(
|
||||
this.svgGroup_,
|
||||
aria.State.LABEL,
|
||||
Msg['WORKSPACE_ARIA_LABEL'],
|
||||
);
|
||||
}
|
||||
|
||||
// Note that a <g> alone does not receive mouse events--it must have a
|
||||
// valid target inside it. If no background class is specified, as in the
|
||||
@@ -756,6 +796,16 @@ export class WorkspaceSvg
|
||||
this.svgBlockCanvas_ = this.layerManager.getBlockLayer();
|
||||
this.svgBubbleCanvas_ = this.layerManager.getBubbleLayer();
|
||||
|
||||
this.setInitialAriaContext();
|
||||
|
||||
if (!this.isFlyout && !this.isMutator) {
|
||||
// Set up a change listener to update the aria label on main workspace
|
||||
this.addChangeListener((e) => {
|
||||
if (e.isUiEvent) return;
|
||||
this.updateAriaLabel();
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isFlyout) {
|
||||
browserEvents.conditionalBind(
|
||||
this.svgGroup_,
|
||||
|
||||
@@ -394,7 +394,6 @@
|
||||
"PROCEDURES_IFRETURN_HELPURL": "https://c2.com/cgi/wiki?GuardClause",
|
||||
"PROCEDURES_IFRETURN_WARNING": "Warning: This block may be used only within a function definition.",
|
||||
"WORKSPACE_COMMENT_DEFAULT_TEXT": "Say something...",
|
||||
"WORKSPACE_ARIA_LABEL": "Blockly Workspace",
|
||||
"COLLAPSED_WARNINGS_WARNING": "Collapsed blocks contain warnings.",
|
||||
"DIALOG_OK": "OK",
|
||||
"DIALOG_CANCEL": "Cancel",
|
||||
@@ -456,6 +455,10 @@
|
||||
"KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Use the arrow keys to move, then %1 to accept the position",
|
||||
"KEYBOARD_NAV_COPIED_HINT": "Copied. Press %1 to paste.",
|
||||
"KEYBOARD_NAV_CUT_HINT": "Cut. Press %1 to paste.",
|
||||
"WORKSPACE_LABEL_1_STACK": "Blocks workspace. 1 stack of blocks",
|
||||
"WORKSPACE_LABEL_MANY_STACKS": "Blocks workspace. %1 stacks of blocks",
|
||||
"WORKSPACE_LABEL_MUTATOR_WORKSPACE": "Block editor workspace",
|
||||
"WORKSPACE_LABEL_FLYOUT_WORKSPACE": "%1 blocks",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_MANY": "%1 stacks of blocks%2 in workspace.",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_ONE": "One stack of blocks%2 in workspace.",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_ZERO": "No blocks%2 in workspace.",
|
||||
@@ -494,5 +497,7 @@
|
||||
"FIELD_LABEL_OPTION_INDEX": "Option %1",
|
||||
"FIELD_LABEL_CHECKBOX_CHECKED": "Checked",
|
||||
"FIELD_LABEL_CHECKBOX_UNCHECKED": "Not checked",
|
||||
"FIELD_LABEL_VARIABLE": "Variable '%1'"
|
||||
"FIELD_LABEL_VARIABLE": "Variable '%1'",
|
||||
"ARIA_LABEL_BUTTON": "button",
|
||||
"ARIA_LABEL_HEADING": "heading"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Ajeje Brazorf",
|
||||
"Amire80",
|
||||
@@ -402,7 +402,6 @@
|
||||
"PROCEDURES_IFRETURN_HELPURL": "{{Optional}} url - Information about guard clauses.",
|
||||
"PROCEDURES_IFRETURN_WARNING": "warning - This appears if the user tries to use this block outside of a function definition.",
|
||||
"WORKSPACE_COMMENT_DEFAULT_TEXT": "comment text - This text appears in a new workspace comment, to hint that the user can type here.",
|
||||
"WORKSPACE_ARIA_LABEL": "workspace - This text is read out when a user navigates to the workspace while using a screen reader.",
|
||||
"COLLAPSED_WARNINGS_WARNING": "warning - This appears if the user collapses a block, and blocks inside that block have warnings attached to them. It should inform the user that the block they collapsed contains blocks that have warnings.",
|
||||
"DIALOG_OK": "button label - Pressing this button closes help information.\n{{Identical|OK}}",
|
||||
"DIALOG_CANCEL": "button label - Pressing this button cancels a proposed action.\n{{Identical|Cancel}}",
|
||||
@@ -464,6 +463,10 @@
|
||||
"KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Message shown to inform users how to move blocks with the keyboard.",
|
||||
"KEYBOARD_NAV_COPIED_HINT": "Message shown when an item is copied in keyboard navigation mode.",
|
||||
"KEYBOARD_NAV_CUT_HINT": "Message shown when an item is cut in keyboard navigation mode.",
|
||||
"WORKSPACE_LABEL_1_STACK": "Aria label for a workspace with one stack of blocks.",
|
||||
"WORKSPACE_LABEL_MANY_STACKS": "Aria label for a workspace with 0 or >1 stacks of blocks. \n\nParameters:\n* %1 - the number of stacks of blocks. A stack of blocks is a group of connected blocks that are not connected to any other blocks. 0 stacks means there are no blocks on the workspace.",
|
||||
"WORKSPACE_LABEL_MUTATOR_WORKSPACE": "Aria label for a mutator workspace, which is a secondary workspace used for editing a block's structure. This type of workspace appears when a user clicks on the gear icon of a block that has a mutator, and allows the user to add, remove, or rearrange inputs to that block.",
|
||||
"WORKSPACE_LABEL_FLYOUT_WORKSPACE": "Aria label for an always-open flyout's workspace. Since the flyout will have a role of list, the resulting screenreader output will be something like 'Logic blocks list, with 5 items'. Do not include the word 'list' in this message. Parameters: %1 - the category of blocks in the flyout, e.g. 'Logic' or 'Math'. This may be empty for an uncategorized flyout.",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_MANY": "ARIA live region message announcing the number of stacks of blocks in the workspace, optionally including comments. \n\nParameters:\n* %1 - the number of stacks (integer greater than 1)\n* %2 - optional phrase announcing comments, including leading space \n\nExamples:\n* '5 stacks of blocks in workspace.'\n* '5 stacks of blocks and 2 comments in workspace.'",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_ONE": "ARIA live region message announcing there is one stack of blocks in the workspace, optionally including a count of comments. \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space \n\nExamples:\n* 'One stack of blocks in workspace.'\n* 'One stack of blocks and 1 comment in workspace.'",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_ZERO": "ARIA live region message announcing there are no blocks in the workspace, optionally including a count of comments. \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space \n\nExamples:\n* 'No blocks in workspace.'\n* 'No blocks and 3 comments in workspace.'",
|
||||
@@ -502,5 +505,7 @@
|
||||
"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''"
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -1599,11 +1599,6 @@ Blockly.Msg.PROCEDURES_IFRETURN_WARNING = 'Warning: This block may be used only
|
||||
/// the user can type here.
|
||||
Blockly.Msg.WORKSPACE_COMMENT_DEFAULT_TEXT = 'Say something...';
|
||||
|
||||
/** @type {string} */
|
||||
/// workspace - This text is read out when a user navigates to the workspace while
|
||||
/// using a screen reader.
|
||||
Blockly.Msg.WORKSPACE_ARIA_LABEL = 'Blockly Workspace';
|
||||
|
||||
/** @type {string} */
|
||||
/// warning - This appears if the user collapses a block, and blocks inside
|
||||
/// that block have warnings attached to them. It should inform the user that the
|
||||
@@ -1802,6 +1797,25 @@ Blockly.Msg.KEYBOARD_NAV_COPIED_HINT = 'Copied. Press %1 to paste.';
|
||||
/// Message shown when an item is cut in keyboard navigation mode.
|
||||
Blockly.Msg.KEYBOARD_NAV_CUT_HINT = 'Cut. Press %1 to paste.';
|
||||
/** @type {string} */
|
||||
/// Aria label for a workspace with one stack of blocks.
|
||||
Blockly.Msg.WORKSPACE_LABEL_1_STACK = 'Blocks workspace. 1 stack of blocks';
|
||||
/** @type {string} */
|
||||
/// Aria label for a workspace with 0 or >1 stacks of blocks.
|
||||
/// \n\nParameters:\n* %1 - the number of stacks of blocks. A stack of blocks is a group of connected
|
||||
/// blocks that are not connected to any other blocks. 0 stacks means there are no blocks on the workspace.
|
||||
Blockly.Msg.WORKSPACE_LABEL_MANY_STACKS = 'Blocks workspace. %1 stacks of blocks';
|
||||
/** @type {string} */
|
||||
/// Aria label for a mutator workspace, which is a secondary workspace used for editing a block's structure.
|
||||
/// This type of workspace appears when a user clicks on the gear icon of a block that has a mutator, and
|
||||
/// allows the user to add, remove, or rearrange inputs to that block.
|
||||
Blockly.Msg.WORKSPACE_LABEL_MUTATOR_WORKSPACE = 'Block editor workspace';
|
||||
/** @type {string} */
|
||||
/// Aria label for an always-open flyout's workspace. Since the flyout will have a role of list,
|
||||
/// the resulting screenreader output will be something like "Logic blocks list, with 5 items".
|
||||
/// Do not include the word "list" in this message.
|
||||
/// Parameters: %1 - the category of blocks in the flyout, e.g. "Logic" or "Math". This may be empty for an uncategorized flyout.
|
||||
Blockly.Msg.WORKSPACE_LABEL_FLYOUT_WORKSPACE = '%1 blocks';
|
||||
/** @type {string} */
|
||||
/// ARIA live region message announcing the number of stacks of blocks in the workspace, optionally including comments.
|
||||
/// \n\nParameters:\n* %1 - the number of stacks (integer greater than 1)\n* %2 - optional phrase announcing comments, including leading space
|
||||
/// \n\nExamples:\n* "5 stacks of blocks in workspace."\n* "5 stacks of blocks and 2 comments in workspace."
|
||||
@@ -1965,4 +1979,13 @@ Blockly.Msg.FIELD_LABEL_CHECKBOX_UNCHECKED = 'Not checked';
|
||||
/// 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"'
|
||||
Blockly.Msg.FIELD_LABEL_VARIABLE = 'Variable "%1"';
|
||||
Blockly.Msg.FIELD_LABEL_VARIABLE = 'Variable "%1"';
|
||||
/// 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.
|
||||
Blockly.Msg.ARIA_LABEL_BUTTON = 'button';
|
||||
/** @type {string} */
|
||||
/// 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.
|
||||
Blockly.Msg.ARIA_LABEL_HEADING = 'heading';
|
||||
|
||||
@@ -435,23 +435,23 @@ suite('Utils', function () {
|
||||
});
|
||||
|
||||
suite('createSvgElement', function () {
|
||||
test('svg elements of type g have the generic role by default', function () {
|
||||
test('svg elements of type g have the presentation role by default', function () {
|
||||
const svgG = Blockly.utils.dom.createSvgElement(
|
||||
Blockly.utils.Svg.G,
|
||||
{},
|
||||
);
|
||||
const g = Blockly.utils.dom.createSvgElement('g', {});
|
||||
assert.equal(svgG.getAttribute('role'), 'generic');
|
||||
assert.equal(g.getAttribute('role'), 'generic');
|
||||
assert.equal(svgG.getAttribute('role'), 'presentation');
|
||||
assert.equal(g.getAttribute('role'), 'presentation');
|
||||
});
|
||||
test('svg elements of type svg have the generic role by default', function () {
|
||||
test('svg elements of type svg have the presentation role by default', function () {
|
||||
const svgSvg = Blockly.utils.dom.createSvgElement(
|
||||
Blockly.utils.Svg.SVG,
|
||||
{},
|
||||
);
|
||||
const svg = Blockly.utils.dom.createSvgElement('svg', {});
|
||||
assert.equal(svgSvg.getAttribute('role'), 'generic');
|
||||
assert.equal(svg.getAttribute('role'), 'generic');
|
||||
assert.equal(svgSvg.getAttribute('role'), 'presentation');
|
||||
assert.equal(svg.getAttribute('role'), 'presentation');
|
||||
});
|
||||
test('svg elements of type g reflect the role passed in when created', function () {
|
||||
const svgG = Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G, {
|
||||
|
||||
Reference in New Issue
Block a user