mirror of
https://github.com/google/blockly.git
synced 2026-02-13 02:50:12 +01:00
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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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());
|
||||
|
||||
@@ -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';
|
||||
@@ -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.
|
||||
@@ -196,4 +197,8 @@ export abstract class Icon implements IIcon {
|
||||
getSourceBlock(): Block {
|
||||
return this.sourceBlock;
|
||||
}
|
||||
|
||||
showContextMenu(e: PointerEvent) {
|
||||
(this.getSourceBlock() as BlockSvg).showContextMenu(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* 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';
|
||||
@@ -11,6 +12,26 @@ 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 () {
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user