From da544af1b8ff21a0184bf06d0c9b7de65842d036 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Jul 2025 09:57:16 -0700 Subject: [PATCH 1/5] feat: Add styling for focused bubbles. --- src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/index.ts b/src/index.ts index 840f87d8..d73beb28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -296,6 +296,14 @@ export class KeyboardNavigation { stroke: var(--blockly-active-node-color); stroke-width: var(--blockly-selection-width); } + + /* The workspace itself is the active node. */ + .blocklyKeyboardNavigation + .blocklyBubble.blocklyActiveFocus + .blocklyDraggable { + stroke: var(--blockly-active-node-color); + stroke-width: var(--blockly-selection-width); + } `); // Keyboard-nav-specific styling for the context menu. From 66438e08cf6e1dd4f9523b9e178063b22306cf6f Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Jul 2025 09:57:40 -0700 Subject: [PATCH 2/5] fix: Navigate into bubbles of all bubble-having icons. --- src/actions/enter.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/actions/enter.ts b/src/actions/enter.ts index 72cb399f..89549dbd 100644 --- a/src/actions/enter.ts +++ b/src/actions/enter.ts @@ -18,6 +18,7 @@ import { renderManagement, comments, getFocusManager, + hasBubble, } from 'blockly/core'; import type {Block} from 'blockly/core'; @@ -164,9 +165,7 @@ export class EnterAction { // opening a bubble of some sort. We then need to wait for the bubble to // appear before attempting to navigate into it. curNode.onClick(); - // This currently only works for MutatorIcons. - // See icon_navigation_policy. - if (curNode instanceof icons.MutatorIcon) { + if (hasBubble(curNode)) { renderManagement.finishQueuedRenders().then(() => { cursor?.in(); }); From 860bf8c52c55449488325b2cc9bb7faa2fc4f80f Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Jul 2025 12:27:56 -0700 Subject: [PATCH 3/5] chore: Add tests for block comment navigation. --- test/webdriverio/test/block_comment_test.ts | 103 ++++++++++++++++++ .../test/workspace_comment_test.ts | 1 - 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 test/webdriverio/test/block_comment_test.ts diff --git a/test/webdriverio/test/block_comment_test.ts b/test/webdriverio/test/block_comment_test.ts new file mode 100644 index 00000000..e965f3ff --- /dev/null +++ b/test/webdriverio/test/block_comment_test.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as chai from 'chai'; +import * as Blockly from 'blockly'; +import { + focusOnBlock, + getCurrentFocusNodeId, + testSetup, + sendKeyAndWait, + testFileLocations, + keyRight, +} from './test_setup.js'; +import {Key} from 'webdriverio'; + +suite('Block comment navigation', function () { + // Setting timeout to unlimited as these tests take a longer time to run than most mocha test + this.timeout(0); + + // Setup Selenium for all of the tests + setup(async function () { + this.browser = await testSetup(testFileLocations.NAVIGATION_TEST_BLOCKS); + await this.browser.execute(() => { + Blockly.getMainWorkspace() + .getBlockById('p5_canvas_1') + ?.setCommentText('test comment'); + }); + }); + + test('Activating a block comment icon focuses the comment', async function () { + await focusOnBlock(this.browser, 'p5_canvas_1'); + await keyRight(this.browser); + await sendKeyAndWait(this.browser, Key.Enter); + const focusedNodeId = await getCurrentFocusNodeId(this.browser); + chai.assert.equal(focusedNodeId, 'blockly-2s_comment_textarea_'); + }); + + test('Escape from a focused comment focuses its bubble', async function () { + await focusOnBlock(this.browser, 'p5_canvas_1'); + await keyRight(this.browser); + await sendKeyAndWait(this.browser, Key.Enter); + await sendKeyAndWait(this.browser, Key.Escape); + const bubbleFocused = await this.browser.execute(() => { + return ( + Blockly.getFocusManager().getFocusedNode() === + Blockly.getMainWorkspace() + .getBlockById('p5_canvas_1') + ?.getIcon(Blockly.icons.IconType.COMMENT) + ?.getBubble() + ); + }); + chai.assert.isTrue(bubbleFocused); + }); + + test('Double Escape from a focused comment closes its bubble', async function () { + await focusOnBlock(this.browser, 'p5_canvas_1'); + await keyRight(this.browser); + await sendKeyAndWait(this.browser, Key.Enter); + await sendKeyAndWait(this.browser, Key.Escape); + await sendKeyAndWait(this.browser, Key.Escape); + const bubbleVisible = await this.browser.execute(() => { + return Blockly.getMainWorkspace() + .getBlockById('p5_canvas_1') + ?.getIcon(Blockly.icons.IconType.COMMENT) + ?.bubbleIsVisible(); + }); + chai.assert.isFalse(bubbleVisible); + }); + + test('Double Escape from a focused comment focuses the comment icon', async function () { + await focusOnBlock(this.browser, 'p5_canvas_1'); + await keyRight(this.browser); + await sendKeyAndWait(this.browser, Key.Enter); + await sendKeyAndWait(this.browser, Key.Escape); + await sendKeyAndWait(this.browser, Key.Escape); + const commentIconFocused = await this.browser.execute(() => { + return ( + Blockly.getFocusManager().getFocusedNode() === + Blockly.getMainWorkspace() + .getBlockById('p5_canvas_1') + ?.getIcon(Blockly.icons.IconType.COMMENT) + ); + }); + chai.assert.isTrue(commentIconFocused); + }); + + test('Block comments can be edited', async function () { + await focusOnBlock(this.browser, 'p5_canvas_1'); + await keyRight(this.browser); + await sendKeyAndWait(this.browser, Key.Enter); + await sendKeyAndWait(this.browser, 'Hello world'); + await sendKeyAndWait(this.browser, Key.Escape); + const commentText = await this.browser.execute(() => { + return Blockly.getMainWorkspace() + .getBlockById('p5_canvas_1') + ?.getCommentText(); + }); + chai.assert.equal(commentText, 'test commentHello world'); + }); +}); diff --git a/test/webdriverio/test/workspace_comment_test.ts b/test/webdriverio/test/workspace_comment_test.ts index 82d6fe12..eb882e27 100644 --- a/test/webdriverio/test/workspace_comment_test.ts +++ b/test/webdriverio/test/workspace_comment_test.ts @@ -7,7 +7,6 @@ import * as chai from 'chai'; import * as Blockly from 'blockly'; import { - contextMenuExists, focusOnBlock, getCurrentFocusNodeId, getFocusedBlockType, From 376bf6b4c3fb6c254071bde470b0731e7e31af76 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 15 Jul 2025 08:52:49 -0700 Subject: [PATCH 4/5] fix: Fix bug that could cause focus to move on Enter when dismissing a bubble. --- src/actions/enter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/enter.ts b/src/actions/enter.ts index 89549dbd..ce47d2d8 100644 --- a/src/actions/enter.ts +++ b/src/actions/enter.ts @@ -165,7 +165,7 @@ export class EnterAction { // opening a bubble of some sort. We then need to wait for the bubble to // appear before attempting to navigate into it. curNode.onClick(); - if (hasBubble(curNode)) { + if (hasBubble(curNode) && curNode.bubbleIsVisible()) { renderManagement.finishQueuedRenders().then(() => { cursor?.in(); }); From 52cd21026ae124b483f6b45e4562404195ae5435 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 15 Jul 2025 10:23:39 -0700 Subject: [PATCH 5/5] fix: Fix timing issue with navigating into bubbles. --- src/actions/enter.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/actions/enter.ts b/src/actions/enter.ts index ce47d2d8..b68e8af2 100644 --- a/src/actions/enter.ts +++ b/src/actions/enter.ts @@ -165,11 +165,11 @@ export class EnterAction { // opening a bubble of some sort. We then need to wait for the bubble to // appear before attempting to navigate into it. curNode.onClick(); - if (hasBubble(curNode) && curNode.bubbleIsVisible()) { - renderManagement.finishQueuedRenders().then(() => { + renderManagement.finishQueuedRenders().then(() => { + if (hasBubble(curNode) && curNode.bubbleIsVisible()) { cursor?.in(); - }); - } + } + }); return true; } else if (curNode instanceof comments.CommentBarButton) { curNode.performAction();