Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/blockly/core/blockly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ Names.prototype.populateProcedures = function (

export * from './interfaces/i_json_block_definition.js';
export * from './interfaces/i_navigation_policy.js';
export * from './interfaces/i_navigator.js';
export * from './keyboard_nav/navigation_policies/block_navigation_policy.js';
export * from './keyboard_nav/navigation_policies/bubble_navigation_policy.js';
export * from './keyboard_nav/navigation_policies/comment_bar_button_navigation_policy.js';
Expand All @@ -442,6 +443,7 @@ export * from './keyboard_nav/navigation_policies/workspace_comment_navigation_p
export * from './keyboard_nav/navigation_policies/workspace_navigation_policy.js';
export * from './keyboard_nav/navigators/flyout_navigator.js';
export * from './keyboard_nav/navigators/navigator.js';
export * from './keyboard_nav/navigators/workspace_control_navigator.js';
export * from './toast.js';

// Re-export submodules that no longer declareLegacyNamespace.
Expand Down
12 changes: 6 additions & 6 deletions packages/blockly/core/interfaces/i_focusable_tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import type {Navigator} from '../keyboard_nav/navigators/navigator';
import type {IFocusableNode} from './i_focusable_node.js';
import type {INavigator} from './i_navigator.js';

/**
* Represents a tree of focusable elements with its own active/passive focus
Expand Down Expand Up @@ -125,12 +125,12 @@ export interface IFocusableTree {
onTreeBlur(nextTree: IFocusableTree | null): void;

/**
* Returns a Navigator instance to be used to determine the navigation order
* between IFocusableNodes contained within this IFocusableTree. Generally
* this can just be an instance of Navigator, but trees may choose to return a
* subclass to customize navigation behavior within their context.
* Returns a navigator used to determine the navigation order between
* IFocusableNodes contained within this IFocusableTree. Generally this can
* just be an instance of Navigator, but trees may choose to return a
* different INavigator to customize navigation behavior within their context.
*/
getNavigator(): Navigator;
getNavigator(): INavigator;
}

/**
Expand Down
74 changes: 74 additions & 0 deletions packages/blockly/core/interfaces/i_navigator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @license
* Copyright 2026 Raspberry Pi Foundation
* SPDX-License-Identifier: Apache-2.0
*/

import type {BlockSvg} from '../block_svg.js';
import type {IFocusableNode} from './i_focusable_node.js';
import type {INavigationPolicy} from './i_navigation_policy.js';

/**
* Coordinates keyboard navigation between focusable nodes.
*
* Implemented by {@link Navigator} and specialized navigators such as
* {@link WorkspaceControlNavigator}.
*/
export interface INavigator {
/** Adds a navigation ruleset to this navigator. */
addNavigationPolicy(policy: INavigationPolicy<any>): void;

/** Returns the first child of the given node, if any. */
getFirstChild(current: IFocusableNode): IFocusableNode | null;

/** Returns the parent of the given node, if any. */
getParent(current: IFocusableNode): IFocusableNode | null;

/** Returns the next sibling of the given node, if any. */
getNextSibling(current: IFocusableNode): IFocusableNode | null;

/** Returns the previous sibling of the given node, if any. */
getPreviousSibling(current: IFocusableNode): IFocusableNode | null;

/** Returns the previous node in navigation order, if any. */
getPreviousNode(node?: IFocusableNode | null): IFocusableNode | null;

/** Returns the node reached by navigating out, if any. */
getOutNode(node?: IFocusableNode | null): IFocusableNode | null;

/** Returns the next node in navigation order, if any. */
getNextNode(node?: IFocusableNode | null): IFocusableNode | null;

/** Returns the node reached by navigating in, if any. */
getInNode(node?: IFocusableNode | null): IFocusableNode | null;

/** Sets whether navigation loops when reaching the end. */
setNavigationLoops(loops: boolean): void;

/** Returns whether navigation loops when reaching the end. */
getNavigationLoops(): boolean;

/** Returns the first navigable node in the focused tree, if any. */
getFirstNode(): IFocusableNode | null;

/** Returns the last navigable node in the focused tree, if any. */
getLastNode(): IFocusableNode | null;

/**
* Returns the navigable top-level items of a tree, in navigation order.
*
* @param root The root node of the tree to list.
* @returns The navigable top-level items under the root.
*/
getNavigableItems(root?: IFocusableNode | null): IFocusableNode[];

/**
* Moves between top-level stacks by the given delta.
*
* @returns The stack root to focus, or null if none.
*/
navigateStacks(current: IFocusableNode, delta: number): IFocusableNode | null;

/** Returns the source block for a focusable node, if any. */
getSourceBlockFromNode(node: IFocusableNode | null): BlockSvg | null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {getFocusManager} from '../../focus_manager.js';
import {Icon} from '../../icons/icon.js';
import type {IFocusableNode} from '../../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../../interfaces/i_navigation_policy.js';
import type {INavigator} from '../../interfaces/i_navigator.js';
import {RenderedConnection} from '../../rendered_connection.js';
import {BlockNavigationPolicy} from '../navigation_policies/block_navigation_policy.js';
import {BubbleNavigationPolicy} from '../navigation_policies/bubble_navigation_policy.js';
Expand Down Expand Up @@ -38,7 +39,7 @@ export enum NavigationDirection {
* Class responsible for determining where focus should move in response to
* keyboard navigation commands.
*/
export class Navigator {
export class Navigator implements INavigator {
/**
* Map from classes to a corresponding ruleset to handle navigation from
* instances of that class.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* @license
* Copyright 2026 Raspberry Pi Foundation
* SPDX-License-Identifier: Apache-2.0
*/

import type {BlockSvg} from '../../block_svg.js';
import type {IFocusableNode} from '../../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../../interfaces/i_navigation_policy.js';
import type {INavigator} from '../../interfaces/i_navigator.js';

/**
* No-op navigator for single-node workspace controls (trashcan, zoom).
*/
export class WorkspaceControlNavigator implements INavigator {
addNavigationPolicy(_policy: INavigationPolicy<any>) {}

getFirstChild() {
return null;
}

getParent() {
return null;
}

getNextSibling() {
return null;
}

getPreviousSibling() {
return null;
}

getPreviousNode() {
return null;
}

getOutNode() {
return null;
}

getNextNode() {
return null;
}

getInNode() {
return null;
}

setNavigationLoops(_loops: boolean) {}

getNavigationLoops() {
return false;
}

getFirstNode() {
return null;
}

getLastNode() {
return null;
}

getNavigableItems(_root?: IFocusableNode | null): IFocusableNode[] {
return [];
}

navigateStacks() {
return null;
}

getSourceBlockFromNode(_node: IFocusableNode | null): BlockSvg | null {
return null;
}
}
2 changes: 1 addition & 1 deletion packages/blockly/core/sprites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

/**
* Contains the path to a single png tat holds the images for the trashcan
* Contains the path to a single svg that holds the images for the trashcan
* as well as the zoom controls.
*/
export const SPRITE = {
Expand Down
78 changes: 64 additions & 14 deletions packages/blockly/core/trashcan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import type {IComponent} from './interfaces/i_component';
import type {IDraggable} from './interfaces/i_draggable.js';
import type {IFlyout} from './interfaces/i_flyout.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import type {IPositionable} from './interfaces/i_positionable.js';
import {KeyboardMover} from './keyboard_nav/keyboard_mover.js';
import {WorkspaceControlNavigator} from './keyboard_nav/navigators/workspace_control_navigator.js';
import {keyboardNavigationController} from './keyboard_navigation_controller.js';
import type {UiMetrics} from './metrics_manager.js';
import {Msg} from './msg.js';
Expand All @@ -50,8 +52,14 @@ import type {WorkspaceSvg} from './workspace_svg.js';
*/
export class Trashcan
extends DeleteArea
implements IAutoHideable, IPositionable, IFocusableNode, IComponent
implements
IAutoHideable,
IPositionable,
IFocusableNode,
IFocusableTree,
IComponent
{
private readonly navigator = new WorkspaceControlNavigator();
/**
* The id for this component that is used to register with the
* ComponentManager.
Expand Down Expand Up @@ -137,17 +145,25 @@ export class Trashcan
*/
createDom(): SVGElement {
/* Here's the markup that will be generated:
<g class="blocklyTrash">
<clippath id="blocklyTrashBodyClipPath837493">
<rect width="47" height="45" y="15"></rect>
</clippath>
<image width="64" height="92" y="-32" xlink:href="media/sprites.png"
clip-path="url(#blocklyTrashBodyClipPath837493)"></image>
<clippath id="blocklyTrashLidClipPath837493">
<rect width="47" height="15"></rect>
</clippath>
<image width="84" height="92" y="-32" xlink:href="media/sprites.png"
clip-path="url(#blocklyTrashLidClipPath837493)"></image>
<g role="button" class="blocklyTrash" tabindex="0" id="blockly-0"
aria-label="Trash, currently empty" aria-disabled="true">
<rect width="55" height="68" x="-4" y="-4" rx="2" ry="2"
fill="none" class="blocklyFocusRing"></rect>
<clipPath id="blocklyTrashBodyClipPath837493">
<rect width="47" height="44" y="16"></rect>
</clipPath>
<image width="96" x="0" height="124" y="-32"
clip-path="url(#blocklyTrashBodyClipPath837493)"
xlink:href="../media/sprites.svg"></image>
<clipPath id="blocklyTrashLidClipPath837493">
<rect width="47" height="16"></rect>
</clipPath>
<g role="none" class="blocklyTrashLid">
<svg role="none" viewBox="0 32 47 16" width="47" height="16">
<image width="96" height="124"
href="../media/sprites.svg"></image>
</svg>
</g>
</g>
*/
this.svgGroup = dom.createSvgElement(Svg.G, {
Expand Down Expand Up @@ -256,6 +272,7 @@ export class Trashcan
this.blockMouseDownWhenOpenable,
);
browserEvents.bind(this.svgGroup, 'pointerup', this, this.click);
getFocusManager().registerTree(this, false);
return this.svgGroup;
}

Expand All @@ -275,7 +292,6 @@ export class Trashcan
ComponentManager.Capability.DELETE_AREA,
ComponentManager.Capability.DRAG_TARGET,
ComponentManager.Capability.POSITIONABLE,
ComponentManager.Capability.FOCUSABLE,
],
});
this.initialized = true;
Expand All @@ -287,6 +303,9 @@ export class Trashcan
* Unlink from all DOM elements to prevent memory leaks.
*/
dispose() {
if (getFocusManager().isRegistered(this)) {
getFocusManager().unregisterTree(this);
}
this.workspace.getComponentManager().removeComponent('trashcan');
if (this.svgGroup) {
dom.removeNode(this.svgGroup);
Expand Down Expand Up @@ -662,7 +681,7 @@ export class Trashcan
}

getFocusableTree() {
return this.workspace;
return this;
}

onNodeFocus() {}
Expand All @@ -672,6 +691,37 @@ export class Trashcan
return !!this.svgGroup;
}

/** See IFocusableTree.getRootFocusableNode. */
getRootFocusableNode(): IFocusableNode {
return this;
}

/** See IFocusableTree.getRestoredFocusableNode. */
getRestoredFocusableNode(): IFocusableNode | null {
return this;
}

/** See IFocusableTree.getNestedTrees. */
getNestedTrees(): IFocusableTree[] {
return [];
}

/** See IFocusableTree.lookUpFocusableNode. */
lookUpFocusableNode(): IFocusableNode | null {
return null;
}

/** See IFocusableTree.onTreeFocus. */
onTreeFocus(): void {}

/** See IFocusableTree.onTreeBlur. */
onTreeBlur(): void {}

/** See IFocusableTree.getNavigator. */
getNavigator(): WorkspaceControlNavigator {
return this.navigator;
}

performAction() {
this.click();
}
Expand Down
Loading
Loading