mirror of
https://github.com/google/blockly.git
synced 2026-04-29 00:20:11 +02:00
feat: Add a keyboard shortcut for displaying the contextual menu (#9602)
* feat: Add support for getting the contextual menu * feat: Add a keyboard shortcut for opening the contextual menu * test: Add tests for `ContextMenu.getMenu()`. * test: Add tests for context menu keyboard shortcut * fix: Fix tests when not run on their own * chore: Add type annotation
This commit is contained in:
@@ -238,6 +238,8 @@ function haltPropagation(e: Event) {
|
||||
export function hide() {
|
||||
WidgetDiv.hideIfOwner(dummyOwner);
|
||||
currentBlock = null;
|
||||
menu_?.dispose();
|
||||
menu_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,3 +295,10 @@ export function callbackFactory(
|
||||
return newBlock;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contextual menu if it is currently being shown.
|
||||
*/
|
||||
export function getMenu(): Menu | null {
|
||||
return menu_;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
import {BlockSvg} from './block_svg.js';
|
||||
import * as clipboard from './clipboard.js';
|
||||
import {RenderedWorkspaceComment} from './comments.js';
|
||||
import * as contextmenu from './contextmenu.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {getFocusManager} from './focus_manager.js';
|
||||
import {hasContextMenu} from './interfaces/i_contextmenu.js';
|
||||
import {isCopyable as isICopyable} from './interfaces/i_copyable.js';
|
||||
import {isDeletable as isIDeletable} from './interfaces/i_deletable.js';
|
||||
import {isDraggable} from './interfaces/i_draggable.js';
|
||||
@@ -33,6 +35,7 @@ export enum names {
|
||||
PASTE = 'paste',
|
||||
UNDO = 'undo',
|
||||
REDO = 'redo',
|
||||
MENU = 'menu',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,6 +374,35 @@ export function registerRedo() {
|
||||
ShortcutRegistry.registry.register(redoShortcut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard shortcut to show the context menu on ctrl/cmd+Enter.
|
||||
*/
|
||||
export function registerShowContextMenu() {
|
||||
const ctrlEnter = ShortcutRegistry.registry.createSerializedKey(
|
||||
KeyCodes.ENTER,
|
||||
[KeyCodes.CTRL_CMD],
|
||||
);
|
||||
|
||||
const contextMenuShortcut: KeyboardShortcut = {
|
||||
name: names.MENU,
|
||||
preconditionFn: (workspace) => {
|
||||
return !workspace.isDragging();
|
||||
},
|
||||
callback: (workspace, e) => {
|
||||
const target = getFocusManager().getFocusedNode();
|
||||
if (hasContextMenu(target)) {
|
||||
target.showContextMenu(e);
|
||||
contextmenu.getMenu()?.highlightNext();
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
keyCodes: [ctrlEnter],
|
||||
};
|
||||
ShortcutRegistry.registry.register(contextMenuShortcut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all default keyboard shortcut item. This should be called once per
|
||||
* instance of KeyboardShortcutRegistry.
|
||||
@@ -385,6 +417,7 @@ export function registerDefaultShortcuts() {
|
||||
registerPaste();
|
||||
registerUndo();
|
||||
registerRedo();
|
||||
registerShowContextMenu();
|
||||
}
|
||||
|
||||
registerDefaultShortcuts();
|
||||
|
||||
@@ -70,4 +70,25 @@ suite('Context Menu', function () {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getMenu', function () {
|
||||
test('returns null when context menu is not shown', function () {
|
||||
assert.isNull(Blockly.ContextMenu.getMenu());
|
||||
});
|
||||
|
||||
test('returns Menu instance when context menu is shown', function () {
|
||||
const e = new PointerEvent('pointerdown', {clientX: 10, clientY: 10});
|
||||
const menuOptions = [
|
||||
{text: 'Test option', enabled: true, callback: function () {}},
|
||||
];
|
||||
Blockly.ContextMenu.show(e, menuOptions, false, this.workspace);
|
||||
|
||||
const menu = Blockly.ContextMenu.getMenu();
|
||||
assert.instanceOf(menu, Blockly.Menu, 'getMenu() should return a Menu');
|
||||
assert.include(menu.getElement().innerText, 'Test option');
|
||||
|
||||
Blockly.ContextMenu.hide();
|
||||
assert.isNull(Blockly.ContextMenu.getMenu());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,8 @@ suite('Keyboard Shortcut Items', function () {
|
||||
sharedTestSetup.call(this);
|
||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||
this.injectionDiv = this.workspace.getInjectionDiv();
|
||||
Blockly.ContextMenuRegistry.registry.reset();
|
||||
Blockly.ContextMenuItems.registerDefaultOptions();
|
||||
});
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
@@ -403,4 +405,85 @@ suite('Keyboard Shortcut Items', function () {
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
suite('Show context menu (Ctrl/Cmd+Enter)', function () {
|
||||
const contextMenuKeyEvent = createKeyDownEvent(
|
||||
Blockly.utils.KeyCodes.ENTER,
|
||||
[Blockly.utils.KeyCodes.CTRL_CMD],
|
||||
);
|
||||
|
||||
test('Displays context menu on a block using the keyboard shortcut', function () {
|
||||
const block = setSelectedBlock(this.workspace);
|
||||
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
|
||||
|
||||
const menu = Blockly.ContextMenu.getMenu();
|
||||
assert.instanceOf(menu, Blockly.Menu, 'Context menu should be shown');
|
||||
|
||||
const menuOptions =
|
||||
Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
|
||||
{block, focusedNode: block},
|
||||
contextMenuKeyEvent,
|
||||
);
|
||||
for (const option of menuOptions) {
|
||||
assert.include(menu.getElement().innerText, option.text);
|
||||
}
|
||||
});
|
||||
|
||||
test('Displays context menu on the workspace using the keyboard shortcut', function () {
|
||||
Blockly.getFocusManager().focusNode(this.workspace);
|
||||
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
|
||||
|
||||
const menu = Blockly.ContextMenu.getMenu();
|
||||
assert.instanceOf(menu, Blockly.Menu, 'Context menu should be shown');
|
||||
const menuOptions =
|
||||
Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
|
||||
{workspace: this.workspace, focusedNode: this.workspace},
|
||||
contextMenuKeyEvent,
|
||||
);
|
||||
for (const option of menuOptions) {
|
||||
assert.include(menu.getElement().innerText, option.text);
|
||||
}
|
||||
});
|
||||
|
||||
test('Displays context menu on a workspace comment using the keyboard shortcut', function () {
|
||||
Blockly.ContextMenuItems.registerCommentOptions();
|
||||
const comment = setSelectedComment(this.workspace);
|
||||
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
|
||||
|
||||
const menu = Blockly.ContextMenu.getMenu();
|
||||
assert.instanceOf(menu, Blockly.Menu, 'Context menu should be shown');
|
||||
const menuOptions =
|
||||
Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
|
||||
{comment, focusedNode: comment},
|
||||
contextMenuKeyEvent,
|
||||
);
|
||||
for (const option of menuOptions) {
|
||||
assert.include(menu.getElement().innerText, option.text);
|
||||
}
|
||||
});
|
||||
|
||||
test('First menu item is highlighted when context menu is shown via keyboard shortcut', function () {
|
||||
setSelectedBlock(this.workspace);
|
||||
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
|
||||
|
||||
const menuEl = Blockly.ContextMenu.getMenu().getElement();
|
||||
const firstMenuItem = menuEl.querySelector('.blocklyMenuItem');
|
||||
assert.isTrue(
|
||||
firstMenuItem.classList.contains('blocklyMenuItemHighlight'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Context menu is not shown when shortcut is invoked while a field is focused', function () {
|
||||
const block = this.workspace.newBlock('math_arithmetic');
|
||||
block.initSvg();
|
||||
const field = block.getField('OP');
|
||||
Blockly.getFocusManager().focusNode(field);
|
||||
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
|
||||
|
||||
assert.isNull(
|
||||
Blockly.ContextMenu.getMenu(),
|
||||
'Context menu should not be triggered when a field is focused',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user