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:
Maribeth Moffatt
2026-04-29 13:50:44 -04:00
committed by GitHub
parent 8d96ea51fc
commit 61ad99440c
12 changed files with 207 additions and 29 deletions
+5 -1
View File
@@ -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) {
+14 -1
View File
@@ -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);
+52
View File
@@ -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.
+11 -1
View File
@@ -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
+7
View File
@@ -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',
}
/**
+2 -2
View File
@@ -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]}`);
+57 -7
View File
@@ -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_,
+7 -2
View File
@@ -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"
}
+8 -3
View File
@@ -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."
}
+29 -6
View File
@@ -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';
+6 -6
View File
@@ -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, {