diff --git a/core/block_svg.ts b/core/block_svg.ts index 00b3b816d44..fd3e7b9a6bd 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -232,9 +232,58 @@ export class BlockSvg * @internal */ recomputeAriaLabel() { + if (this.initialized) { + const childElemIds: string[] = []; + for (const input of this.inputList) { + if (input.isVisible() && input.connection) { + if (input.connection.type === ConnectionType.NEXT_STATEMENT) { + let currentBlock: BlockSvg | null = + input.connection.targetBlock() as BlockSvg | null; + while (currentBlock) { + if (currentBlock.canBeFocused()) { + childElemIds.push(currentBlock.getBlockSvgFocusElem().id); + } + currentBlock = currentBlock.getNextBlock(); + } + } else if (input.connection.type === ConnectionType.INPUT_VALUE) { + const inpBlock = input.connection.targetBlock() as BlockSvg | null; + if (inpBlock && inpBlock.canBeFocused()) { + childElemIds.push(inpBlock.getBlockSvgFocusElem().id); + } + } + } + for (const field of input.fieldRow) { + if (field.getSvgRoot() && field.canBeFocused()) { + // Only track the field if it's been initialized. + childElemIds.push(field.getFocusableElement().id); + } + } + for (const icon of this.icons) { + if (icon.canBeFocused()) { + childElemIds.push(icon.getFocusableElement().id); + } + } + for (const connection of this.getConnections_(true)) { + // TODO: Somehow it's possible for a connection to be highlighted but + // have no focusable element. This might be some sort of race + // condition or perhaps dispose-esque situation happening. + if ( + connection.canBeFocused() && + connection.isHighlighted() && + connection.findHighlightSvg() !== null + ) { + childElemIds.push(connection.getFocusableElement().id); + } + } + } + aria.setState(this.getBlockSvgFocusElem(), aria.State.OWNS, childElemIds); + } + if (this.isSimpleReporter()) { const field = Array.from(this.getFields())[0]; - if (field.isFullBlockField() && field.isCurrentlyEditable()) return; + if (field && field.isFullBlockField() && field.isCurrentlyEditable()) { + return; + } } aria.setState( @@ -244,6 +293,12 @@ export class BlockSvg ); } + private getBlockSvgFocusElem(): Element { + // Note that this deviates from getFocusableElement() to ensure that + // single field blocks are properly set up in the hierarchy. + return this.pathObject.svgPath; + } + private computeAriaLabel(): string { const {blockSummary, inputCount} = buildBlockSummary(this); const inputSummary = inputCount @@ -352,6 +407,7 @@ export class BlockSvg this.workspace.getCanvas().appendChild(svg); } this.initialized = true; + this.recomputeAriaLabel(); } /** @@ -456,6 +512,12 @@ export class BlockSvg this.applyColour(); this.workspace.recomputeAriaTree(); + this.recomputeAriaLabelRecursive(); + } + + private recomputeAriaLabelRecursive() { + this.recomputeAriaLabel(); + this.parentBlock_?.recomputeAriaLabelRecursive(); } /** diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index bbf32006bc8..5f305fdc271 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -362,6 +362,8 @@ export class RenderedConnection aria.setState(highlightSvg, aria.State.LABEL, 'Open connection'); } } + + this.sourceBlock_.recomputeAriaLabel(); } /** Remove the highlighting around this connection. */ @@ -373,6 +375,8 @@ export class RenderedConnection if (highlightSvg) { highlightSvg.style.display = 'none'; } + + this.sourceBlock_.recomputeAriaLabel(); } /** Returns true if this connection is highlighted, false otherwise. */ @@ -688,7 +692,8 @@ export class RenderedConnection return true; } - private findHighlightSvg(): SVGPathElement | null { + // TODO: Figure out how to make this private again. + findHighlightSvg(): SVGPathElement | null { // This cast is valid as TypeScript's definition is wrong. See: // https://github.com/microsoft/TypeScript/issues/60996. return document.getElementById(this.id) as