From b1d3d6d9180fcfa9db80b7ba0bac9d4a43a9eade Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 30 Oct 2025 00:37:02 +0000 Subject: [PATCH 01/10] feat: Clean up a11y node hierarchy. --- core/block_svg.ts | 53 +++++++++++++++++++++++++++++++++++++ core/rendered_connection.ts | 4 +++ 2 files changed, 57 insertions(+) diff --git a/core/block_svg.ts b/core/block_svg.ts index 00b3b816d44..37ec38e9378 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -232,6 +232,46 @@ 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)) { + if (connection.canBeFocused() && connection.isHighlighted()) { + 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; @@ -244,6 +284,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 +398,7 @@ export class BlockSvg this.workspace.getCanvas().appendChild(svg); } this.initialized = true; + this.recomputeAriaLabel(); } /** @@ -456,6 +503,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..97b56c57896 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. */ From f81e2b2fecae5aa28c58a8c94ad8ddeb570d4071 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 12 Nov 2025 01:02:51 +0000 Subject: [PATCH 02/10] fix: Fix CI failures. Addresses some observed gaps in behaviors with connections. This is unfortunately mainly a patch without fully understanding the deep issue being avoided but it seems reasonable for the experimental branch. --- core/block_svg.ts | 13 +++++++++++-- core/rendered_connection.ts | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 37ec38e9378..fd3e7b9a6bd 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -264,7 +264,14 @@ export class BlockSvg } } for (const connection of this.getConnections_(true)) { - if (connection.canBeFocused() && connection.isHighlighted()) { + // 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); } } @@ -274,7 +281,9 @@ export class BlockSvg 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( diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 97b56c57896..5f305fdc271 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -692,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 From eeaff739e6426b6fbd9308c5032d290de363d67c Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 8 Dec 2025 22:25:48 +0000 Subject: [PATCH 03/10] chore: Simplify connectiong gating logic. It seems that the extra safety checks weren't actually needed. --- core/block_svg.ts | 9 +-------- core/rendered_connection.ts | 5 ++--- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index fc96cc3fec8..96be1bb69e0 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -265,14 +265,7 @@ export class BlockSvg } } 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 - ) { + if (connection.canBeFocused()) { childElemIds.push(connection.getFocusableElement().id); } } diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index e9f30f38a1b..2542802aa54 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -692,11 +692,10 @@ export class RenderedConnection /** See IFocusableNode.canBeFocused. */ canBeFocused(): boolean { - return true; + return this.findHighlightSvg() !== null; } - // TODO: Figure out how to make this private again. - findHighlightSvg(): SVGPathElement | null { + private 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 From 5e28067f067f99b4a9460a818f1ca40d61530f3e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 9 Dec 2025 17:57:02 +0000 Subject: [PATCH 04/10] chore: Reduce complexity. More investigation and fixes are likely still needed. --- core/block_svg.ts | 12 ++++++++---- core/rendered_connection.ts | 4 ---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 96be1bb69e0..b40a20a9781 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -264,10 +264,14 @@ export class BlockSvg childElemIds.push(icon.getFocusableElement().id); } } - for (const connection of this.getConnections_(true)) { - if (connection.canBeFocused()) { - childElemIds.push(connection.getFocusableElement().id); - } + const connection = input.connection as RenderedConnection | null; + if ( + connection && + connection.canBeFocused() && + (connection.type === ConnectionType.INPUT_VALUE || + connection.type === ConnectionType.NEXT_STATEMENT) + ) { + childElemIds.push(connection.getFocusableElement().id); } } aria.setState(this.getBlockSvgFocusElem(), aria.State.OWNS, childElemIds); diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 2542802aa54..ffef106f579 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -365,8 +365,6 @@ export class RenderedConnection aria.setState(highlightSvg, aria.State.LABEL, 'Empty'); } } - - this.sourceBlock_.recomputeAriaLabel(); } /** Remove the highlighting around this connection. */ @@ -378,8 +376,6 @@ export class RenderedConnection if (highlightSvg) { highlightSvg.style.display = 'none'; } - - this.sourceBlock_.recomputeAriaLabel(); } /** Returns true if this connection is highlighted, false otherwise. */ From 8dacb413625f84d93d5d82ef5261b484ad86ceb6 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 12 Dec 2025 01:52:11 +0000 Subject: [PATCH 05/10] fix: Broken tests due to connection change. Only one behavioral change was needed. --- tests/mocha/cursor_test.js | 8 +- tests/mocha/navigation_test.js | 77 +++++++------------ tests/mocha/test_helpers/block_definitions.js | 4 +- 3 files changed, 35 insertions(+), 54 deletions(-) diff --git a/tests/mocha/cursor_test.js b/tests/mocha/cursor_test.js index 2273ec4b381..6816cf8c1e7 100644 --- a/tests/mocha/cursor_test.js +++ b/tests/mocha/cursor_test.js @@ -342,7 +342,7 @@ suite('Cursor', function () { }); suite('one empty block', function () { setup(function () { - this.blockA = this.workspace.newBlock('empty_block'); + this.blockA = createRenderedBlock(this.workspace, 'empty_block'); }); teardown(function () { this.workspace.clear(); @@ -359,7 +359,7 @@ suite('Cursor', function () { suite('one stack block', function () { setup(function () { - this.blockA = this.workspace.newBlock('stack_block'); + this.blockA = createRenderedBlock(this.workspace, 'stack_block'); }); teardown(function () { this.workspace.clear(); @@ -376,7 +376,7 @@ suite('Cursor', function () { suite('one row block', function () { setup(function () { - this.blockA = this.workspace.newBlock('row_block'); + this.blockA = createRenderedBlock(this.workspace, 'row_block'); }); teardown(function () { this.workspace.clear(); @@ -392,7 +392,7 @@ suite('Cursor', function () { }); suite('one c-hat block', function () { setup(function () { - this.blockA = this.workspace.newBlock('c_hat_block'); + this.blockA = createRenderedBlock(this.workspace, 'c_hat_block'); }); teardown(function () { this.workspace.clear(); diff --git a/tests/mocha/navigation_test.js b/tests/mocha/navigation_test.js index 3a9292b9209..8b4c5632fcf 100644 --- a/tests/mocha/navigation_test.js +++ b/tests/mocha/navigation_test.js @@ -5,10 +5,10 @@ */ import {assert} from '../../node_modules/chai/index.js'; +import { createRenderedBlock } from './test_helpers/block_definitions.js'; import { sharedTestSetup, sharedTestTeardown, - workspaceTeardown, } from './test_helpers/setup_teardown.js'; suite('Navigation', function () { @@ -89,13 +89,13 @@ suite('Navigation', function () { ]); this.workspace = Blockly.inject('blocklyDiv', {}); this.navigator = this.workspace.getNavigator(); - const statementInput1 = this.workspace.newBlock('input_statement'); - const statementInput2 = this.workspace.newBlock('input_statement'); - const statementInput3 = this.workspace.newBlock('input_statement'); - const statementInput4 = this.workspace.newBlock('input_statement'); - const fieldWithOutput = this.workspace.newBlock('field_input'); - const doubleValueInput = this.workspace.newBlock('double_value_input'); - const valueInput = this.workspace.newBlock('value_input'); + const statementInput1 = createRenderedBlock(this.workspace, 'input_statement'); + const statementInput2 = createRenderedBlock(this.workspace, 'input_statement'); + const statementInput3 = createRenderedBlock(this.workspace, 'input_statement'); + const statementInput4 = createRenderedBlock(this.workspace, 'input_statement'); + const fieldWithOutput = createRenderedBlock(this.workspace, 'field_input'); + const doubleValueInput = createRenderedBlock(this.workspace, 'double_value_input'); + const valueInput = createRenderedBlock(this.workspace, 'value_input'); statementInput1.nextConnection.connect(statementInput2.previousConnection); statementInput1.inputList[0].connection.connect( @@ -355,13 +355,13 @@ suite('Navigation', function () { 'helpUrl': '', }, ]); - const noNextConnection = this.workspace.newBlock('top_connection'); - const fieldAndInputs = this.workspace.newBlock('fields_and_input'); - const twoFields = this.workspace.newBlock('two_fields'); - const fieldAndInputs2 = this.workspace.newBlock('fields_and_input2'); - const noPrevConnection = this.workspace.newBlock('start_block'); - const hiddenField = this.workspace.newBlock('hidden_field'); - const hiddenInput = this.workspace.newBlock('hidden_input'); + const noNextConnection = createRenderedBlock(this.workspace, 'top_connection'); + const fieldAndInputs = createRenderedBlock(this.workspace, 'fields_and_input'); + const twoFields = createRenderedBlock(this.workspace, 'two_fields'); + const fieldAndInputs2 = createRenderedBlock(this.workspace, 'fields_and_input2'); + const noPrevConnection = createRenderedBlock(this.workspace, 'start_block'); + const hiddenField = createRenderedBlock(this.workspace, 'hidden_field'); + const hiddenInput = createRenderedBlock(this.workspace, 'hidden_input'); this.blocks.noNextConnection = noNextConnection; this.blocks.fieldAndInputs = fieldAndInputs; this.blocks.twoFields = twoFields; @@ -373,28 +373,28 @@ suite('Navigation', function () { hiddenField.inputList[0].fieldRow[1].setVisible(false); hiddenInput.inputList[1].setVisible(false); - const dummyInput = this.workspace.newBlock('dummy_input'); - const dummyInputValue = this.workspace.newBlock('dummy_inputValue'); - const fieldWithOutput2 = this.workspace.newBlock('field_input'); + const dummyInput = createRenderedBlock(this.workspace, 'dummy_input'); + const dummyInputValue = createRenderedBlock(this.workspace, 'dummy_inputValue'); + const fieldWithOutput2 = createRenderedBlock(this.workspace, 'field_input'); this.blocks.dummyInput = dummyInput; this.blocks.dummyInputValue = dummyInputValue; this.blocks.fieldWithOutput2 = fieldWithOutput2; - const secondBlock = this.workspace.newBlock('input_statement'); - const outputNextBlock = this.workspace.newBlock('output_next'); + const secondBlock = createRenderedBlock(this.workspace, 'input_statement'); + const outputNextBlock = createRenderedBlock(this.workspace, 'output_next'); this.blocks.secondBlock = secondBlock; this.blocks.outputNextBlock = outputNextBlock; - const buttonBlock = this.workspace.newBlock('buttons', 'button_block'); - const buttonInput1 = this.workspace.newBlock( + const buttonBlock = createRenderedBlock(this.workspace, 'buttons', 'button_block'); + const buttonInput1 = createRenderedBlock(this.workspace, 'field_input', 'button_input1', ); - const buttonInput2 = this.workspace.newBlock( + const buttonInput2 = createRenderedBlock(this.workspace, 'field_input', 'button_input2', ); - const buttonNext = this.workspace.newBlock( + const buttonNext = createRenderedBlock(this.workspace, 'input_statement', 'button_next', ); @@ -420,15 +420,6 @@ suite('Navigation', function () { this.workspace.cleanUp(); }); suite('Next', function () { - setup(function () { - this.singleBlockWorkspace = new Blockly.Workspace(); - const singleBlock = this.singleBlockWorkspace.newBlock('two_fields'); - this.blocks.singleBlock = singleBlock; - }); - teardown(function () { - workspaceTeardown.call(this, this.singleBlockWorkspace); - }); - test('fromPreviousToBlock', function () { const prevConnection = this.blocks.statementInput1.previousConnection; const nextNode = this.navigator.getNextSibling(prevConnection); @@ -471,8 +462,6 @@ suite('Navigation', function () { }); test('fromFieldToNestedBlock', function () { const field = this.blocks.statementInput1.inputList[0].fieldRow[1]; - const inputConnection = - this.blocks.statementInput1.inputList[0].connection; const nextNode = this.navigator.getNextSibling(field); assert.equal(nextNode, this.blocks.fieldWithOutput); }); @@ -576,7 +565,6 @@ suite('Navigation', function () { const prevNode = this.navigator.getPreviousSibling( this.blocks.fieldWithOutput, ); - const outputConnection = this.blocks.fieldWithOutput.outputConnection; assert.equal(prevNode, [...this.blocks.statementInput1.getFields()][1]); }); test('fromNextToBlock', function () { @@ -617,14 +605,12 @@ suite('Navigation', function () { assert.isNull(prevNode); }); test('fromFieldToInput', function () { - const outputBlock = this.workspace.newBlock('field_input'); + const outputBlock = createRenderedBlock(this.workspace, 'field_input'); this.blocks.fieldAndInputs2.inputList[0].connection.connect( outputBlock.outputConnection, ); const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; - const inputConnection = - this.blocks.fieldAndInputs2.inputList[0].connection; const prevNode = this.navigator.getPreviousSibling(field); assert.equal(prevNode, outputBlock); }); @@ -701,18 +687,13 @@ suite('Navigation', function () { }); suite('In', function () { - setup(function () { - this.emptyWorkspace = Blockly.inject(document.createElement('div'), {}); - }); - teardown(function () { - workspaceTeardown.call(this, this.emptyWorkspace); - }); - test('fromInputToOutput', function () { + // The first child is the connected block since the connection itself + // cannot be navigated to directly. const input = this.blocks.statementInput1.inputList[0]; const inNode = this.navigator.getFirstChild(input.connection); - const outputConnection = this.blocks.fieldWithOutput.outputConnection; - assert.equal(inNode, outputConnection); + const connectedBlock = this.blocks.fieldWithOutput; + assert.equal(inNode, connectedBlock); }); test('fromInputToNull', function () { const input = this.blocks.statementInput2.inputList[0]; diff --git a/tests/mocha/test_helpers/block_definitions.js b/tests/mocha/test_helpers/block_definitions.js index 26507b29cb8..e5ca106d2c4 100644 --- a/tests/mocha/test_helpers/block_definitions.js +++ b/tests/mocha/test_helpers/block_definitions.js @@ -196,8 +196,8 @@ export function createTestBlock() { }; } -export function createRenderedBlock(workspaceSvg, type) { - const block = workspaceSvg.newBlock(type); +export function createRenderedBlock(workspaceSvg, type, opt_id) { + const block = workspaceSvg.newBlock(type, opt_id); block.initSvg(); block.render(); return block; From c19eaa5d1161b43d39336fb57d755ef5cb63364f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 12 Dec 2025 02:36:41 +0000 Subject: [PATCH 06/10] chore: Lint fixes. --- tests/mocha/navigation_test.js | 82 ++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/tests/mocha/navigation_test.js b/tests/mocha/navigation_test.js index 8b4c5632fcf..37972318d50 100644 --- a/tests/mocha/navigation_test.js +++ b/tests/mocha/navigation_test.js @@ -5,7 +5,7 @@ */ import {assert} from '../../node_modules/chai/index.js'; -import { createRenderedBlock } from './test_helpers/block_definitions.js'; +import {createRenderedBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, sharedTestTeardown, @@ -89,12 +89,27 @@ suite('Navigation', function () { ]); this.workspace = Blockly.inject('blocklyDiv', {}); this.navigator = this.workspace.getNavigator(); - const statementInput1 = createRenderedBlock(this.workspace, 'input_statement'); - const statementInput2 = createRenderedBlock(this.workspace, 'input_statement'); - const statementInput3 = createRenderedBlock(this.workspace, 'input_statement'); - const statementInput4 = createRenderedBlock(this.workspace, 'input_statement'); + const statementInput1 = createRenderedBlock( + this.workspace, + 'input_statement', + ); + const statementInput2 = createRenderedBlock( + this.workspace, + 'input_statement', + ); + const statementInput3 = createRenderedBlock( + this.workspace, + 'input_statement', + ); + const statementInput4 = createRenderedBlock( + this.workspace, + 'input_statement', + ); const fieldWithOutput = createRenderedBlock(this.workspace, 'field_input'); - const doubleValueInput = createRenderedBlock(this.workspace, 'double_value_input'); + const doubleValueInput = createRenderedBlock( + this.workspace, + 'double_value_input', + ); const valueInput = createRenderedBlock(this.workspace, 'value_input'); statementInput1.nextConnection.connect(statementInput2.previousConnection); @@ -355,11 +370,23 @@ suite('Navigation', function () { 'helpUrl': '', }, ]); - const noNextConnection = createRenderedBlock(this.workspace, 'top_connection'); - const fieldAndInputs = createRenderedBlock(this.workspace, 'fields_and_input'); + const noNextConnection = createRenderedBlock( + this.workspace, + 'top_connection', + ); + const fieldAndInputs = createRenderedBlock( + this.workspace, + 'fields_and_input', + ); const twoFields = createRenderedBlock(this.workspace, 'two_fields'); - const fieldAndInputs2 = createRenderedBlock(this.workspace, 'fields_and_input2'); - const noPrevConnection = createRenderedBlock(this.workspace, 'start_block'); + const fieldAndInputs2 = createRenderedBlock( + this.workspace, + 'fields_and_input2', + ); + const noPrevConnection = createRenderedBlock( + this.workspace, + 'start_block', + ); const hiddenField = createRenderedBlock(this.workspace, 'hidden_field'); const hiddenInput = createRenderedBlock(this.workspace, 'hidden_input'); this.blocks.noNextConnection = noNextConnection; @@ -374,27 +401,46 @@ suite('Navigation', function () { hiddenInput.inputList[1].setVisible(false); const dummyInput = createRenderedBlock(this.workspace, 'dummy_input'); - const dummyInputValue = createRenderedBlock(this.workspace, 'dummy_inputValue'); - const fieldWithOutput2 = createRenderedBlock(this.workspace, 'field_input'); + const dummyInputValue = createRenderedBlock( + this.workspace, + 'dummy_inputValue', + ); + const fieldWithOutput2 = createRenderedBlock( + this.workspace, + 'field_input', + ); this.blocks.dummyInput = dummyInput; this.blocks.dummyInputValue = dummyInputValue; this.blocks.fieldWithOutput2 = fieldWithOutput2; - const secondBlock = createRenderedBlock(this.workspace, 'input_statement'); - const outputNextBlock = createRenderedBlock(this.workspace, 'output_next'); + const secondBlock = createRenderedBlock( + this.workspace, + 'input_statement', + ); + const outputNextBlock = createRenderedBlock( + this.workspace, + 'output_next', + ); this.blocks.secondBlock = secondBlock; this.blocks.outputNextBlock = outputNextBlock; - const buttonBlock = createRenderedBlock(this.workspace, 'buttons', 'button_block'); - const buttonInput1 = createRenderedBlock(this.workspace, + const buttonBlock = createRenderedBlock( + this.workspace, + 'buttons', + 'button_block', + ); + const buttonInput1 = createRenderedBlock( + this.workspace, 'field_input', 'button_input1', ); - const buttonInput2 = createRenderedBlock(this.workspace, + const buttonInput2 = createRenderedBlock( + this.workspace, 'field_input', 'button_input2', ); - const buttonNext = createRenderedBlock(this.workspace, + const buttonNext = createRenderedBlock( + this.workspace, 'input_statement', 'button_next', ); From 71a96785ddf5e5576d4076f948675ed4f9f859f2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 12 Dec 2025 02:37:23 +0000 Subject: [PATCH 07/10] fix: Fix output connections in C-shaped blocks. Fixes ARIA ownership for these connection types. --- core/block_svg.ts | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 4566c957f94..a865593a2ff 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -237,21 +237,24 @@ export class BlockSvg 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; + const connection = input.connection as RenderedConnection | null; + if (input.isVisible() && connection) { + if (connection.type === ConnectionType.NEXT_STATEMENT) { + let currentBlock: BlockSvg | null = connection.targetBlock(); 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; + } else if (connection.type === ConnectionType.INPUT_VALUE) { + const inpBlock = connection.targetBlock() as BlockSvg | null; if (inpBlock && inpBlock.canBeFocused()) { childElemIds.push(inpBlock.getBlockSvgFocusElem().id); } + if (connection.canBeFocused()) { + childElemIds.push(connection.getFocusableElement().id); + } } } for (const field of input.fieldRow) { @@ -265,16 +268,17 @@ export class BlockSvg childElemIds.push(icon.getFocusableElement().id); } } - const connection = input.connection as RenderedConnection | null; - if ( - connection && - connection.canBeFocused() && - (connection.type === ConnectionType.INPUT_VALUE || - connection.type === ConnectionType.NEXT_STATEMENT) - ) { - childElemIds.push(connection.getFocusableElement().id); - } } + + const nextConnection = this.nextConnection as RenderedConnection | null; + if ( + nextConnection && + nextConnection.canBeFocused() && + nextConnection.type === ConnectionType.NEXT_STATEMENT + ) { + childElemIds.push(nextConnection.getFocusableElement().id); + } + aria.setState(this.getBlockSvgFocusElem(), aria.State.OWNS, childElemIds); } From 3c9df1f2d8d2bf5d31ce88d25e142ad425cc4b88 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 16 Dec 2025 21:42:59 +0000 Subject: [PATCH 08/10] chore: Remove redundant check. Addresses a review comment. --- core/block_svg.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 0144f38a59a..5522c6ad713 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -271,11 +271,7 @@ export class BlockSvg } const nextConnection = this.nextConnection as RenderedConnection | null; - if ( - nextConnection && - nextConnection.canBeFocused() && - nextConnection.type === ConnectionType.NEXT_STATEMENT - ) { + if (nextConnection && nextConnection.canBeFocused()) { childElemIds.push(nextConnection.getFocusableElement().id); } From a06370995b211d8b3ba596f6934b6a067a4213bf Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 17 Dec 2025 01:51:39 +0000 Subject: [PATCH 09/10] chore: Attempt a different approach. --- core/block_svg.ts | 2 +- core/utils/dom.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 5522c6ad713..114e7eef4e2 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -275,7 +275,7 @@ export class BlockSvg childElemIds.push(nextConnection.getFocusableElement().id); } - aria.setState(this.getBlockSvgFocusElem(), aria.State.OWNS, childElemIds); + // aria.setState(this.getBlockSvgFocusElem(), aria.State.OWNS, childElemIds); } if (this.isSimpleReporter(true, true)) return; diff --git a/core/utils/dom.ts b/core/utils/dom.ts index e32cad4d604..a7fc35dc3a5 100644 --- a/core/utils/dom.ts +++ b/core/utils/dom.ts @@ -64,7 +64,7 @@ export function createSvgElement( opt_parent.appendChild(e); } if (name === Svg.SVG || name === Svg.G) { - aria.setRole(e, aria.Role.PRESENTATION); + aria.setRole(e, aria.Role.GENERIC); } return e; } From f9e745fb1ae7480a5cc01f7e01c2c9e35d1b809d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 18 Dec 2025 00:04:06 +0000 Subject: [PATCH 10/10] chore: Remove ownership code. --- core/block_svg.ts | 56 ----------------------------------------------- 1 file changed, 56 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 114e7eef4e2..ec39e681445 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -234,50 +234,6 @@ export class BlockSvg * @internal */ recomputeAriaLabel() { - if (this.initialized) { - const childElemIds: string[] = []; - for (const input of this.inputList) { - const connection = input.connection as RenderedConnection | null; - if (input.isVisible() && connection) { - if (connection.type === ConnectionType.NEXT_STATEMENT) { - let currentBlock: BlockSvg | null = connection.targetBlock(); - while (currentBlock) { - if (currentBlock.canBeFocused()) { - childElemIds.push(currentBlock.getBlockSvgFocusElem().id); - } - currentBlock = currentBlock.getNextBlock(); - } - } else if (connection.type === ConnectionType.INPUT_VALUE) { - const inpBlock = connection.targetBlock() as BlockSvg | null; - if (inpBlock && inpBlock.canBeFocused()) { - childElemIds.push(inpBlock.getBlockSvgFocusElem().id); - } - if (connection.canBeFocused()) { - childElemIds.push(connection.getFocusableElement().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); - } - } - } - - const nextConnection = this.nextConnection as RenderedConnection | null; - if (nextConnection && nextConnection.canBeFocused()) { - childElemIds.push(nextConnection.getFocusableElement().id); - } - - // aria.setState(this.getBlockSvgFocusElem(), aria.State.OWNS, childElemIds); - } - if (this.isSimpleReporter(true, true)) return; aria.setState( @@ -289,12 +245,6 @@ 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 computeAriaLabelForFlyoutBlock(): string { return `${this.computeAriaLabel(true)}, block`; } @@ -520,12 +470,6 @@ export class BlockSvg this.applyColour(); this.workspace.recomputeAriaTree(); - this.recomputeAriaLabelRecursive(); - } - - private recomputeAriaLabelRecursive() { - this.recomputeAriaLabel(); - this.parentBlock_?.recomputeAriaLabelRecursive(); } /**