Skip to content

Commit c9fcb52

Browse files
authored
feat: Add support for keyboard navigation in block comments. (#668)
* feat: Add styling for focused bubbles. * fix: Navigate into bubbles of all bubble-having icons. * chore: Add tests for block comment navigation. * fix: Fix bug that could cause focus to move on Enter when dismissing a bubble. * fix: Fix timing issue with navigating into bubbles.
1 parent 48600df commit c9fcb52

File tree

3 files changed

+116
-6
lines changed

3 files changed

+116
-6
lines changed

src/actions/enter.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
renderManagement,
1919
comments,
2020
getFocusManager,
21+
hasBubble,
2122
} from 'blockly/core';
2223

2324
import type {Block} from 'blockly/core';
@@ -163,13 +164,11 @@ export class EnterAction {
163164
// opening a bubble of some sort. We then need to wait for the bubble to
164165
// appear before attempting to navigate into it.
165166
curNode.onClick();
166-
// This currently only works for MutatorIcons.
167-
// See icon_navigation_policy.
168-
if (curNode instanceof icons.MutatorIcon) {
169-
renderManagement.finishQueuedRenders().then(() => {
167+
renderManagement.finishQueuedRenders().then(() => {
168+
if (hasBubble(curNode) && curNode.bubbleIsVisible()) {
170169
cursor.in();
171-
});
172-
}
170+
}
171+
});
173172
return true;
174173
} else if (curNode instanceof comments.CommentBarButton) {
175174
curNode.performAction();

src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,14 @@ export class KeyboardNavigation {
296296
stroke: var(--blockly-active-node-color);
297297
stroke-width: var(--blockly-selection-width);
298298
}
299+
300+
/* The workspace itself is the active node. */
301+
.blocklyKeyboardNavigation
302+
.blocklyBubble.blocklyActiveFocus
303+
.blocklyDraggable {
304+
stroke: var(--blockly-active-node-color);
305+
stroke-width: var(--blockly-selection-width);
306+
}
299307
`);
300308

301309
// Keyboard-nav-specific styling for the context menu.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import * as chai from 'chai';
8+
import * as Blockly from 'blockly';
9+
import {
10+
focusOnBlock,
11+
getCurrentFocusNodeId,
12+
testSetup,
13+
sendKeyAndWait,
14+
testFileLocations,
15+
keyRight,
16+
} from './test_setup.js';
17+
import {Key} from 'webdriverio';
18+
19+
suite('Block comment navigation', function () {
20+
// Setting timeout to unlimited as these tests take a longer time to run than most mocha test
21+
this.timeout(0);
22+
23+
// Setup Selenium for all of the tests
24+
setup(async function () {
25+
this.browser = await testSetup(testFileLocations.NAVIGATION_TEST_BLOCKS);
26+
await this.browser.execute(() => {
27+
Blockly.getMainWorkspace()
28+
.getBlockById('p5_canvas_1')
29+
?.setCommentText('test comment');
30+
});
31+
});
32+
33+
test('Activating a block comment icon focuses the comment', async function () {
34+
await focusOnBlock(this.browser, 'p5_canvas_1');
35+
await keyRight(this.browser);
36+
await sendKeyAndWait(this.browser, Key.Enter);
37+
const focusedNodeId = await getCurrentFocusNodeId(this.browser);
38+
chai.assert.equal(focusedNodeId, 'blockly-2s_comment_textarea_');
39+
});
40+
41+
test('Escape from a focused comment focuses its bubble', async function () {
42+
await focusOnBlock(this.browser, 'p5_canvas_1');
43+
await keyRight(this.browser);
44+
await sendKeyAndWait(this.browser, Key.Enter);
45+
await sendKeyAndWait(this.browser, Key.Escape);
46+
const bubbleFocused = await this.browser.execute(() => {
47+
return (
48+
Blockly.getFocusManager().getFocusedNode() ===
49+
Blockly.getMainWorkspace()
50+
.getBlockById('p5_canvas_1')
51+
?.getIcon(Blockly.icons.IconType.COMMENT)
52+
?.getBubble()
53+
);
54+
});
55+
chai.assert.isTrue(bubbleFocused);
56+
});
57+
58+
test('Double Escape from a focused comment closes its bubble', async function () {
59+
await focusOnBlock(this.browser, 'p5_canvas_1');
60+
await keyRight(this.browser);
61+
await sendKeyAndWait(this.browser, Key.Enter);
62+
await sendKeyAndWait(this.browser, Key.Escape);
63+
await sendKeyAndWait(this.browser, Key.Escape);
64+
const bubbleVisible = await this.browser.execute(() => {
65+
return Blockly.getMainWorkspace()
66+
.getBlockById('p5_canvas_1')
67+
?.getIcon(Blockly.icons.IconType.COMMENT)
68+
?.bubbleIsVisible();
69+
});
70+
chai.assert.isFalse(bubbleVisible);
71+
});
72+
73+
test('Double Escape from a focused comment focuses the comment icon', async function () {
74+
await focusOnBlock(this.browser, 'p5_canvas_1');
75+
await keyRight(this.browser);
76+
await sendKeyAndWait(this.browser, Key.Enter);
77+
await sendKeyAndWait(this.browser, Key.Escape);
78+
await sendKeyAndWait(this.browser, Key.Escape);
79+
const commentIconFocused = await this.browser.execute(() => {
80+
return (
81+
Blockly.getFocusManager().getFocusedNode() ===
82+
Blockly.getMainWorkspace()
83+
.getBlockById('p5_canvas_1')
84+
?.getIcon(Blockly.icons.IconType.COMMENT)
85+
);
86+
});
87+
chai.assert.isTrue(commentIconFocused);
88+
});
89+
90+
test('Block comments can be edited', async function () {
91+
await focusOnBlock(this.browser, 'p5_canvas_1');
92+
await keyRight(this.browser);
93+
await sendKeyAndWait(this.browser, Key.Enter);
94+
await sendKeyAndWait(this.browser, 'Hello world');
95+
await sendKeyAndWait(this.browser, Key.Escape);
96+
const commentText = await this.browser.execute(() => {
97+
return Blockly.getMainWorkspace()
98+
.getBlockById('p5_canvas_1')
99+
?.getCommentText();
100+
});
101+
chai.assert.equal(commentText, 'test commentHello world');
102+
});
103+
});

0 commit comments

Comments
 (0)