Skip to content

Commit 25968ff

Browse files
authored
feat: Add keyboard shortcut to focus the workspace (#9615)
* feat: Add keyboard shortcut to focus the workspace * test: Added tests for keyboard shortcut to focus workspace * fix: Disable the focus workspace shortcut while dragging
1 parent 09d19d8 commit 25968ff

2 files changed

Lines changed: 115 additions & 9 deletions

File tree

packages/blockly/core/shortcut_items.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ export enum names {
3838
UNDO = 'undo',
3939
REDO = 'redo',
4040
MENU = 'menu',
41+
FOCUS_WORKSPACE = 'focus_workspace',
42+
START_MOVE = 'start_move',
43+
FINISH_MOVE = 'finish_move',
44+
ABORT_MOVE = 'abort_move',
45+
MOVE_UP = 'move_up',
46+
MOVE_DOWN = 'move_down',
47+
MOVE_LEFT = 'move_left',
48+
MOVE_RIGHT = 'move_right',
4149
}
4250

4351
/**
@@ -391,7 +399,7 @@ export function registerMovementShortcuts() {
391399

392400
const shortcuts: ShortcutRegistry.KeyboardShortcut[] = [
393401
{
394-
name: 'start_move',
402+
name: names.START_MOVE,
395403
preconditionFn: (workspace) => {
396404
const startDraggable = getCurrentDraggable(workspace);
397405
return !!startDraggable && KeyboardMover.mover.canMove(startDraggable);
@@ -412,23 +420,23 @@ export function registerMovementShortcuts() {
412420
keyCodes: [KeyCodes.M],
413421
},
414422
{
415-
name: 'finish_move',
423+
name: names.FINISH_MOVE,
416424
preconditionFn: () => KeyboardMover.mover.isMoving(),
417425
callback: (_workspace, e) =>
418426
KeyboardMover.mover.finishMove(e as KeyboardEvent),
419427
keyCodes: [KeyCodes.ENTER, KeyCodes.SPACE],
420428
allowCollision: true,
421429
},
422430
{
423-
name: 'abort_move',
431+
name: names.ABORT_MOVE,
424432
preconditionFn: () => KeyboardMover.mover.isMoving(),
425433
callback: (_workspace, e) =>
426434
KeyboardMover.mover.abortMove(e as KeyboardEvent),
427435
keyCodes: [KeyCodes.ESC],
428436
allowCollision: true,
429437
},
430438
{
431-
name: 'move_left',
439+
name: names.MOVE_LEFT,
432440
preconditionFn: () => KeyboardMover.mover.isMoving(),
433441
callback: (_workspace, e) => {
434442
e.preventDefault();
@@ -443,7 +451,7 @@ export function registerMovementShortcuts() {
443451
allowCollision: true,
444452
},
445453
{
446-
name: 'move_right',
454+
name: names.MOVE_RIGHT,
447455
preconditionFn: () => KeyboardMover.mover.isMoving(),
448456
callback: (_workspace, e) => {
449457
e.preventDefault();
@@ -458,7 +466,7 @@ export function registerMovementShortcuts() {
458466
allowCollision: true,
459467
},
460468
{
461-
name: 'move_up',
469+
name: names.MOVE_UP,
462470
preconditionFn: () => KeyboardMover.mover.isMoving(),
463471
callback: (_workspace, e) => {
464472
e.preventDefault();
@@ -473,7 +481,7 @@ export function registerMovementShortcuts() {
473481
allowCollision: true,
474482
},
475483
{
476-
name: 'move_down',
484+
name: names.MOVE_DOWN,
477485
preconditionFn: () => KeyboardMover.mover.isMoving(),
478486
callback: (_workspace, e) => {
479487
e.preventDefault();
@@ -508,7 +516,7 @@ export function registerShowContextMenu() {
508516
preconditionFn: (workspace) => {
509517
return !workspace.isDragging();
510518
},
511-
callback: (workspace, e) => {
519+
callback: (_workspace, e) => {
512520
const target = getFocusManager().getFocusedNode();
513521
if (hasContextMenu(target)) {
514522
target.showContextMenu(e);
@@ -523,6 +531,33 @@ export function registerShowContextMenu() {
523531
ShortcutRegistry.registry.register(contextMenuShortcut);
524532
}
525533

534+
/**
535+
* Registers keyboard shortcut to focus the workspace.
536+
*/
537+
export function registerFocusWorkspace() {
538+
const resolveWorkspace = (workspace: WorkspaceSvg) => {
539+
if (workspace.isFlyout) {
540+
const target = workspace.targetWorkspace;
541+
if (target) {
542+
return resolveWorkspace(target);
543+
}
544+
}
545+
return workspace.getRootWorkspace() ?? workspace;
546+
};
547+
548+
const contextMenuShortcut: KeyboardShortcut = {
549+
name: names.FOCUS_WORKSPACE,
550+
preconditionFn: (workspace) => !workspace.isDragging(),
551+
callback: (workspace) => {
552+
keyboardNavigationController.setIsActive(true);
553+
getFocusManager().focusNode(resolveWorkspace(workspace));
554+
return true;
555+
},
556+
keyCodes: [KeyCodes.W],
557+
};
558+
ShortcutRegistry.registry.register(contextMenuShortcut);
559+
}
560+
526561
/**
527562
* Registers all default keyboard shortcut item. This should be called once per
528563
* instance of KeyboardShortcutRegistry.
@@ -537,8 +572,17 @@ export function registerDefaultShortcuts() {
537572
registerPaste();
538573
registerUndo();
539574
registerRedo();
575+
}
576+
577+
/**
578+
* Registers an extended set of keyboard shortcuts used to support deep keyboard
579+
* navigation of Blockly.
580+
*/
581+
export function registerKeyboardNavigationShortcuts() {
540582
registerShowContextMenu();
541583
registerMovementShortcuts();
584+
registerFocusWorkspace();
542585
}
543586

544587
registerDefaultShortcuts();
588+
registerKeyboardNavigationShortcuts();

packages/blockly/tests/mocha/shortcut_items_test.js

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {createKeyDownEvent} from './test_helpers/user_input.js';
1616
suite('Keyboard Shortcut Items', function () {
1717
setup(function () {
1818
sharedTestSetup.call(this);
19-
this.workspace = Blockly.inject('blocklyDiv', {});
19+
const toolbox = document.getElementById('toolbox-categories');
20+
this.workspace = Blockly.inject('blocklyDiv', {toolbox});
2021
this.injectionDiv = this.workspace.getInjectionDiv();
2122
Blockly.ContextMenuRegistry.registry.reset();
2223
Blockly.ContextMenuItems.registerDefaultOptions();
@@ -486,4 +487,65 @@ suite('Keyboard Shortcut Items', function () {
486487
);
487488
});
488489
});
490+
491+
suite('Focus Workspace (W)', function () {
492+
setup(function () {
493+
this.testFocusChange = (startingElement) => {
494+
Blockly.getFocusManager().focusNode(startingElement);
495+
assert.strictEqual(
496+
Blockly.getFocusManager().getFocusedNode(),
497+
startingElement,
498+
);
499+
const event = createKeyDownEvent(Blockly.utils.KeyCodes.W);
500+
this.workspace.getInjectionDiv().dispatchEvent(event);
501+
assert.strictEqual(
502+
Blockly.getFocusManager().getFocusedNode(),
503+
this.workspace,
504+
);
505+
};
506+
});
507+
508+
test('Does not change focus when workspace is already focused', function () {
509+
this.testFocusChange(this.workspace);
510+
});
511+
512+
test('Focuses workspace when toolbox is focused', function () {
513+
this.testFocusChange(this.workspace.getToolbox());
514+
});
515+
516+
test('Focuses workspace when flyout is focused', function () {
517+
this.workspace.getToolbox().getFlyout().show();
518+
const flyoutWorkspace = this.workspace
519+
.getToolbox()
520+
.getFlyout()
521+
.getWorkspace();
522+
this.testFocusChange(flyoutWorkspace);
523+
});
524+
525+
test('Focuses workspace when a block is focused', function () {
526+
const block = this.workspace.newBlock('controls_if');
527+
this.testFocusChange(block);
528+
});
529+
530+
suite('With mutator', function () {
531+
test('Focuses root workspace when a mutator block is focused', async function () {
532+
const block = this.workspace.newBlock('controls_if');
533+
const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE);
534+
await icon.setBubbleVisible(true);
535+
const mutatorWorkspace = icon.getWorkspace();
536+
this.testFocusChange(mutatorWorkspace.getAllBlocks()[0]);
537+
});
538+
539+
test("Focuses workspace when a mutator's flyout is focused", async function () {
540+
const block = this.workspace.newBlock('controls_if');
541+
const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE);
542+
await icon.setBubbleVisible(true);
543+
const mutatorFlyoutWorkspace = icon
544+
.getWorkspace()
545+
.getFlyout()
546+
.getWorkspace();
547+
this.testFocusChange(mutatorFlyoutWorkspace);
548+
});
549+
});
550+
});
489551
});

0 commit comments

Comments
 (0)