From b5b916c68372c053b68f359c48fb27f5155e38f7 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 5 Mar 2026 15:46:50 -0800 Subject: [PATCH 1/3] feat: Add keyboard shortcut to focus the workspace --- packages/blockly/core/shortcut_items.ts | 60 +++++++++++++++++++++---- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/packages/blockly/core/shortcut_items.ts b/packages/blockly/core/shortcut_items.ts index 85252154991..ebaeb2b432e 100644 --- a/packages/blockly/core/shortcut_items.ts +++ b/packages/blockly/core/shortcut_items.ts @@ -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: () => true, + 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(); From f43c417658d36dd24aff689393209ec67f84278a Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 6 Mar 2026 09:48:46 -0800 Subject: [PATCH 2/3] test: Added tests for keyboard shortcut to focus workspace --- .../tests/mocha/shortcut_items_test.js | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/blockly/tests/mocha/shortcut_items_test.js b/packages/blockly/tests/mocha/shortcut_items_test.js index 99c7bd4d0f9..608a651712b 100644 --- a/packages/blockly/tests/mocha/shortcut_items_test.js +++ b/packages/blockly/tests/mocha/shortcut_items_test.js @@ -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); + }); + }); + }); }); From b6103cb83663220742ef150831808557752307ec Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 9 Mar 2026 13:26:09 -0700 Subject: [PATCH 3/3] fix: Disable the focus workspace shortcut while dragging --- packages/blockly/core/shortcut_items.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blockly/core/shortcut_items.ts b/packages/blockly/core/shortcut_items.ts index ebaeb2b432e..e2ec187470e 100644 --- a/packages/blockly/core/shortcut_items.ts +++ b/packages/blockly/core/shortcut_items.ts @@ -547,7 +547,7 @@ export function registerFocusWorkspace() { const contextMenuShortcut: KeyboardShortcut = { name: names.FOCUS_WORKSPACE, - preconditionFn: () => true, + preconditionFn: (workspace) => !workspace.isDragging(), callback: (workspace) => { keyboardNavigationController.setIsActive(true); getFocusManager().focusNode(resolveWorkspace(workspace));