Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/blockly/core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,9 @@ export class BlockSvg
if (this.isDeadOrDying()) return;
const gesture = this.workspace.getGesture(e);
if (gesture) {
this.bringToFront();
gesture.setStartIcon(icon);
getFocusManager().focusNode(icon);
}
};
}
Expand Down
12 changes: 9 additions & 3 deletions packages/blockly/core/gesture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,12 @@ export class Gesture {
this.setStartWorkspace(ws);
this.mostRecentEvent = e;

if (!this.targetBlock && !this.startBubble && !this.startComment) {
if (
!this.targetBlock &&
!this.startBubble &&
!this.startComment &&
!this.startIcon
) {
// Ensure the workspace is selected if nothing else should be. Note that
// this is focusNode() instead of focusTree() because if any active node
// is focused in the workspace it should be defocused.
Expand Down Expand Up @@ -1009,8 +1014,9 @@ export class Gesture {
* @internal
*/
setStartBlock(block: BlockSvg) {
// If the gesture already went through a bubble, don't set the start block.
if (!this.startBlock && !this.startBubble) {
// If the gesture already went through a block child, don't set the start
// block.
if (!this.startBlock && !this.startBubble && !this.startIcon) {
this.startBlock = block;
if (block.isInFlyout && block !== block.getRootBlock()) {
this.setTargetBlock(block.getRootBlock());
Expand Down
7 changes: 6 additions & 1 deletion packages/blockly/core/icons/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type {Block} from '../block.js';
import type {BlockSvg} from '../block_svg.js';
import * as browserEvents from '../browser_events.js';
import type {IContextMenu} from '../interfaces/i_contextmenu.js';
import type {IFocusableTree} from '../interfaces/i_focusable_tree.js';
import {hasBubble} from '../interfaces/i_has_bubble.js';
import type {IIcon} from '../interfaces/i_icon.js';
Expand All @@ -26,7 +27,7 @@ import type {IconType} from './icon_types.js';
* block (such as warnings or comments) as opposed to fields, which provide
* "actual" information, related to how a block functions.
*/
export abstract class Icon implements IIcon {
export abstract class Icon implements IIcon, IContextMenu {
/**
* The position of this icon relative to its blocks top-start,
* in workspace units.
Expand Down Expand Up @@ -196,4 +197,8 @@ export abstract class Icon implements IIcon {
getSourceBlock(): Block {
return this.sourceBlock;
}

showContextMenu(e: PointerEvent) {
(this.getSourceBlock() as BlockSvg).showContextMenu(e);
}
}
62 changes: 62 additions & 0 deletions packages/blockly/tests/mocha/icon_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,34 @@
* SPDX-License-Identifier: Apache-2.0
*/

import * as Blockly from '../../build/src/core/blockly.js';
import {assert} from '../../node_modules/chai/index.js';
import {defineEmptyBlock} from './test_helpers/block_definitions.js';
import {MockIcon, MockSerializableIcon} from './test_helpers/icon_mocks.js';
import {
sharedTestSetup,
sharedTestTeardown,
} from './test_helpers/setup_teardown.js';
import {simulateClick} from './test_helpers/user_input.js';

class TestIcon extends Blockly.icons.Icon {
showContextMenu(e) {
const menuItems = [
{text: 'Test icon menu item', enabled: true, callback: () => {}},
];
Blockly.ContextMenu.show(
e,
menuItems,
false,
this.getSourceBlock().workspace,
this.workspaceLocation,
);
}

getType() {
new Blockly.icons.IconType('test');
}
}

suite('Icon', function () {
setup(function () {
Expand Down Expand Up @@ -366,4 +387,45 @@ suite('Icon', function () {
);
});
});

suite('Contextual menus', function () {
setup(function () {
this.workspace = Blockly.inject('blocklyDiv', {});
Blockly.icons.registry.register(
new Blockly.icons.IconType('test'),
TestIcon,
);

this.block = this.workspace.newBlock('empty_block');
this.block.initSvg();
});

test('are shown when icons are right clicked', function () {
const icon = new TestIcon(this.block);
this.block.addIcon(icon);
simulateClick(icon.getFocusableElement(), {button: 2});

const menu = document.querySelector('.blocklyContextMenu');
assert.isNotNull(menu);
assert.isTrue(menu.innerText.includes('Test icon menu item'));
});

test('default to the contextual menu of the parent block', function () {
this.block.setCommentText('hello there');
const icon = this.block.getIcon(Blockly.icons.IconType.COMMENT);
simulateClick(icon.getFocusableElement(), {button: 2});

const expectedItems =
Blockly.ContextMenuRegistry.registry.getContextMenuOptions({
block: this.block,
});

assert.isNotEmpty(expectedItems);
const menu = document.querySelector('.blocklyContextMenu');
for (const item of expectedItems) {
if (!item.text) continue;
assert.isTrue(menu.innerText.includes(item.text));
}
});
});
});