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
This commit is contained in:
Aaron Dodson
2026-03-09 13:28:34 -07:00
committed by GitHub
parent 09d19d8f7b
commit 25968ffbdf
2 changed files with 115 additions and 9 deletions
+52 -8
View File
@@ -38,6 +38,14 @@ export enum names {
UNDO = 'undo',
REDO = 'redo',
MENU = 'menu',
FOCUS_WORKSPACE = 'focus_workspace',
START_MOVE = 'start_move',
FINISH_MOVE = 'finish_move',
ABORT_MOVE = 'abort_move',
MOVE_UP = 'move_up',
MOVE_DOWN = 'move_down',
MOVE_LEFT = 'move_left',
MOVE_RIGHT = 'move_right',
}
/**
@@ -391,7 +399,7 @@ export function registerMovementShortcuts() {
const shortcuts: ShortcutRegistry.KeyboardShortcut[] = [
{
name: 'start_move',
name: names.START_MOVE,
preconditionFn: (workspace) => {
const startDraggable = getCurrentDraggable(workspace);
return !!startDraggable && KeyboardMover.mover.canMove(startDraggable);
@@ -412,7 +420,7 @@ export function registerMovementShortcuts() {
keyCodes: [KeyCodes.M],
},
{
name: 'finish_move',
name: names.FINISH_MOVE,
preconditionFn: () => KeyboardMover.mover.isMoving(),
callback: (_workspace, e) =>
KeyboardMover.mover.finishMove(e as KeyboardEvent),
@@ -420,7 +428,7 @@ export function registerMovementShortcuts() {
allowCollision: true,
},
{
name: 'abort_move',
name: names.ABORT_MOVE,
preconditionFn: () => KeyboardMover.mover.isMoving(),
callback: (_workspace, e) =>
KeyboardMover.mover.abortMove(e as KeyboardEvent),
@@ -428,7 +436,7 @@ export function registerMovementShortcuts() {
allowCollision: true,
},
{
name: 'move_left',
name: names.MOVE_LEFT,
preconditionFn: () => KeyboardMover.mover.isMoving(),
callback: (_workspace, e) => {
e.preventDefault();
@@ -443,7 +451,7 @@ export function registerMovementShortcuts() {
allowCollision: true,
},
{
name: 'move_right',
name: names.MOVE_RIGHT,
preconditionFn: () => KeyboardMover.mover.isMoving(),
callback: (_workspace, e) => {
e.preventDefault();
@@ -458,7 +466,7 @@ export function registerMovementShortcuts() {
allowCollision: true,
},
{
name: 'move_up',
name: names.MOVE_UP,
preconditionFn: () => KeyboardMover.mover.isMoving(),
callback: (_workspace, e) => {
e.preventDefault();
@@ -473,7 +481,7 @@ export function registerMovementShortcuts() {
allowCollision: true,
},
{
name: 'move_down',
name: names.MOVE_DOWN,
preconditionFn: () => KeyboardMover.mover.isMoving(),
callback: (_workspace, e) => {
e.preventDefault();
@@ -508,7 +516,7 @@ export function registerShowContextMenu() {
preconditionFn: (workspace) => {
return !workspace.isDragging();
},
callback: (workspace, e) => {
callback: (_workspace, e) => {
const target = getFocusManager().getFocusedNode();
if (hasContextMenu(target)) {
target.showContextMenu(e);
@@ -523,6 +531,33 @@ export function registerShowContextMenu() {
ShortcutRegistry.registry.register(contextMenuShortcut);
}
/**
* Registers keyboard shortcut to focus the workspace.
*/
export function registerFocusWorkspace() {
const resolveWorkspace = (workspace: WorkspaceSvg) => {
if (workspace.isFlyout) {
const target = workspace.targetWorkspace;
if (target) {
return resolveWorkspace(target);
}
}
return workspace.getRootWorkspace() ?? workspace;
};
const contextMenuShortcut: KeyboardShortcut = {
name: names.FOCUS_WORKSPACE,
preconditionFn: (workspace) => !workspace.isDragging(),
callback: (workspace) => {
keyboardNavigationController.setIsActive(true);
getFocusManager().focusNode(resolveWorkspace(workspace));
return true;
},
keyCodes: [KeyCodes.W],
};
ShortcutRegistry.registry.register(contextMenuShortcut);
}
/**
* Registers all default keyboard shortcut item. This should be called once per
* instance of KeyboardShortcutRegistry.
@@ -537,8 +572,17 @@ export function registerDefaultShortcuts() {
registerPaste();
registerUndo();
registerRedo();
}
/**
* Registers an extended set of keyboard shortcuts used to support deep keyboard
* navigation of Blockly.
*/
export function registerKeyboardNavigationShortcuts() {
registerShowContextMenu();
registerMovementShortcuts();
registerFocusWorkspace();
}
registerDefaultShortcuts();
registerKeyboardNavigationShortcuts();
@@ -16,7 +16,8 @@ import {createKeyDownEvent} from './test_helpers/user_input.js';
suite('Keyboard Shortcut Items', function () {
setup(function () {
sharedTestSetup.call(this);
this.workspace = Blockly.inject('blocklyDiv', {});
const toolbox = document.getElementById('toolbox-categories');
this.workspace = Blockly.inject('blocklyDiv', {toolbox});
this.injectionDiv = this.workspace.getInjectionDiv();
Blockly.ContextMenuRegistry.registry.reset();
Blockly.ContextMenuItems.registerDefaultOptions();
@@ -486,4 +487,65 @@ suite('Keyboard Shortcut Items', function () {
);
});
});
suite('Focus Workspace (W)', function () {
setup(function () {
this.testFocusChange = (startingElement) => {
Blockly.getFocusManager().focusNode(startingElement);
assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
startingElement,
);
const event = createKeyDownEvent(Blockly.utils.KeyCodes.W);
this.workspace.getInjectionDiv().dispatchEvent(event);
assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
this.workspace,
);
};
});
test('Does not change focus when workspace is already focused', function () {
this.testFocusChange(this.workspace);
});
test('Focuses workspace when toolbox is focused', function () {
this.testFocusChange(this.workspace.getToolbox());
});
test('Focuses workspace when flyout is focused', function () {
this.workspace.getToolbox().getFlyout().show();
const flyoutWorkspace = this.workspace
.getToolbox()
.getFlyout()
.getWorkspace();
this.testFocusChange(flyoutWorkspace);
});
test('Focuses workspace when a block is focused', function () {
const block = this.workspace.newBlock('controls_if');
this.testFocusChange(block);
});
suite('With mutator', function () {
test('Focuses root workspace when a mutator block is focused', async function () {
const block = this.workspace.newBlock('controls_if');
const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE);
await icon.setBubbleVisible(true);
const mutatorWorkspace = icon.getWorkspace();
this.testFocusChange(mutatorWorkspace.getAllBlocks()[0]);
});
test("Focuses workspace when a mutator's flyout is focused", async function () {
const block = this.workspace.newBlock('controls_if');
const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE);
await icon.setBubbleVisible(true);
const mutatorFlyoutWorkspace = icon
.getWorkspace()
.getFlyout()
.getWorkspace();
this.testFocusChange(mutatorFlyoutWorkspace);
});
});
});
});