refactor: Remove INavigable in favor of IFocusableNode. (#9037)

* refactor: Remove INavigable in favor of IFocusableNode.

* chore: Fix JSDoc.

* chore: Address review feedback.
This commit is contained in:
Aaron Dodson
2025-05-13 15:04:49 -07:00
committed by GitHub
parent e34a9690ed
commit ae22165cbe
29 changed files with 236 additions and 393 deletions

View File

@@ -47,7 +47,6 @@ import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import {IIcon} from './interfaces/i_icon.js';
import type {INavigable} from './interfaces/i_navigable.js';
import * as internalConstants from './internal_constants.js';
import {Msg} from './msg.js';
import * as renderManagement from './render_management.js';
@@ -77,8 +76,7 @@ export class BlockSvg
ICopyable<BlockCopyData>,
IDraggable,
IDeletable,
IFocusableNode,
INavigable<BlockSvg>
IFocusableNode
{
/**
* Constant for identifying rows that are to be rendered inline.
@@ -1796,16 +1794,4 @@ export class BlockSvg
canBeFocused(): boolean {
return true;
}
/**
* Returns this block's class.
*
* Used by keyboard navigation to look up the rules for navigating from this
* block.
*
* @returns This block's class.
*/
getClass() {
return BlockSvg;
}
}

View File

@@ -171,7 +171,7 @@ import {
import {IVariableMap} from './interfaces/i_variable_map.js';
import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
import * as internalConstants from './internal_constants.js';
import {CursorOptions, LineCursor} from './keyboard_nav/line_cursor.js';
import {LineCursor} from './keyboard_nav/line_cursor.js';
import {Marker} from './keyboard_nav/marker.js';
import type {LayerManager} from './layer_manager.js';
import * as layers from './layers.js';
@@ -429,7 +429,7 @@ Names.prototype.populateProcedures = function (
};
// clang-format on
export * from './interfaces/i_navigable.js';
export * from './flyout_navigator.js';
export * from './interfaces/i_navigation_policy.js';
export * from './keyboard_nav/block_navigation_policy.js';
export * from './keyboard_nav/connection_navigation_policy.js';
@@ -457,7 +457,6 @@ export {
ContextMenuItems,
ContextMenuRegistry,
Css,
CursorOptions,
DeleteArea,
DragTarget,
Events,

View File

@@ -26,7 +26,6 @@ import type {Input} from './inputs/input.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
import type {INavigable} from './interfaces/i_navigable.js';
import type {IRegistrable} from './interfaces/i_registrable.js';
import {ISerializable} from './interfaces/i_serializable.js';
import type {ConstantProvider} from './renderers/common/constants.js';
@@ -68,12 +67,7 @@ export type FieldValidator<T = any> = (newValue: T) => T | null | undefined;
* @typeParam T - The value stored on the field.
*/
export abstract class Field<T = any>
implements
IKeyboardAccessible,
IRegistrable,
ISerializable,
IFocusableNode,
INavigable<Field<T>>
implements IKeyboardAccessible, IRegistrable, ISerializable, IFocusableNode
{
/**
* To overwrite the default value which is set in **Field**, directly update
@@ -1410,16 +1404,6 @@ export abstract class Field<T = any>
`Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`,
);
}
/**
* Returns this field's class.
*
* Used by keyboard navigation to look up the rules for navigating from this
* field. Must be implemented by subclasses.
*
* @returns This field's class.
*/
abstract getClass(): new (...args: any) => Field<T>;
}
/**

View File

@@ -228,18 +228,6 @@ export class FieldCheckbox extends Field<CheckboxBool> {
// 'override' the static fromJson method.
return new this(options.checked, undefined, options);
}
/**
* Returns this field's class.
*
* Used by keyboard navigation to look up the rules for navigating from this
* field.
*
* @returns This field's class.
*/
getClass() {
return FieldCheckbox;
}
}
fieldRegistry.register('field_checkbox', FieldCheckbox);

View File

@@ -796,18 +796,6 @@ export class FieldDropdown extends Field<string> {
throw TypeError('Found invalid FieldDropdown options.');
}
}
/**
* Returns this field's class.
*
* Used by keyboard navigation to look up the rules for navigating from this
* field.
*
* @returns This field's class.
*/
getClass() {
return FieldDropdown;
}
}
/**

View File

@@ -272,18 +272,6 @@ export class FieldImage extends Field<string> {
options,
);
}
/**
* Returns this field's class.
*
* Used by keyboard navigation to look up the rules for navigating from this
* field.
*
* @returns This field's class.
*/
getClass() {
return FieldImage;
}
}
fieldRegistry.register('field_image', FieldImage);

View File

@@ -126,18 +126,6 @@ export class FieldLabel extends Field<string> {
// the static fromJson method.
return new this(text, undefined, options);
}
/**
* Returns this field's class.
*
* Used by keyboard navigation to look up the rules for navigating from this
* field.
*
* @returns This field's class.
*/
getClass() {
return FieldLabel;
}
}
fieldRegistry.register('field_label', FieldLabel);

View File

@@ -341,18 +341,6 @@ export class FieldNumber extends FieldInput<number> {
options,
);
}
/**
* Returns this field's class.
*
* Used by keyboard navigation to look up the rules for navigating from this
* field.
*
* @returns This field's class.
*/
getClass() {
return FieldNumber;
}
}
fieldRegistry.register('field_number', FieldNumber);

View File

@@ -89,18 +89,6 @@ export class FieldTextInput extends FieldInput<string> {
// override the static fromJson method.
return new this(text, undefined, options);
}
/**
* Returns this field's class.
*
* Used by keyboard navigation to look up the rules for navigating from this
* field.
*
* @returns This field's class.
*/
getClass() {
return FieldTextInput;
}
}
fieldRegistry.register('field_input', FieldTextInput);

View File

@@ -20,6 +20,7 @@ import {EventType} from './events/type.js';
import * as eventUtils from './events/utils.js';
import {FlyoutItem} from './flyout_item.js';
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
import {FlyoutNavigator} from './flyout_navigator.js';
import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js';
import {getFocusManager} from './focus_manager.js';
import {IAutoHideable} from './interfaces/i_autohideable.js';
@@ -243,6 +244,7 @@ export abstract class Flyout
this.workspace_.internalIsFlyout = true;
// Keep the workspace visibility consistent with the flyout's visibility.
this.workspace_.setVisible(this.visible);
this.workspace_.setNavigator(new FlyoutNavigator(this));
/**
* The unique id for this component that is used to register with the

View File

@@ -16,7 +16,6 @@ import * as Css from './css.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import type {INavigable} from './interfaces/i_navigable.js';
import type {IRenderedElement} from './interfaces/i_rendered_element.js';
import {idGenerator} from './utils.js';
import {Coordinate} from './utils/coordinate.js';
@@ -32,11 +31,7 @@ import type {WorkspaceSvg} from './workspace_svg.js';
* Class for a button or label in the flyout.
*/
export class FlyoutButton
implements
IBoundedElement,
IRenderedElement,
IFocusableNode,
INavigable<FlyoutButton>
implements IBoundedElement, IRenderedElement, IFocusableNode
{
/** The horizontal margin around the text in the button. */
static TEXT_MARGIN_X = 5;
@@ -412,18 +407,6 @@ export class FlyoutButton
canBeFocused(): boolean {
return true;
}
/**
* Returns this button's class.
*
* Used by keyboard navigation to look up the rules for navigating from this
* button.
*
* @returns This button's class.
*/
getClass() {
return FlyoutButton;
}
}
/** CSS for buttons and labels. See css.js for use. */

View File

@@ -1,5 +1,6 @@
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {INavigable} from './interfaces/i_navigable.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
/**
* Representation of an item displayed in a flyout.
*/
@@ -12,7 +13,7 @@ export class FlyoutItem {
* flyout inflater that created this object.
*/
constructor(
private element: IBoundedElement & INavigable<any>,
private element: IBoundedElement & IFocusableNode,
private type: string,
) {}

24
core/flyout_navigator.ts Normal file
View File

@@ -0,0 +1,24 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IFlyout} from './interfaces/i_flyout.js';
import {FlyoutButtonNavigationPolicy} from './keyboard_nav/flyout_button_navigation_policy.js';
import {FlyoutNavigationPolicy} from './keyboard_nav/flyout_navigation_policy.js';
import {FlyoutSeparatorNavigationPolicy} from './keyboard_nav/flyout_separator_navigation_policy.js';
import {Navigator} from './navigator.js';
export class FlyoutNavigator extends Navigator {
constructor(flyout: IFlyout) {
super();
this.rules.push(
new FlyoutButtonNavigationPolicy(),
new FlyoutSeparatorNavigationPolicy(),
);
this.rules = this.rules.map(
(rule) => new FlyoutNavigationPolicy(rule, flyout),
);
}
}

View File

@@ -7,15 +7,12 @@
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import type {INavigable} from './interfaces/i_navigable.js';
import {Rect} from './utils/rect.js';
/**
* Representation of a gap between elements in a flyout.
*/
export class FlyoutSeparator
implements IBoundedElement, INavigable<FlyoutSeparator>, IFocusableNode
{
export class FlyoutSeparator implements IBoundedElement, IFocusableNode {
private x = 0;
private y = 0;
@@ -66,18 +63,6 @@ export class FlyoutSeparator
return false;
}
/**
* Returns this separator's class.
*
* Used by keyboard navigation to look up the rules for navigating from this
* separator.
*
* @returns This separator's class.
*/
getClass() {
return FlyoutSeparator;
}
/** See IFocusableNode.getFocusableElement. */
getFocusableElement(): HTMLElement | SVGElement {
throw new Error('Cannot be focused');

View File

@@ -1,19 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IFocusableNode} from './i_focusable_node.js';
/**
* Represents a UI element which can be navigated to using the keyboard.
*/
export interface INavigable<T> extends IFocusableNode {
/**
* Returns the class of this instance.
*
* @returns This object's class.
*/
getClass(): new (...args: any) => T;
}

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {INavigable} from './i_navigable.js';
import type {IFocusableNode} from './i_focusable_node.js';
/**
* A set of rules that specify where keyboard navigation should proceed.
@@ -16,7 +16,7 @@ export interface INavigationPolicy<T> {
* @param current The element which the user is navigating into.
* @returns The current element's first child, or null if it has none.
*/
getFirstChild(current: T): INavigable<any> | null;
getFirstChild(current: T): IFocusableNode | null;
/**
* Returns the parent element of the given element, if any.
@@ -24,7 +24,7 @@ export interface INavigationPolicy<T> {
* @param current The element which the user is navigating out of.
* @returns The parent element of the current element, or null if it has none.
*/
getParent(current: T): INavigable<any> | null;
getParent(current: T): IFocusableNode | null;
/**
* Returns the peer element following the given element, if any.
@@ -33,7 +33,7 @@ export interface INavigationPolicy<T> {
* @returns The next peer element of the current element, or null if there is
* none.
*/
getNextSibling(current: T): INavigable<any> | null;
getNextSibling(current: T): IFocusableNode | null;
/**
* Returns the peer element preceding the given element, if any.
@@ -42,7 +42,7 @@ export interface INavigationPolicy<T> {
* @returns The previous peer element of the current element, or null if
* there is none.
*/
getPreviousSibling(current: T): INavigable<any> | null;
getPreviousSibling(current: T): IFocusableNode | null;
/**
* Returns whether or not the given instance should be reachable via keyboard
@@ -57,4 +57,13 @@ export interface INavigationPolicy<T> {
* @returns True if this element should be included in keyboard navigation.
*/
isNavigable(current: T): boolean;
/**
* Returns whether or not this navigation policy corresponds to the type of
* the given object.
*
* @param current An instance to check whether this policy applies to.
* @returns True if the given object is of a type handled by this policy.
*/
isApplicable(current: any): current is T;
}

View File

@@ -6,7 +6,7 @@
import {BlockSvg} from '../block_svg.js';
import type {Field} from '../field.js';
import type {INavigable} from '../interfaces/i_navigable.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
import {WorkspaceSvg} from '../workspace_svg.js';
@@ -20,7 +20,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* @param current The block to return the first child of.
* @returns The first field or input of the given block, if any.
*/
getFirstChild(current: BlockSvg): INavigable<unknown> | null {
getFirstChild(current: BlockSvg): IFocusableNode | null {
for (const input of current.inputList) {
for (const field of input.fieldRow) {
return field;
@@ -39,7 +39,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* @returns The top block of the given block's stack, or the connection to
* which it is attached.
*/
getParent(current: BlockSvg): INavigable<unknown> | null {
getParent(current: BlockSvg): IFocusableNode | null {
if (current.previousConnection?.targetBlock()) {
const surroundParent = current.getSurroundParent();
if (surroundParent) return surroundParent;
@@ -57,7 +57,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* @returns The first block of the next stack if the given block is a terminal
* block, or its next connection.
*/
getNextSibling(current: BlockSvg): INavigable<unknown> | null {
getNextSibling(current: BlockSvg): IFocusableNode | null {
if (current.nextConnection?.targetBlock()) {
return current.nextConnection?.targetBlock();
}
@@ -101,7 +101,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* @returns The block's previous/output connection, or the last
* connection/block of the previous block stack if it is a root block.
*/
getPreviousSibling(current: BlockSvg): INavigable<unknown> | null {
getPreviousSibling(current: BlockSvg): IFocusableNode | null {
if (current.previousConnection?.targetBlock()) {
return current.previousConnection?.targetBlock();
}
@@ -127,7 +127,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
}
const currentIndex = siblings.indexOf(current);
let result: INavigable<any> | null = null;
let result: IFocusableNode | null = null;
if (currentIndex >= 1) {
result = siblings[currentIndex - 1];
} else if (currentIndex === 0 && navigatingCrossStacks) {
@@ -152,4 +152,14 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
isNavigable(current: BlockSvg): boolean {
return current.canBeFocused();
}
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a BlockSvg.
*/
isApplicable(current: any): current is BlockSvg {
return current instanceof BlockSvg;
}
}

View File

@@ -6,9 +6,9 @@
import type {BlockSvg} from '../block_svg.js';
import {ConnectionType} from '../connection_type.js';
import type {INavigable} from '../interfaces/i_navigable.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
import type {RenderedConnection} from '../rendered_connection.js';
import {RenderedConnection} from '../rendered_connection.js';
/**
* Set of rules controlling keyboard navigation from a connection.
@@ -22,7 +22,7 @@ export class ConnectionNavigationPolicy
* @param current The connection to return the first child of.
* @returns The connection's first child element, or null if not none.
*/
getFirstChild(current: RenderedConnection): INavigable<unknown> | null {
getFirstChild(current: RenderedConnection): IFocusableNode | null {
if (current.getParentInput()) {
return current.targetConnection;
}
@@ -36,7 +36,7 @@ export class ConnectionNavigationPolicy
* @param current The connection to return the parent of.
* @returns The given connection's parent connection or block.
*/
getParent(current: RenderedConnection): INavigable<unknown> | null {
getParent(current: RenderedConnection): IFocusableNode | null {
if (current.type === ConnectionType.OUTPUT_VALUE) {
return current.targetConnection ?? current.getSourceBlock();
} else if (current.getParentInput()) {
@@ -56,7 +56,7 @@ export class ConnectionNavigationPolicy
* @param current The connection to navigate from.
* @returns The field, input connection or block following this connection.
*/
getNextSibling(current: RenderedConnection): INavigable<unknown> | null {
getNextSibling(current: RenderedConnection): IFocusableNode | null {
if (current.getParentInput()) {
const parentInput = current.getParentInput();
const block = parentInput?.getSourceBlock();
@@ -101,7 +101,7 @@ export class ConnectionNavigationPolicy
* @param current The connection to navigate from.
* @returns The field, input connection or block preceding this connection.
*/
getPreviousSibling(current: RenderedConnection): INavigable<unknown> | null {
getPreviousSibling(current: RenderedConnection): IFocusableNode | null {
if (current.getParentInput()) {
const parentInput = current.getParentInput();
const block = parentInput?.getSourceBlock();
@@ -176,4 +176,14 @@ export class ConnectionNavigationPolicy
isNavigable(current: RenderedConnection): boolean {
return current.canBeFocused();
}
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a RenderedConnection.
*/
isApplicable(current: any): current is RenderedConnection {
return current instanceof RenderedConnection;
}
}

View File

@@ -5,8 +5,8 @@
*/
import type {BlockSvg} from '../block_svg.js';
import type {Field} from '../field.js';
import type {INavigable} from '../interfaces/i_navigable.js';
import {Field} from '../field.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
/**
@@ -19,7 +19,7 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
* @param _current The field to navigate from.
* @returns Null.
*/
getFirstChild(_current: Field<any>): INavigable<unknown> | null {
getFirstChild(_current: Field<any>): IFocusableNode | null {
return null;
}
@@ -29,7 +29,7 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
* @param current The field to navigate from.
* @returns The given field's parent block.
*/
getParent(current: Field<any>): INavigable<unknown> | null {
getParent(current: Field<any>): IFocusableNode | null {
return current.getSourceBlock() as BlockSvg;
}
@@ -39,7 +39,7 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
* @param current The field to navigate from.
* @returns The next field or input in the given field's block.
*/
getNextSibling(current: Field<any>): INavigable<unknown> | null {
getNextSibling(current: Field<any>): IFocusableNode | null {
const input = current.getParentInput();
const block = current.getSourceBlock();
if (!block) return null;
@@ -64,7 +64,7 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
* @param current The field to navigate from.
* @returns The preceding field or input in the given field's block.
*/
getPreviousSibling(current: Field<any>): INavigable<unknown> | null {
getPreviousSibling(current: Field<any>): IFocusableNode | null {
const parentInput = current.getParentInput();
const block = current.getSourceBlock();
if (!block) return null;
@@ -106,4 +106,14 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
current.getParentInput().isVisible()
);
}
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a Field.
*/
isApplicable(current: any): current is Field {
return current instanceof Field;
}
}

View File

@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {FlyoutButton} from '../flyout_button.js';
import type {INavigable} from '../interfaces/i_navigable.js';
import {FlyoutButton} from '../flyout_button.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
/**
@@ -20,7 +20,7 @@ export class FlyoutButtonNavigationPolicy
* @param _current The FlyoutButton instance to navigate from.
* @returns Null.
*/
getFirstChild(_current: FlyoutButton): INavigable<unknown> | null {
getFirstChild(_current: FlyoutButton): IFocusableNode | null {
return null;
}
@@ -30,7 +30,7 @@ export class FlyoutButtonNavigationPolicy
* @param current The FlyoutButton instance to navigate from.
* @returns The given flyout button's parent workspace.
*/
getParent(current: FlyoutButton): INavigable<unknown> | null {
getParent(current: FlyoutButton): IFocusableNode | null {
return current.getWorkspace();
}
@@ -40,7 +40,7 @@ export class FlyoutButtonNavigationPolicy
* @param _current The FlyoutButton instance to navigate from.
* @returns Null.
*/
getNextSibling(_current: FlyoutButton): INavigable<unknown> | null {
getNextSibling(_current: FlyoutButton): IFocusableNode | null {
return null;
}
@@ -50,7 +50,7 @@ export class FlyoutButtonNavigationPolicy
* @param _current The FlyoutButton instance to navigate from.
* @returns Null.
*/
getPreviousSibling(_current: FlyoutButton): INavigable<unknown> | null {
getPreviousSibling(_current: FlyoutButton): IFocusableNode | null {
return null;
}
@@ -63,4 +63,14 @@ export class FlyoutButtonNavigationPolicy
isNavigable(current: FlyoutButton): boolean {
return current.canBeFocused();
}
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a FlyoutButton.
*/
isApplicable(current: any): current is FlyoutButton {
return current instanceof FlyoutButton;
}
}

View File

@@ -5,7 +5,7 @@
*/
import type {IFlyout} from '../interfaces/i_flyout.js';
import type {INavigable} from '../interfaces/i_navigable.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
/**
@@ -29,7 +29,7 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
* @param _current The flyout item to navigate from.
* @returns Null to prevent navigating into flyout items.
*/
getFirstChild(_current: T): INavigable<unknown> | null {
getFirstChild(_current: T): IFocusableNode | null {
return null;
}
@@ -39,7 +39,7 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
* @param current The flyout item to navigate from.
* @returns The parent of the given flyout item.
*/
getParent(current: T): INavigable<unknown> | null {
getParent(current: T): IFocusableNode | null {
return this.policy.getParent(current);
}
@@ -49,7 +49,7 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
* @param current The flyout item to navigate from.
* @returns The flyout item following the given one.
*/
getNextSibling(current: T): INavigable<unknown> | null {
getNextSibling(current: T): IFocusableNode | null {
const flyoutContents = this.flyout.getContents();
if (!flyoutContents) return null;
@@ -72,7 +72,7 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
* @param current The flyout item to navigate from.
* @returns The flyout item preceding the given one.
*/
getPreviousSibling(current: T): INavigable<unknown> | null {
getPreviousSibling(current: T): IFocusableNode | null {
const flyoutContents = this.flyout.getContents();
if (!flyoutContents) return null;
@@ -98,4 +98,14 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
isNavigable(current: T): boolean {
return this.policy.isNavigable(current);
}
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a BlockSvg.
*/
isApplicable(current: any): current is T {
return this.policy.isApplicable(current);
}
}

View File

@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {FlyoutSeparator} from '../flyout_separator.js';
import type {INavigable} from '../interfaces/i_navigable.js';
import {FlyoutSeparator} from '../flyout_separator.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
/**
@@ -15,19 +15,19 @@ import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
export class FlyoutSeparatorNavigationPolicy
implements INavigationPolicy<FlyoutSeparator>
{
getFirstChild(_current: FlyoutSeparator): INavigable<unknown> | null {
getFirstChild(_current: FlyoutSeparator): IFocusableNode | null {
return null;
}
getParent(_current: FlyoutSeparator): INavigable<unknown> | null {
getParent(_current: FlyoutSeparator): IFocusableNode | null {
return null;
}
getNextSibling(_current: FlyoutSeparator): INavigable<unknown> | null {
getNextSibling(_current: FlyoutSeparator): IFocusableNode | null {
return null;
}
getPreviousSibling(_current: FlyoutSeparator): INavigable<unknown> | null {
getPreviousSibling(_current: FlyoutSeparator): IFocusableNode | null {
return null;
}
@@ -40,4 +40,14 @@ export class FlyoutSeparatorNavigationPolicy
isNavigable(_current: FlyoutSeparator): boolean {
return false;
}
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a FlyoutSeparator.
*/
isApplicable(current: any): current is FlyoutSeparator {
return current instanceof FlyoutSeparator;
}
}

View File

@@ -14,43 +14,12 @@
*/
import {BlockSvg} from '../block_svg.js';
import {FieldCheckbox} from '../field_checkbox.js';
import {FieldDropdown} from '../field_dropdown.js';
import {FieldImage} from '../field_image.js';
import {FieldLabel} from '../field_label.js';
import {FieldNumber} from '../field_number.js';
import {FieldTextInput} from '../field_textinput.js';
import {FlyoutButton} from '../flyout_button.js';
import {FlyoutSeparator} from '../flyout_separator.js';
import {getFocusManager} from '../focus_manager.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import {isFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigable} from '../interfaces/i_navigable.js';
import * as registry from '../registry.js';
import {RenderedConnection} from '../rendered_connection.js';
import {Renderer} from '../renderers/zelos/renderer.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import {BlockNavigationPolicy} from './block_navigation_policy.js';
import {ConnectionNavigationPolicy} from './connection_navigation_policy.js';
import {FieldNavigationPolicy} from './field_navigation_policy.js';
import {FlyoutButtonNavigationPolicy} from './flyout_button_navigation_policy.js';
import {FlyoutNavigationPolicy} from './flyout_navigation_policy.js';
import {FlyoutSeparatorNavigationPolicy} from './flyout_separator_navigation_policy.js';
import {Marker} from './marker.js';
import {WorkspaceNavigationPolicy} from './workspace_navigation_policy.js';
/** Options object for LineCursor instances. */
export interface CursorOptions {
/**
* Can the cursor visit all stack connections (next/previous), or
* (if false) only unconnected next connections?
*/
stackConnections: boolean;
}
/** Default options for LineCursor instances. */
const defaultOptions: CursorOptions = {
stackConnections: true,
};
/**
* Class for a line cursor.
@@ -58,76 +27,14 @@ const defaultOptions: CursorOptions = {
export class LineCursor extends Marker {
override type = 'cursor';
/** Options for this line cursor. */
private readonly options: CursorOptions;
/** Locations to try moving the cursor to after a deletion. */
private potentialNodes: INavigable<any>[] | null = null;
/** Whether the renderer is zelos-style. */
private isZelos = false;
private potentialNodes: IFocusableNode[] | null = null;
/**
* @param workspace The workspace this cursor belongs to.
* @param options Cursor options.
*/
constructor(
protected readonly workspace: WorkspaceSvg,
options?: Partial<CursorOptions>,
) {
constructor(protected readonly workspace: WorkspaceSvg) {
super();
// Regularise options and apply defaults.
this.options = {...defaultOptions, ...options};
this.isZelos = workspace.getRenderer() instanceof Renderer;
this.registerNavigationPolicies();
}
/**
* Registers default navigation policies for Blockly's built-in types with
* this cursor's workspace.
*/
protected registerNavigationPolicies() {
const navigator = this.workspace.getNavigator();
const blockPolicy = new BlockNavigationPolicy();
if (this.workspace.isFlyout) {
const flyout = this.workspace.targetWorkspace?.getFlyout();
if (flyout) {
navigator.set(
BlockSvg,
new FlyoutNavigationPolicy(blockPolicy, flyout),
);
const buttonPolicy = new FlyoutButtonNavigationPolicy();
navigator.set(
FlyoutButton,
new FlyoutNavigationPolicy(buttonPolicy, flyout),
);
navigator.set(
FlyoutSeparator,
new FlyoutNavigationPolicy(
new FlyoutSeparatorNavigationPolicy(),
flyout,
),
);
}
} else {
navigator.set(BlockSvg, blockPolicy);
}
navigator.set(RenderedConnection, new ConnectionNavigationPolicy());
navigator.set(WorkspaceSvg, new WorkspaceNavigationPolicy());
const fieldPolicy = new FieldNavigationPolicy();
navigator.set(FieldCheckbox, fieldPolicy);
navigator.set(FieldDropdown, fieldPolicy);
navigator.set(FieldImage, fieldPolicy);
navigator.set(FieldLabel, fieldPolicy);
navigator.set(FieldNumber, fieldPolicy);
navigator.set(FieldTextInput, fieldPolicy);
}
/**
@@ -137,14 +44,14 @@ export class LineCursor extends Marker {
* @returns The next node, or null if the current node is
* not set or there is no next value.
*/
next(): INavigable<any> | null {
next(): IFocusableNode | null {
const curNode = this.getCurNode();
if (!curNode) {
return null;
}
const newNode = this.getNextNode(
curNode,
(candidate: INavigable<any> | null) => {
(candidate: IFocusableNode | null) => {
return (
candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock()
@@ -166,7 +73,7 @@ export class LineCursor extends Marker {
* @returns The next node, or null if the current node is
* not set or there is no next value.
*/
in(): INavigable<any> | null {
in(): IFocusableNode | null {
const curNode = this.getCurNode();
if (!curNode) {
return null;
@@ -186,14 +93,14 @@ export class LineCursor extends Marker {
* @returns The previous node, or null if the current node
* is not set or there is no previous value.
*/
prev(): INavigable<any> | null {
prev(): IFocusableNode | null {
const curNode = this.getCurNode();
if (!curNode) {
return null;
}
const newNode = this.getPreviousNode(
curNode,
(candidate: INavigable<any> | null) => {
(candidate: IFocusableNode | null) => {
return (
candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock()
@@ -215,7 +122,7 @@ export class LineCursor extends Marker {
* @returns The previous node, or null if the current node
* is not set or there is no previous value.
*/
out(): INavigable<any> | null {
out(): IFocusableNode | null {
const curNode = this.getCurNode();
if (!curNode) {
return null;
@@ -241,7 +148,7 @@ export class LineCursor extends Marker {
const inNode = this.getNextNode(curNode, () => true, true);
const nextNode = this.getNextNode(
curNode,
(candidate: INavigable<any> | null) => {
(candidate: IFocusableNode | null) => {
return (
candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock()
@@ -265,10 +172,10 @@ export class LineCursor extends Marker {
* @returns The next node in the traversal.
*/
private getNextNodeImpl(
node: INavigable<any> | null,
isValid: (p1: INavigable<any> | null) => boolean,
visitedNodes: Set<INavigable<any>> = new Set<INavigable<any>>(),
): INavigable<any> | null {
node: IFocusableNode | null,
isValid: (p1: IFocusableNode | null) => boolean,
visitedNodes: Set<IFocusableNode> = new Set<IFocusableNode>(),
): IFocusableNode | null {
if (!node || visitedNodes.has(node)) return null;
let newNode =
this.workspace.getNavigator().getFirstChild(node) ||
@@ -301,10 +208,10 @@ export class LineCursor extends Marker {
* @returns The next node in the traversal.
*/
getNextNode(
node: INavigable<any> | null,
isValid: (p1: INavigable<any> | null) => boolean,
node: IFocusableNode | null,
isValid: (p1: IFocusableNode | null) => boolean,
loop: boolean,
): INavigable<any> | null {
): IFocusableNode | null {
if (!node || (!loop && this.getLastNode() === node)) return null;
return this.getNextNodeImpl(node, isValid);
@@ -323,10 +230,10 @@ export class LineCursor extends Marker {
* exists.
*/
private getPreviousNodeImpl(
node: INavigable<any> | null,
isValid: (p1: INavigable<any> | null) => boolean,
visitedNodes: Set<INavigable<any>> = new Set<INavigable<any>>(),
): INavigable<any> | null {
node: IFocusableNode | null,
isValid: (p1: IFocusableNode | null) => boolean,
visitedNodes: Set<IFocusableNode> = new Set<IFocusableNode>(),
): IFocusableNode | null {
if (!node || visitedNodes.has(node)) return null;
const newNode =
@@ -355,10 +262,10 @@ export class LineCursor extends Marker {
* exists.
*/
getPreviousNode(
node: INavigable<any> | null,
isValid: (p1: INavigable<any> | null) => boolean,
node: IFocusableNode | null,
isValid: (p1: IFocusableNode | null) => boolean,
loop: boolean,
): INavigable<any> | null {
): IFocusableNode | null {
if (!node || (!loop && this.getFirstNode() === node)) return null;
return this.getPreviousNodeImpl(node, isValid);
@@ -371,15 +278,15 @@ export class LineCursor extends Marker {
* @returns The right most child of the given node, or the node if no child
* exists.
*/
getRightMostChild(
node: INavigable<any> | null,
stopIfFound: INavigable<any>,
): INavigable<any> | null {
private getRightMostChild(
node: IFocusableNode | null,
stopIfFound: IFocusableNode,
): IFocusableNode | null {
if (!node) return node;
let newNode = this.workspace.getNavigator().getFirstChild(node);
if (!newNode || newNode === stopIfFound) return node;
for (
let nextNode: INavigable<any> | null = newNode;
let nextNode: IFocusableNode | null = newNode;
nextNode;
nextNode = this.workspace.getNavigator().getNextSibling(newNode)
) {
@@ -414,7 +321,7 @@ export class LineCursor extends Marker {
preDelete(deletedBlock: BlockSvg) {
const curNode = this.getCurNode();
const nodes: INavigable<any>[] = curNode ? [curNode] : [];
const nodes: IFocusableNode[] = curNode ? [curNode] : [];
// The connection to which the deleted block is attached.
const parentConnection =
deletedBlock.previousConnection?.targetConnection ??
@@ -466,7 +373,7 @@ export class LineCursor extends Marker {
*
* @returns The current field, connection, or block the cursor is on.
*/
override getCurNode(): INavigable<any> | null {
override getCurNode(): IFocusableNode | null {
this.updateCurNodeFromFocus();
return super.getCurNode();
}
@@ -479,7 +386,7 @@ export class LineCursor extends Marker {
*
* @param newNode The new location of the cursor.
*/
override setCurNode(newNode: INavigable<any> | null) {
override setCurNode(newNode: IFocusableNode | null) {
super.setCurNode(newNode);
if (isFocusableNode(newNode)) {
@@ -513,7 +420,7 @@ export class LineCursor extends Marker {
*
* @returns The first navigable node on the workspace, or null.
*/
getFirstNode(): INavigable<any> | null {
getFirstNode(): IFocusableNode | null {
return this.workspace.getNavigator().getFirstChild(this.workspace);
}
@@ -522,7 +429,7 @@ export class LineCursor extends Marker {
*
* @returns The last navigable node on the workspace, or null.
*/
getLastNode(): INavigable<any> | null {
getLastNode(): IFocusableNode | null {
const first = this.getFirstNode();
return this.getPreviousNode(first, () => true, true);
}

View File

@@ -14,7 +14,7 @@
import {BlockSvg} from '../block_svg.js';
import {Field} from '../field.js';
import type {INavigable} from '../interfaces/i_navigable.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import {RenderedConnection} from '../rendered_connection.js';
/**
@@ -26,7 +26,7 @@ export class Marker {
colour: string | null = null;
/** The current location of the marker. */
protected curNode: INavigable<any> | null = null;
protected curNode: IFocusableNode | null = null;
/** The type of the marker. */
type = 'marker';
@@ -36,7 +36,7 @@ export class Marker {
*
* @returns The current field, connection, or block the marker is on.
*/
getCurNode(): INavigable<any> | null {
getCurNode(): IFocusableNode | null {
return this.curNode;
}
@@ -45,7 +45,7 @@ export class Marker {
*
* @param newNode The new location of the marker, or null to remove it.
*/
setCurNode(newNode: INavigable<any> | null) {
setCurNode(newNode: IFocusableNode | null) {
this.curNode = newNode;
}
@@ -59,7 +59,7 @@ export class Marker {
*
* @returns The parent block of the node if any, otherwise null.
*/
getSourceBlockFromNode(node: INavigable<any> | null): BlockSvg | null {
getSourceBlockFromNode(node: IFocusableNode | null): BlockSvg | null {
if (node instanceof BlockSvg) {
return node;
} else if (node instanceof Field) {

View File

@@ -4,9 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {INavigable} from '../interfaces/i_navigable.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
import type {WorkspaceSvg} from '../workspace_svg.js';
import {WorkspaceSvg} from '../workspace_svg.js';
/**
* Set of rules controlling keyboard navigation from a workspace.
@@ -20,7 +20,7 @@ export class WorkspaceNavigationPolicy
* @param current The workspace to return the first child of.
* @returns The top block of the first block stack, if any.
*/
getFirstChild(current: WorkspaceSvg): INavigable<unknown> | null {
getFirstChild(current: WorkspaceSvg): IFocusableNode | null {
const blocks = current.getTopBlocks(true);
return blocks.length ? blocks[0] : null;
}
@@ -31,7 +31,7 @@ export class WorkspaceNavigationPolicy
* @param _current The workspace to return the parent of.
* @returns Null.
*/
getParent(_current: WorkspaceSvg): INavigable<unknown> | null {
getParent(_current: WorkspaceSvg): IFocusableNode | null {
return null;
}
@@ -41,7 +41,7 @@ export class WorkspaceNavigationPolicy
* @param _current The workspace to return the next sibling of.
* @returns Null.
*/
getNextSibling(_current: WorkspaceSvg): INavigable<unknown> | null {
getNextSibling(_current: WorkspaceSvg): IFocusableNode | null {
return null;
}
@@ -51,7 +51,7 @@ export class WorkspaceNavigationPolicy
* @param _current The workspace to return the previous sibling of.
* @returns Null.
*/
getPreviousSibling(_current: WorkspaceSvg): INavigable<unknown> | null {
getPreviousSibling(_current: WorkspaceSvg): IFocusableNode | null {
return null;
}
@@ -64,4 +64,14 @@ export class WorkspaceNavigationPolicy
isNavigable(current: WorkspaceSvg): boolean {
return current.canBeFocused();
}
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a WorkspaceSvg.
*/
isApplicable(current: any): current is WorkspaceSvg {
return current instanceof WorkspaceSvg;
}
}

View File

@@ -4,10 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {INavigable} from './interfaces/i_navigable.js';
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 {ConnectionNavigationPolicy} from './keyboard_nav/connection_navigation_policy.js';
import {FieldNavigationPolicy} from './keyboard_nav/field_navigation_policy.js';
import {WorkspaceNavigationPolicy} from './keyboard_nav/workspace_navigation_policy.js';
type RuleMap<T> = Map<new (...args: any) => T, INavigationPolicy<T>>;
type RuleList<T> = INavigationPolicy<T>[];
/**
* Class responsible for determining where focus should move in response to
@@ -18,35 +22,35 @@ export class Navigator {
* Map from classes to a corresponding ruleset to handle navigation from
* instances of that class.
*/
private rules: RuleMap<any> = new Map();
protected rules: RuleList<any> = [
new BlockNavigationPolicy(),
new FieldNavigationPolicy(),
new ConnectionNavigationPolicy(),
new WorkspaceNavigationPolicy(),
];
/**
* Associates a navigation ruleset with its corresponding class.
* Adds a navigation ruleset to this Navigator.
*
* @param key The class whose object instances should have their navigation
* controlled by the associated policy.
* @param policy A ruleset that determines where focus should move starting
* from an instance of the given class.
* from an instance of its managed class.
*/
set<T extends INavigable<T>>(
key: new (...args: any) => T,
policy: INavigationPolicy<T>,
) {
this.rules.set(key, policy);
addNavigationPolicy(policy: INavigationPolicy<any>) {
this.rules.push(policy);
}
/**
* Returns the navigation ruleset associated with the given object instance's
* class.
*
* @param key An object to retrieve a navigation ruleset for.
* @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<T extends INavigable<T>>(
key: T,
): INavigationPolicy<T> | undefined {
return this.rules.get(key.getClass());
private get(
current: IFocusableNode,
): INavigationPolicy<typeof current> | undefined {
return this.rules.find((rule) => rule.isApplicable(current));
}
/**
@@ -55,7 +59,7 @@ export class Navigator {
* @param current The object to retrieve the first child of.
* @returns The first child node of the given object, if any.
*/
getFirstChild<T extends INavigable<T>>(current: T): INavigable<any> | null {
getFirstChild(current: IFocusableNode): IFocusableNode | null {
const result = this.get(current)?.getFirstChild(current);
if (!result) return null;
// If the child isn't navigable, don't traverse into it; check its peers.
@@ -71,7 +75,7 @@ export class Navigator {
* @param current The object to retrieve the parent of.
* @returns The parent node of the given object, if any.
*/
getParent<T extends INavigable<T>>(current: T): INavigable<any> | null {
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);
@@ -84,7 +88,7 @@ export class Navigator {
* @param current The object to retrieve the next sibling node of.
* @returns The next sibling node of the given object, if any.
*/
getNextSibling<T extends INavigable<T>>(current: T): INavigable<any> | null {
getNextSibling(current: IFocusableNode): IFocusableNode | null {
const result = this.get(current)?.getNextSibling(current);
if (!result) return null;
if (!this.get(result)?.isNavigable(result)) {
@@ -99,9 +103,7 @@ export class Navigator {
* @param current The object to retrieve the previous sibling node of.
* @returns The previous sibling node of the given object, if any.
*/
getPreviousSibling<T extends INavigable<T>>(
current: T,
): INavigable<any> | null {
getPreviousSibling(current: IFocusableNode): IFocusableNode | null {
const result = this.get(current)?.getPreviousSibling(current);
if (!result) return null;
if (!this.get(result)?.isNavigable(result)) {

View File

@@ -24,7 +24,6 @@ import {IContextMenu} from './interfaces/i_contextmenu.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import {hasBubble} from './interfaces/i_has_bubble.js';
import type {INavigable} from './interfaces/i_navigable.js';
import * as internalConstants from './internal_constants.js';
import {Coordinate} from './utils/coordinate.js';
import * as svgMath from './utils/svg_math.js';
@@ -38,7 +37,7 @@ const BUMP_RANDOMNESS = 10;
*/
export class RenderedConnection
extends Connection
implements IContextMenu, IFocusableNode, INavigable<RenderedConnection>
implements IContextMenu, IFocusableNode
{
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
sourceBlock_!: BlockSvg;
@@ -664,15 +663,6 @@ export class RenderedConnection
| unknown
| null as SVGElement | null;
}
/**
* Returns this connection's class for keyboard navigation.
*
* @returns RenderedConnection.
*/
getClass() {
return RenderedConnection;
}
}
export namespace RenderedConnection {

View File

@@ -53,7 +53,6 @@ import {
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import {hasBubble} from './interfaces/i_has_bubble.js';
import type {IMetricsManager} from './interfaces/i_metrics_manager.js';
import type {INavigable} from './interfaces/i_navigable.js';
import type {IToolbox} from './interfaces/i_toolbox.js';
import type {LineCursor} from './keyboard_nav/line_cursor.js';
import type {Marker} from './keyboard_nav/marker.js';
@@ -100,11 +99,7 @@ const ZOOM_TO_FIT_MARGIN = 20;
*/
export class WorkspaceSvg
extends Workspace
implements
IContextMenu,
IFocusableNode,
IFocusableTree,
INavigable<WorkspaceSvg>
implements IContextMenu, IFocusableNode, IFocusableTree
{
/**
* A wrapper function called when a resize event occurs.
@@ -2823,15 +2818,6 @@ export class WorkspaceSvg
}
}
/**
* Returns the class of this workspace.
*
* @returns WorkspaceSvg.
*/
getClass() {
return WorkspaceSvg;
}
/**
* Returns an object responsible for coordinating movement of focus between
* items on this workspace in response to keyboard navigation commands.
@@ -2841,6 +2827,16 @@ export class WorkspaceSvg
getNavigator(): Navigator {
return this.navigator;
}
/**
* Sets the Navigator instance used by this workspace.
*
* @param newNavigator A Navigator object to coordinate movement between
* elements on the workspace.
*/
setNavigator(newNavigator: Navigator) {
this.navigator = newNavigator;
}
}
/**

View File

@@ -61,10 +61,6 @@ class FieldMitosis extends Field<CellGroup> {
});
this.value_ = {cells};
}
getClass() {
return FieldMitosis;
}
}
fieldRegistry.register('field_mitosis', FieldMitosis);