Skip to content

Commit 4657199

Browse files
authored
feat: Add support for displaying contextual menus on icons (#9581)
* feat: Add support for displaying contextual menus on icons * test: Add tests for contextual menus on icons * fix: Designate `Icon` as implementing `IContextMenu` * fix: Don't write Typescript in a JS file
1 parent 65bc2b5 commit 4657199

4 files changed

Lines changed: 79 additions & 4 deletions

File tree

packages/blockly/core/block_svg.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,9 @@ export class BlockSvg
11261126
if (this.isDeadOrDying()) return;
11271127
const gesture = this.workspace.getGesture(e);
11281128
if (gesture) {
1129+
this.bringToFront();
11291130
gesture.setStartIcon(icon);
1131+
getFocusManager().focusNode(icon);
11301132
}
11311133
};
11321134
}

packages/blockly/core/gesture.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,12 @@ export class Gesture {
764764
this.setStartWorkspace(ws);
765765
this.mostRecentEvent = e;
766766

767-
if (!this.targetBlock && !this.startBubble && !this.startComment) {
767+
if (
768+
!this.targetBlock &&
769+
!this.startBubble &&
770+
!this.startComment &&
771+
!this.startIcon
772+
) {
768773
// Ensure the workspace is selected if nothing else should be. Note that
769774
// this is focusNode() instead of focusTree() because if any active node
770775
// is focused in the workspace it should be defocused.
@@ -1009,8 +1014,9 @@ export class Gesture {
10091014
* @internal
10101015
*/
10111016
setStartBlock(block: BlockSvg) {
1012-
// If the gesture already went through a bubble, don't set the start block.
1013-
if (!this.startBlock && !this.startBubble) {
1017+
// If the gesture already went through a block child, don't set the start
1018+
// block.
1019+
if (!this.startBlock && !this.startBubble && !this.startIcon) {
10141020
this.startBlock = block;
10151021
if (block.isInFlyout && block !== block.getRootBlock()) {
10161022
this.setTargetBlock(block.getRootBlock());

packages/blockly/core/icons/icon.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import type {Block} from '../block.js';
88
import type {BlockSvg} from '../block_svg.js';
99
import * as browserEvents from '../browser_events.js';
10+
import type {IContextMenu} from '../interfaces/i_contextmenu.js';
1011
import type {IFocusableTree} from '../interfaces/i_focusable_tree.js';
1112
import {hasBubble} from '../interfaces/i_has_bubble.js';
1213
import type {IIcon} from '../interfaces/i_icon.js';
@@ -26,7 +27,7 @@ import type {IconType} from './icon_types.js';
2627
* block (such as warnings or comments) as opposed to fields, which provide
2728
* "actual" information, related to how a block functions.
2829
*/
29-
export abstract class Icon implements IIcon {
30+
export abstract class Icon implements IIcon, IContextMenu {
3031
/**
3132
* The position of this icon relative to its blocks top-start,
3233
* in workspace units.
@@ -196,4 +197,8 @@ export abstract class Icon implements IIcon {
196197
getSourceBlock(): Block {
197198
return this.sourceBlock;
198199
}
200+
201+
showContextMenu(e: PointerEvent) {
202+
(this.getSourceBlock() as BlockSvg).showContextMenu(e);
203+
}
199204
}

packages/blockly/tests/mocha/icon_test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,34 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import * as Blockly from '../../build/src/core/blockly.js';
78
import {assert} from '../../node_modules/chai/index.js';
89
import {defineEmptyBlock} from './test_helpers/block_definitions.js';
910
import {MockIcon, MockSerializableIcon} from './test_helpers/icon_mocks.js';
1011
import {
1112
sharedTestSetup,
1213
sharedTestTeardown,
1314
} from './test_helpers/setup_teardown.js';
15+
import {simulateClick} from './test_helpers/user_input.js';
16+
17+
class TestIcon extends Blockly.icons.Icon {
18+
showContextMenu(e) {
19+
const menuItems = [
20+
{text: 'Test icon menu item', enabled: true, callback: () => {}},
21+
];
22+
Blockly.ContextMenu.show(
23+
e,
24+
menuItems,
25+
false,
26+
this.getSourceBlock().workspace,
27+
this.workspaceLocation,
28+
);
29+
}
30+
31+
getType() {
32+
new Blockly.icons.IconType('test');
33+
}
34+
}
1435

1536
suite('Icon', function () {
1637
setup(function () {
@@ -366,4 +387,45 @@ suite('Icon', function () {
366387
);
367388
});
368389
});
390+
391+
suite('Contextual menus', function () {
392+
setup(function () {
393+
this.workspace = Blockly.inject('blocklyDiv', {});
394+
Blockly.icons.registry.register(
395+
new Blockly.icons.IconType('test'),
396+
TestIcon,
397+
);
398+
399+
this.block = this.workspace.newBlock('empty_block');
400+
this.block.initSvg();
401+
});
402+
403+
test('are shown when icons are right clicked', function () {
404+
const icon = new TestIcon(this.block);
405+
this.block.addIcon(icon);
406+
simulateClick(icon.getFocusableElement(), {button: 2});
407+
408+
const menu = document.querySelector('.blocklyContextMenu');
409+
assert.isNotNull(menu);
410+
assert.isTrue(menu.innerText.includes('Test icon menu item'));
411+
});
412+
413+
test('default to the contextual menu of the parent block', function () {
414+
this.block.setCommentText('hello there');
415+
const icon = this.block.getIcon(Blockly.icons.IconType.COMMENT);
416+
simulateClick(icon.getFocusableElement(), {button: 2});
417+
418+
const expectedItems =
419+
Blockly.ContextMenuRegistry.registry.getContextMenuOptions({
420+
block: this.block,
421+
});
422+
423+
assert.isNotEmpty(expectedItems);
424+
const menu = document.querySelector('.blocklyContextMenu');
425+
for (const item of expectedItems) {
426+
if (!item.text) continue;
427+
assert.isTrue(menu.innerText.includes(item.text));
428+
}
429+
});
430+
});
369431
});

0 commit comments

Comments
 (0)