Skip to content

Commit a16580a

Browse files
authored
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
1 parent 3d78491 commit a16580a

4 files changed

Lines changed: 146 additions & 0 deletions

File tree

packages/blockly/core/contextmenu.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ function haltPropagation(e: Event) {
238238
export function hide() {
239239
WidgetDiv.hideIfOwner(dummyOwner);
240240
currentBlock = null;
241+
menu_?.dispose();
242+
menu_ = null;
241243
}
242244

243245
/**
@@ -293,3 +295,10 @@ export function callbackFactory(
293295
return newBlock;
294296
};
295297
}
298+
299+
/**
300+
* Returns the contextual menu if it is currently being shown.
301+
*/
302+
export function getMenu(): Menu | null {
303+
return menu_;
304+
}

packages/blockly/core/shortcut_items.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
import {BlockSvg} from './block_svg.js';
1010
import * as clipboard from './clipboard.js';
1111
import {RenderedWorkspaceComment} from './comments.js';
12+
import * as contextmenu from './contextmenu.js';
1213
import * as eventUtils from './events/utils.js';
1314
import {getFocusManager} from './focus_manager.js';
15+
import {hasContextMenu} from './interfaces/i_contextmenu.js';
1416
import {isCopyable as isICopyable} from './interfaces/i_copyable.js';
1517
import {isDeletable as isIDeletable} from './interfaces/i_deletable.js';
1618
import {isDraggable} from './interfaces/i_draggable.js';
@@ -33,6 +35,7 @@ export enum names {
3335
PASTE = 'paste',
3436
UNDO = 'undo',
3537
REDO = 'redo',
38+
MENU = 'menu',
3639
}
3740

3841
/**
@@ -371,6 +374,35 @@ export function registerRedo() {
371374
ShortcutRegistry.registry.register(redoShortcut);
372375
}
373376

377+
/**
378+
* Keyboard shortcut to show the context menu on ctrl/cmd+Enter.
379+
*/
380+
export function registerShowContextMenu() {
381+
const ctrlEnter = ShortcutRegistry.registry.createSerializedKey(
382+
KeyCodes.ENTER,
383+
[KeyCodes.CTRL_CMD],
384+
);
385+
386+
const contextMenuShortcut: KeyboardShortcut = {
387+
name: names.MENU,
388+
preconditionFn: (workspace) => {
389+
return !workspace.isDragging();
390+
},
391+
callback: (workspace, e) => {
392+
const target = getFocusManager().getFocusedNode();
393+
if (hasContextMenu(target)) {
394+
target.showContextMenu(e);
395+
contextmenu.getMenu()?.highlightNext();
396+
397+
return true;
398+
}
399+
return false;
400+
},
401+
keyCodes: [ctrlEnter],
402+
};
403+
ShortcutRegistry.registry.register(contextMenuShortcut);
404+
}
405+
374406
/**
375407
* Registers all default keyboard shortcut item. This should be called once per
376408
* instance of KeyboardShortcutRegistry.
@@ -385,6 +417,7 @@ export function registerDefaultShortcuts() {
385417
registerPaste();
386418
registerUndo();
387419
registerRedo();
420+
registerShowContextMenu();
388421
}
389422

390423
registerDefaultShortcuts();

packages/blockly/tests/mocha/contextmenu_test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,25 @@ suite('Context Menu', function () {
7070
);
7171
});
7272
});
73+
74+
suite('getMenu', function () {
75+
test('returns null when context menu is not shown', function () {
76+
assert.isNull(Blockly.ContextMenu.getMenu());
77+
});
78+
79+
test('returns Menu instance when context menu is shown', function () {
80+
const e = new PointerEvent('pointerdown', {clientX: 10, clientY: 10});
81+
const menuOptions = [
82+
{text: 'Test option', enabled: true, callback: function () {}},
83+
];
84+
Blockly.ContextMenu.show(e, menuOptions, false, this.workspace);
85+
86+
const menu = Blockly.ContextMenu.getMenu();
87+
assert.instanceOf(menu, Blockly.Menu, 'getMenu() should return a Menu');
88+
assert.include(menu.getElement().innerText, 'Test option');
89+
90+
Blockly.ContextMenu.hide();
91+
assert.isNull(Blockly.ContextMenu.getMenu());
92+
});
93+
});
7394
});

packages/blockly/tests/mocha/shortcut_items_test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ suite('Keyboard Shortcut Items', function () {
1818
sharedTestSetup.call(this);
1919
this.workspace = Blockly.inject('blocklyDiv', {});
2020
this.injectionDiv = this.workspace.getInjectionDiv();
21+
Blockly.ContextMenuRegistry.registry.reset();
22+
Blockly.ContextMenuItems.registerDefaultOptions();
2123
});
2224
teardown(function () {
2325
sharedTestTeardown.call(this);
@@ -403,4 +405,85 @@ suite('Keyboard Shortcut Items', function () {
403405
]),
404406
);
405407
});
408+
409+
suite('Show context menu (Ctrl/Cmd+Enter)', function () {
410+
const contextMenuKeyEvent = createKeyDownEvent(
411+
Blockly.utils.KeyCodes.ENTER,
412+
[Blockly.utils.KeyCodes.CTRL_CMD],
413+
);
414+
415+
test('Displays context menu on a block using the keyboard shortcut', function () {
416+
const block = setSelectedBlock(this.workspace);
417+
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
418+
419+
const menu = Blockly.ContextMenu.getMenu();
420+
assert.instanceOf(menu, Blockly.Menu, 'Context menu should be shown');
421+
422+
const menuOptions =
423+
Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
424+
{block, focusedNode: block},
425+
contextMenuKeyEvent,
426+
);
427+
for (const option of menuOptions) {
428+
assert.include(menu.getElement().innerText, option.text);
429+
}
430+
});
431+
432+
test('Displays context menu on the workspace using the keyboard shortcut', function () {
433+
Blockly.getFocusManager().focusNode(this.workspace);
434+
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
435+
436+
const menu = Blockly.ContextMenu.getMenu();
437+
assert.instanceOf(menu, Blockly.Menu, 'Context menu should be shown');
438+
const menuOptions =
439+
Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
440+
{workspace: this.workspace, focusedNode: this.workspace},
441+
contextMenuKeyEvent,
442+
);
443+
for (const option of menuOptions) {
444+
assert.include(menu.getElement().innerText, option.text);
445+
}
446+
});
447+
448+
test('Displays context menu on a workspace comment using the keyboard shortcut', function () {
449+
Blockly.ContextMenuItems.registerCommentOptions();
450+
const comment = setSelectedComment(this.workspace);
451+
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
452+
453+
const menu = Blockly.ContextMenu.getMenu();
454+
assert.instanceOf(menu, Blockly.Menu, 'Context menu should be shown');
455+
const menuOptions =
456+
Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
457+
{comment, focusedNode: comment},
458+
contextMenuKeyEvent,
459+
);
460+
for (const option of menuOptions) {
461+
assert.include(menu.getElement().innerText, option.text);
462+
}
463+
});
464+
465+
test('First menu item is highlighted when context menu is shown via keyboard shortcut', function () {
466+
setSelectedBlock(this.workspace);
467+
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
468+
469+
const menuEl = Blockly.ContextMenu.getMenu().getElement();
470+
const firstMenuItem = menuEl.querySelector('.blocklyMenuItem');
471+
assert.isTrue(
472+
firstMenuItem.classList.contains('blocklyMenuItemHighlight'),
473+
);
474+
});
475+
476+
test('Context menu is not shown when shortcut is invoked while a field is focused', function () {
477+
const block = this.workspace.newBlock('math_arithmetic');
478+
block.initSvg();
479+
const field = block.getField('OP');
480+
Blockly.getFocusManager().focusNode(field);
481+
this.injectionDiv.dispatchEvent(contextMenuKeyEvent);
482+
483+
assert.isNull(
484+
Blockly.ContextMenu.getMenu(),
485+
'Context menu should not be triggered when a field is focused',
486+
);
487+
});
488+
});
406489
});

0 commit comments

Comments
 (0)