diff --git a/plugins/keyboard-navigation/package-lock.json b/plugins/keyboard-navigation/package-lock.json index ae8b4f1808..2ec95b8060 100644 --- a/plugins/keyboard-navigation/package-lock.json +++ b/plugins/keyboard-navigation/package-lock.json @@ -749,9 +749,9 @@ } }, "node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -3216,9 +3216,9 @@ } }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" diff --git a/plugins/keyboard-navigation/src/navigation.js b/plugins/keyboard-navigation/src/navigation.js index 4651c11b15..e53cb598b1 100644 --- a/plugins/keyboard-navigation/src/navigation.js +++ b/plugins/keyboard-navigation/src/navigation.js @@ -486,7 +486,7 @@ export class Navigation { /** * Sets the navigation state to flyout and moves the cursor to the first - * block in the flyout. + * block or button in the flyout. * @param {!Blockly.WorkspaceSvg} workspace The workspace the flyout is on. * @package */ @@ -500,9 +500,14 @@ export class Navigation { } if (flyout && flyout.getWorkspace()) { - const topBlocks = flyout.getWorkspace().getTopBlocks(true); - if (topBlocks.length > 0) { - const astNode = Blockly.ASTNode.createStackNode(topBlocks[0]); + const flyoutContents = flyout.getContents(); + const firstFlyoutItem = flyoutContents[0]; + if (firstFlyoutItem instanceof Blockly.FlyoutButton) { + const astNode = Blockly.ASTNode.createButtonNode(firstFlyoutItem); + this.getFlyoutCursor(workspace).setCurNode(astNode); + } + if (firstFlyoutItem instanceof Blockly.BlockSvg) { + const astNode = Blockly.ASTNode.createStackNode(firstFlyoutItem); this.getFlyoutCursor(workspace).setCurNode(astNode); } } @@ -1248,6 +1253,26 @@ export class Navigation { return isHandled; } + /** + * Triggers a flyout button's callback. + * @param {!Blockly.WorkspaceSvg} workspace The main workspace. The workspace + * containing a flyout with a button. + * @package + */ + triggerButtonCallback(workspace) { + const button = /** @type {!Blockly.FlyoutButton} */ ( + this.getFlyoutCursor(workspace).getCurNode().getLocation() + ); + const buttonCallback = workspace.flyoutButtonCallbacks.get( + button.callbackKey, + ); + if (typeof buttonCallback === 'function') { + buttonCallback(button); + } else { + throw new Error('No callback function found for flyout button.'); + } + } + /** * Removes the change listeners on all registered workspaces. * @package diff --git a/plugins/keyboard-navigation/src/navigation_controller.js b/plugins/keyboard-navigation/src/navigation_controller.js index a070950646..76298f328b 100644 --- a/plugins/keyboard-navigation/src/navigation_controller.js +++ b/plugins/keyboard-navigation/src/navigation_controller.js @@ -504,12 +504,31 @@ export class NavigationController { ); }, callback: (workspace) => { + let flyoutCursor; + let curNode; + let nodeType; + switch (this.navigation.getState(workspace)) { case Constants.STATE.WORKSPACE: this.navigation.handleEnterForWS(workspace); return true; case Constants.STATE.FLYOUT: - this.navigation.insertFromFlyout(workspace); + flyoutCursor = this.navigation.getFlyoutCursor(workspace); + if (!flyoutCursor) { + return false; + } + curNode = flyoutCursor.getCurNode(); + nodeType = curNode.getType(); + + switch (nodeType) { + case Blockly.ASTNode.types.STACK: + this.navigation.insertFromFlyout(workspace); + break; + case Blockly.ASTNode.types.BUTTON: + this.navigation.triggerButtonCallback(workspace); + break; + } + return true; default: return false; diff --git a/plugins/keyboard-navigation/test/index.js b/plugins/keyboard-navigation/test/index.js index e5449280d4..919ab31037 100644 --- a/plugins/keyboard-navigation/test/index.js +++ b/plugins/keyboard-navigation/test/index.js @@ -8,8 +8,9 @@ * @fileoverview Plugin test. */ -import {createPlayground, toolboxCategories} from '@blockly/dev-tools'; +import {createPlayground} from '@blockly/dev-tools'; import * as Blockly from 'blockly'; +import {toolbox} from './toolbox'; import {LineCursor, NavigationController} from '../src'; @@ -31,7 +32,7 @@ document.addEventListener('DOMContentLoaded', function () { controller = new NavigationController(); controller.init(); const defaultOptions = { - toolbox: toolboxCategories, + toolbox: toolbox, }; createPlayground( document.getElementById('root'), diff --git a/plugins/keyboard-navigation/test/toolbox.js b/plugins/keyboard-navigation/test/toolbox.js new file mode 100644 index 0000000000..95dd4ff7bd --- /dev/null +++ b/plugins/keyboard-navigation/test/toolbox.js @@ -0,0 +1,218 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview A custom toolbox for the plugin test. + */ + +export const toolbox = { + kind: 'categoryToolbox', + contents: [ + { + kind: 'category', + name: 'Logic', + categorystyle: 'logic_category', + contents: [ + { + type: 'controls_if', + kind: 'block', + }, + { + type: 'logic_compare', + kind: 'block', + fields: { + OP: 'EQ', + }, + }, + { + type: 'logic_operation', + kind: 'block', + fields: { + OP: 'AND', + }, + }, + ], + }, + { + kind: 'category', + name: 'Loops', + categorystyle: 'loop_category', + contents: [ + { + type: 'controls_repeat_ext', + kind: 'block', + inputs: { + TIMES: { + shadow: { + type: 'math_number', + fields: { + NUM: 10, + }, + }, + }, + }, + }, + { + type: 'controls_repeat', + kind: 'block', + enabled: false, + fields: { + TIMES: 10, + }, + }, + { + type: 'controls_whileUntil', + kind: 'block', + fields: { + MODE: 'WHILE', + }, + }, + { + type: 'controls_for', + kind: 'block', + fields: { + VAR: { + name: 'i', + }, + }, + inputs: { + FROM: { + shadow: { + type: 'math_number', + fields: { + NUM: 1, + }, + }, + }, + TO: { + shadow: { + type: 'math_number', + fields: { + NUM: 10, + }, + }, + }, + BY: { + shadow: { + type: 'math_number', + fields: { + NUM: 1, + }, + }, + }, + }, + }, + { + type: 'controls_forEach', + kind: 'block', + fields: { + VAR: { + name: 'j', + }, + }, + }, + { + type: 'controls_flow_statements', + kind: 'block', + enabled: false, + fields: { + FLOW: 'BREAK', + }, + }, + ], + }, + { + kind: 'sep', + }, + { + kind: 'category', + name: 'Variables', + custom: 'VARIABLE', + categorystyle: 'variable_category', + }, + { + kind: 'category', + name: 'Buttons and Blocks', + categorystyle: 'loop_category', + contents: [ + { + type: 'controls_repeat', + kind: 'block', + fields: { + TIMES: 10, + }, + }, + { + kind: 'BUTTON', + text: 'Randomize Button Style', + callbackkey: 'setRandomStyle', + }, + { + kind: 'BUTTON', + text: 'Randomize Button Style', + callbackkey: 'setRandomStyle', + }, + { + type: 'controls_repeat', + kind: 'block', + fields: { + TIMES: 10, + }, + }, + { + kind: 'BUTTON', + text: 'Randomize Button Style', + callbackkey: 'setRandomStyle', + }, + ], + }, + { + kind: 'sep', + }, + { + kind: 'category', + name: 'Nested Categories', + contents: [ + { + kind: 'category', + name: 'sub-category 1', + contents: [ + { + kind: 'BUTTON', + text: 'Randomize Button Style', + callbackkey: 'setRandomStyle', + }, + { + type: 'logic_boolean', + kind: 'block', + fields: { + BOOL: 'TRUE', + }, + }, + ], + }, + { + kind: 'category', + name: 'sub-category 2', + contents: [ + { + type: 'logic_boolean', + kind: 'block', + fields: { + BOOL: 'FALSE', + }, + }, + { + kind: 'BUTTON', + text: 'Randomize Button Style', + callbackkey: 'setRandomStyle', + }, + ], + }, + ], + }, + ], +};