diff --git a/plugins/continuous-toolbox/src/ContinuousFlyout.ts b/plugins/continuous-toolbox/src/ContinuousFlyout.ts index ac638c428..27f1c63ac 100644 --- a/plugins/continuous-toolbox/src/ContinuousFlyout.ts +++ b/plugins/continuous-toolbox/src/ContinuousFlyout.ts @@ -192,6 +192,26 @@ export class ContinuousFlyout extends Blockly.VerticalFlyout { this.scrollTo(position); } + /** + * Returns the header item in the flyout corresponding to the given + * toolbox category, if any. + * + * @param category The toolbox category to retrieve header item for. + * @returns The given category's header item, or undefined if not found. + */ + headerForCategory( + category: Blockly.ISelectableToolboxItem, + ): Blockly.IFocusableNode | undefined { + return this.getContents() + .find((item) => { + return ( + this.toolboxItemIsLabel(item) && + item.getElement().getButtonText() === category.getName() + ); + }) + ?.getElement(); + } + /** * Step the scrolling animation by scrolling a fraction of the way to * a scroll target, and request the next frame if necessary. diff --git a/plugins/continuous-toolbox/src/ContinuousToolbox.ts b/plugins/continuous-toolbox/src/ContinuousToolbox.ts index 67267b8dd..1bf4dc759 100644 --- a/plugins/continuous-toolbox/src/ContinuousToolbox.ts +++ b/plugins/continuous-toolbox/src/ContinuousToolbox.ts @@ -10,6 +10,7 @@ import * as Blockly from 'blockly/core'; import {ContinuousFlyout} from './ContinuousFlyout'; +import {ContinuousToolboxNavigator} from './ContinuousToolboxNavigator'; /** * Class for continuous toolbox. @@ -21,6 +22,12 @@ export class ContinuousToolbox extends Blockly.Toolbox { */ private refreshDebouncer?: ReturnType; + /** + * Navigator object responsible for handling keyboard navigation within this + * toolbox. + */ + private continuousToolboxNavigator = new ContinuousToolboxNavigator(this); + /** * Initializes the continuous toolbox. */ @@ -192,4 +199,12 @@ export class ContinuousToolbox extends Blockly.Toolbox { } return super.getClientRect(); } + + /** + * Returns the Navigator object responsible for handling keyboard navigation + * inside this toolbox. + */ + override getNavigator(): Blockly.ToolboxNavigator { + return this.continuousToolboxNavigator; + } } diff --git a/plugins/continuous-toolbox/src/ContinuousToolboxNavigator.ts b/plugins/continuous-toolbox/src/ContinuousToolboxNavigator.ts new file mode 100644 index 000000000..717c7aa8c --- /dev/null +++ b/plugins/continuous-toolbox/src/ContinuousToolboxNavigator.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from 'blockly/core'; +import {ContinuousToolbox} from './ContinuousToolbox'; +import {ContinuousCategory} from './ContinuousCategory'; + +/** + * A Navigator that handles keyboard navigation within a continuous toolbox. + */ +export class ContinuousToolboxNavigator extends Blockly.ToolboxNavigator { + constructor(protected toolbox: ContinuousToolbox) { + super(toolbox); + } + + /** + * Returns the next node when navigating "in", in this case the first flyout + * item in the toolbox's currently selected category. + * + * @param node The node to navigate relative to. + * @returns The node "in" relative to the given node. + */ + override getInNode( + node = Blockly.getFocusManager().getFocusedNode(), + ): Blockly.IFocusableNode | null { + if (!(node instanceof ContinuousCategory)) return null; + return this.toolbox.getFlyout().headerForCategory(node) ?? null; + } +}