From 896a10cb138e4dc6b62f47a6e1975aaf639399a0 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Fri, 5 Jun 2026 17:03:01 +0100 Subject: [PATCH] fix: allow nav into collapsed block row via right arrow This supports e.g. expand/collapse icons added outside core Blockly. MakeCode has an expand FieldImageNoText on collapsed blocks that wasn't otherwise navigable (inserted into COLLAPSED_INPUT_NAME): input.appendField(new FieldImageNoText(image, 24, 24, "Expand", () => { this.setCollapsed(false) }, false)); To support this: 1. the input that shares the block row id takes into account visibility 2. we drop the short circuit for collapsed blocks and rely on the filtering already in place to filter out the collapsed content Also filter icons whose updateCollapsed() hides them via display:none; without this they remain in the candidate list and the navigator can focus e.g. an invisible cog for a mutator workspace. Add tests for icon visibility. --- packages/blockly/core/inputs/input.ts | 8 +++- .../block_navigation_policy.ts | 7 ++-- .../blockly/tests/mocha/navigation_test.js | 37 +++++++++++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/blockly/core/inputs/input.ts b/packages/blockly/core/inputs/input.ts index 31b51a1c32d..48a4849d04b 100644 --- a/packages/blockly/core/inputs/input.ts +++ b/packages/blockly/core/inputs/input.ts @@ -375,7 +375,13 @@ export class Input { getRowId(): string { const inputs = this.getSourceBlock().inputList; - // The first input in a block has the same ID as its parent block. + // The first visible input shares the block's row id; this also covers + // the collapsed-input placeholder, since every other input is hidden. + if (this === inputs.find((i) => i.isVisible())) { + return (this.getSourceBlock() as BlockSvg).getRowId(); + } + + // Fallback when inputs[0] itself is hidden. if (this === inputs[0]) { return (this.getSourceBlock() as BlockSvg).getRowId(); } diff --git a/packages/blockly/core/keyboard_nav/navigation_policies/block_navigation_policy.ts b/packages/blockly/core/keyboard_nav/navigation_policies/block_navigation_policy.ts index bd144f74dc4..389888142ce 100644 --- a/packages/blockly/core/keyboard_nav/navigation_policies/block_navigation_policy.ts +++ b/packages/blockly/core/keyboard_nav/navigation_policies/block_navigation_policy.ts @@ -119,13 +119,12 @@ export class BlockNavigationPolicy implements INavigationPolicy { * @returns A list of navigable/focusable children of the given block. */ function getBlockNavigationCandidates(block: BlockSvg): IFocusableNode[] { - // Collapsed blocks have no navigable children. - if (block.isCollapsed()) return []; - const candidates: IFocusableNode[] = []; // Icons and open bubbles are navigable. for (const icon of block.getIcons()) { + // Icons hidden when the block is collapsed shouldn't be navigable. + if (block.isCollapsed() && !icon.isShownWhenCollapsed()) continue; candidates.push(icon); let bubble; if ( @@ -162,7 +161,7 @@ function getBlockNavigationCandidates(block: BlockSvg): IFocusableNode[] { } // The block's next connection is navigable. - if (block.nextConnection) { + if (block.nextConnection && !block.isCollapsed()) { candidates.push(block.nextConnection); } diff --git a/packages/blockly/tests/mocha/navigation_test.js b/packages/blockly/tests/mocha/navigation_test.js index 92cb2dc8bba..6baa85968c3 100644 --- a/packages/blockly/tests/mocha/navigation_test.js +++ b/packages/blockly/tests/mocha/navigation_test.js @@ -774,6 +774,43 @@ suite('Navigation', function () { const inNode = this.navigator.getFirstChild(this.blocks.buttonBlock); assert.isNull(inNode); }); + test('reachesClickableFieldOnCollapsedInput', function () { + // Models a pattern where a clickable field is appended to + // COLLAPSED_INPUT_NAME when the block collapses. + const block = this.blocks.buttonBlock; + block.setCollapsed(true); + const input = block.getInput(Blockly.constants.COLLAPSED_INPUT_NAME); + const expandButton = new Blockly.FieldImage( + 'https://www.gstatic.com/codesite/ph/images/star_on.gif', + 16, + 16, + 'expand', + () => {}, + ); + input.appendField(expandButton); + + const inNode = this.navigator.getInNode(block); + assert.equal(inNode, expandButton); + }); + test('skipsIconsHiddenWhenCollapsed', function () { + // Comment icons are hidden when the block is collapsed, so they + // should not be navigable. + const block = this.blocks.statementInput1; + block.setCommentText('comment'); + block.setCollapsed(true); + assert.isNull(this.navigator.getInNode(block)); + }); + test('reachesIconsShownWhenCollapsed', function () { + // Warning icons remain shown when the block is collapsed, so they + // should still be navigable. + const block = this.blocks.statementInput1; + block.setWarningText('warning'); + block.setCollapsed(true); + assert.equal( + this.navigator.getInNode(block), + block.getIcon(Blockly.icons.IconType.WARNING), + ); + }); }); suite('Out', function () {