Files
blockly/core/navigator.ts
Aaron Dodson acdad98653 refactor!: Use navigation rulesets instead of ASTNode to control keyboard navigation. (#8992)
* feat: Add interfaces for keyboard navigation.

* feat: Add the Navigator.

* feat: Make core types conform to INavigable.

* feat: Require FlyoutItems elements to be INavigable.

* feat: Add navigation policies for built-in types.

* refactor: Convert Marker and LineCursor to operate on INavigables instead of ASTNodes.

* chore: Delete dead code in ASTNode.

* fix: Fix the tests.

* chore: Assuage the linter.

* fix: Fix advanced build/tests.

* chore: Restore ASTNode tests.

* refactor: Move isNavigable() validation into Navigator.

* refactor: Exercise navigation instead of ASTNode.

* chore: Rename astnode_test.js to navigation_test.js.

* chore: Enable the navigation tests.

* fix: Fix bug when retrieving the first child of an empty workspace.
2025-05-07 08:47:52 -07:00

107 lines
3.5 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {INavigable} from './interfaces/i_navigable.js';
import type {INavigationPolicy} from './interfaces/i_navigation_policy.js';
type RuleMap<T> = Map<new (...args: any) => 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.
*/
private rules: RuleMap<any> = new Map();
/**
* Associates a navigation ruleset with its corresponding class.
*
* @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.
*/
set<T extends INavigable<T>>(
key: new (...args: any) => T,
policy: INavigationPolicy<T>,
) {
this.rules.set(key, policy);
}
/**
* Returns the navigation ruleset associated with the given object instance's
* class.
*
* @param key 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());
}
/**
* 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<T extends INavigable<T>>(current: T): INavigable<any> | 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.
if (!result.isNavigable()) return 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<T extends INavigable<T>>(current: T): INavigable<any> | null {
const result = this.get(current)?.getParent(current);
if (!result) return null;
if (!result.isNavigable()) 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<T extends INavigable<T>>(current: T): INavigable<any> | null {
const result = this.get(current)?.getNextSibling(current);
if (!result) return null;
if (!result.isNavigable()) 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<T extends INavigable<T>>(
current: T,
): INavigable<any> | null {
const result = this.get(current)?.getPreviousSibling(current);
if (!result) return null;
if (!result.isNavigable()) return this.getPreviousSibling(result);
return result;
}
}