From c8a7fc66c4e9fe5d2a76840578c112a8ff184c3f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 13 Oct 2025 12:37:21 -0700 Subject: [PATCH] feat: Remove most block tree support. (#9412) Also, use regions for identifiying toolbox, workspace, and flyout. --- core/block_svg.ts | 38 +++++----------------- core/toolbox/toolbox.ts | 10 ++++-- core/utils/aria.ts | 1 + core/workspace_svg.ts | 70 +++++------------------------------------ 4 files changed, 23 insertions(+), 96 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index f5327ceff..fed2d7ea1 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -300,42 +300,18 @@ export class BlockSvg private computeAriaRole() { if (this.isSimpleReporter()) { aria.setRole(this.pathObject.svgPath, aria.Role.BUTTON); - } else { - // This isn't read out by VoiceOver and it will read in the wrong place - // as a duplicate in ChromeVox due to the other changes in this branch. - // aria.setState( - // this.pathObject.svgPath, - // aria.State.ROLEDESCRIPTION, - // 'block', - // ); + } else if (this.workspace.isFlyout) { aria.setRole(this.pathObject.svgPath, aria.Role.TREEITEM); - } - } - - collectSiblingBlocks(surroundParent: BlockSvg | null): BlockSvg[] { - // NOTE TO DEVELOPERS: it's very important that these are NOT sorted. The - // returned list needs to be relatively stable for consistent block indexes - // read out to users via screen readers. - if (surroundParent) { - // Start from the first sibling and iterate in navigation order. - const firstSibling: BlockSvg = surroundParent.getChildren(false)[0]; - const siblings: BlockSvg[] = [firstSibling]; - let nextSibling: BlockSvg | null = firstSibling; - while ((nextSibling = nextSibling?.getNextBlock())) { - siblings.push(nextSibling); - } - return siblings; } else { - // For top-level blocks, simply return those from the workspace. - return this.workspace.getTopBlocks(false); + aria.setState( + this.pathObject.svgPath, + aria.State.ROLEDESCRIPTION, + 'block', + ); + aria.setRole(this.pathObject.svgPath, aria.Role.FIGURE); } } - computeLevelInWorkspace(): number { - const surroundParent = this.getSurroundParent(); - return surroundParent ? surroundParent.computeLevelInWorkspace() + 1 : 0; - } - /** * Create and initialize the SVG representation of the block. * May be called more than once. diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 2ede1bad1..e03b09a37 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -154,9 +154,6 @@ export class Toolbox this.setVisible(true); this.flyout.init(workspace); - aria.setRole(this.HtmlDiv, aria.Role.TREE); - aria.setState(this.HtmlDiv, aria.State.LABEL, Msg['TOOLBOX_ARIA_LABEL']); - this.render(this.toolboxDef_); const themeManager = workspace.getThemeManager(); themeManager.subscribe( @@ -208,6 +205,12 @@ export class Toolbox toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v'); dom.addClass(toolboxContainer, 'blocklyToolbox'); toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); + aria.setRole(toolboxContainer, aria.Role.REGION); + aria.setState( + toolboxContainer, + aria.State.LABEL, + Msg['TOOLBOX_ARIA_LABEL'], + ); return toolboxContainer; } @@ -222,6 +225,7 @@ export class Toolbox if (this.isHorizontal()) { contentsContainer.style.flexDirection = 'row'; } + aria.setRole(contentsContainer, aria.Role.TREE); return contentsContainer; } diff --git a/core/utils/aria.ts b/core/utils/aria.ts index a1f7d83b8..c099d10c7 100644 --- a/core/utils/aria.ts +++ b/core/utils/aria.ts @@ -53,6 +53,7 @@ export enum Role { TEXTBOX = 'textbox', COMBOBOX = 'combobox', SPINBUTTON = 'spinbutton', + REGION = 'region', } /** diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index cc7da2fcf..e09618bb9 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -31,7 +31,6 @@ import {WorkspaceComment} from './comments/workspace_comment.js'; import * as common from './common.js'; import {ComponentManager} from './component_manager.js'; import {ConnectionDB} from './connection_db.js'; -import {ConnectionType} from './connection_type.js'; import * as ContextMenu from './contextmenu.js'; import { ContextMenuOption, @@ -763,19 +762,15 @@ export class WorkspaceSvg }); let ariaLabel = null; - if (injectionDiv) { - ariaLabel = Msg['WORKSPACE_ARIA_LABEL']; - } else if (this.isFlyout) { + if (this.isFlyout) { ariaLabel = 'Flyout'; } else if (this.isMutator) { - ariaLabel = 'Mutator'; + ariaLabel = 'Mutator Workspace'; } else { - // This case can happen in some test scenarios. - // TODO: Figure out when this can happen in non-test scenarios (if ever). - ariaLabel = 'Workspace'; + ariaLabel = Msg['WORKSPACE_ARIA_LABEL']; } + aria.setRole(this.svgGroup_, aria.Role.REGION); aria.setState(this.svgGroup_, aria.State.LABEL, ariaLabel); - aria.setRole(this.svgGroup_, aria.Role.TREE); // Note that a alone does not receive mouse events--it must have a // valid target inside it. If no background class is specified, as in the @@ -803,7 +798,10 @@ export class WorkspaceSvg this.svgBlockCanvas_ = this.layerManager.getBlockLayer(); this.svgBubbleCanvas_ = this.layerManager.getBubbleLayer(); - if (!this.isFlyout) { + if (this.isFlyout) { + // Use the block canvas as the primary tree parent for flyout blocks. + aria.setRole(this.svgBlockCanvas_, aria.Role.TREE); + } else { browserEvents.conditionalBind( this.svgGroup_, 'pointerdown', @@ -2959,61 +2957,9 @@ export class WorkspaceSvg aria.setState(treeItemElem, aria.State.POSINSET, index + 1); aria.setState(treeItemElem, aria.State.SETSIZE, focusableItems.length); aria.setState(treeItemElem, aria.State.LEVEL, 1); // They are always top-level. - if (item instanceof BlockSvg) { - item - .getChildren(false) - .forEach((child) => - this.recomputeAriaTreeItemDetailsRecursively(child), - ); - } }); - } else { - // TODO: Do this efficiently (probably incrementally). - this.getTopBlocks(false).forEach((block) => - this.recomputeAriaTreeItemDetailsRecursively(block), - ); } } - - private recomputeAriaTreeItemDetailsRecursively(block: BlockSvg) { - const elem = block.getFocusableElement(); - const connection = block.currentConnectionCandidate; - let childPosition: number; - let parentsChildCount: number; - let hierarchyDepth: number; - if (connection) { - // If the block is being inserted into a new location, the position is hypothetical. - // TODO: Figure out how to deal with output connections. - let surroundParent: BlockSvg | null; - let siblingBlocks: BlockSvg[]; - if (connection.type === ConnectionType.INPUT_VALUE) { - surroundParent = connection.sourceBlock_; - siblingBlocks = block.collectSiblingBlocks(surroundParent); - // The block is being added as a child since it's input. - // TODO: Figure out how to compute the correct position. - childPosition = 0; - } else { - surroundParent = connection.sourceBlock_.getSurroundParent(); - siblingBlocks = block.collectSiblingBlocks(surroundParent); - // The block is being added after the connected block. - childPosition = siblingBlocks.indexOf(connection.sourceBlock_) + 1; - } - parentsChildCount = siblingBlocks.length + 1; - hierarchyDepth = surroundParent?.computeLevelInWorkspace() ?? 0; - } else { - const surroundParent = block.getSurroundParent(); - const siblingBlocks = block.collectSiblingBlocks(surroundParent); - childPosition = siblingBlocks.indexOf(block); - parentsChildCount = siblingBlocks.length; - hierarchyDepth = block.computeLevelInWorkspace(); - } - aria.setState(elem, aria.State.POSINSET, childPosition + 1); - aria.setState(elem, aria.State.SETSIZE, parentsChildCount); - aria.setState(elem, aria.State.LEVEL, hierarchyDepth + 1); - block - .getChildren(false) - .forEach((child) => this.recomputeAriaTreeItemDetailsRecursively(child)); - } } /**