Files
blockly/core/navigator.ts
Aaron Dodson e5804e7095 feat: Add support for keyboard navigation in/to workspace comments. (#9182)
* feat: Enhance the Rect API.

* feat: Add support for sorting IBoundedElements in general.

* fix: Improve typings of getTopElement/Comment methods.

* feat: Add classes to represent comment icons.

* refactor: Use comment icons in comment view.

* feat: Update navigation policies to support workspace comments.

* feat: Make the navigator and workspace handle workspace comments.

* feat: Visit workspace comments when navigating with the up/down arrows.

* chore: Make the linter happy.

* chore: Rename comment icons to bar buttons.

* refactor: Rename CommentIcons to CommentBarButtons.

* chore: Improve docstrings.

* chore: Clarify unit type.

* refactor: Remove workspace argument from `navigateStacks()`.

* fix: Fix errant find and replace in CSS.

* fix: Fix issue that could cause delete button to become misaligned.
2025-07-01 15:13:13 -07:00

120 lines
4.2 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {INavigationPolicy} from './interfaces/i_navigation_policy.js';
import {BlockNavigationPolicy} from './keyboard_nav/block_navigation_policy.js';
import {CommentBarButtonNavigationPolicy} from './keyboard_nav/comment_bar_button_navigation_policy.js';
import {ConnectionNavigationPolicy} from './keyboard_nav/connection_navigation_policy.js';
import {FieldNavigationPolicy} from './keyboard_nav/field_navigation_policy.js';
import {IconNavigationPolicy} from './keyboard_nav/icon_navigation_policy.js';
import {WorkspaceCommentNavigationPolicy} from './keyboard_nav/workspace_comment_navigation_policy.js';
import {WorkspaceNavigationPolicy} from './keyboard_nav/workspace_navigation_policy.js';
type RuleList<T> = INavigationPolicy<T>[];
/**
* Class responsible for determining where focus should move in response to
* keyboard navigation commands.
*/
export class Navigator {
/**
* Map from classes to a corresponding ruleset to handle navigation from
* instances of that class.
*/
protected rules: RuleList<any> = [
new BlockNavigationPolicy(),
new FieldNavigationPolicy(),
new ConnectionNavigationPolicy(),
new WorkspaceNavigationPolicy(),
new IconNavigationPolicy(),
new WorkspaceCommentNavigationPolicy(),
new CommentBarButtonNavigationPolicy(),
];
/**
* Adds a navigation ruleset to this Navigator.
*
* @param policy A ruleset that determines where focus should move starting
* from an instance of its managed class.
*/
addNavigationPolicy(policy: INavigationPolicy<any>) {
this.rules.push(policy);
}
/**
* Returns the navigation ruleset associated with the given object instance's
* class.
*
* @param current An object to retrieve a navigation ruleset for.
* @returns The navigation ruleset of objects of the given object's class, or
* undefined if no ruleset has been registered for the object's class.
*/
private get(
current: IFocusableNode,
): INavigationPolicy<typeof current> | undefined {
return this.rules.find((rule) => rule.isApplicable(current));
}
/**
* Returns the first child of the given object instance, if any.
*
* @param current The object to retrieve the first child of.
* @returns The first child node of the given object, if any.
*/
getFirstChild(current: IFocusableNode): IFocusableNode | null {
const result = this.get(current)?.getFirstChild(current);
if (!result) return null;
if (!this.get(result)?.isNavigable(result)) {
return this.getFirstChild(result) || this.getNextSibling(result);
}
return result;
}
/**
* Returns the parent of the given object instance, if any.
*
* @param current The object to retrieve the parent of.
* @returns The parent node of the given object, if any.
*/
getParent(current: IFocusableNode): IFocusableNode | null {
const result = this.get(current)?.getParent(current);
if (!result) return null;
if (!this.get(result)?.isNavigable(result)) return this.getParent(result);
return result;
}
/**
* Returns the next sibling of the given object instance, if any.
*
* @param current The object to retrieve the next sibling node of.
* @returns The next sibling node of the given object, if any.
*/
getNextSibling(current: IFocusableNode): IFocusableNode | null {
const result = this.get(current)?.getNextSibling(current);
if (!result) return null;
if (!this.get(result)?.isNavigable(result)) {
return this.getNextSibling(result);
}
return result;
}
/**
* Returns the previous sibling of the given object instance, if any.
*
* @param current The object to retrieve the previous sibling node of.
* @returns The previous sibling node of the given object, if any.
*/
getPreviousSibling(current: IFocusableNode): IFocusableNode | null {
const result = this.get(current)?.getPreviousSibling(current);
if (!result) return null;
if (!this.get(result)?.isNavigable(result)) {
return this.getPreviousSibling(result);
}
return result;
}
}