feat: Lay the foundation for keyboard navigation into flyout items.

This commit is contained in:
Aaron Dodson
2025-08-18 14:00:33 -07:00
parent ef235cff76
commit 29dabb331c
4 changed files with 83 additions and 25 deletions
+2 -1
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 {LineCursor} from './keyboard_nav/line_cursor.js';
import {LineCursor, NavigationDirection} from './keyboard_nav/line_cursor.js';
import {Marker} from './keyboard_nav/marker.js';
import {
KeyboardNavigationController,
@@ -466,6 +466,7 @@ export {
Events,
Extensions,
LineCursor,
NavigationDirection,
Procedures,
ShortcutItems,
Themes,
@@ -29,8 +29,8 @@ 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): IFocusableNode | null {
return null;
getFirstChild(current: T): IFocusableNode | null {
return this.policy.getFirstChild(current);
}
/**
@@ -57,10 +57,10 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
(flyoutItem) => flyoutItem.getElement() === current,
);
if (index === -1) return null;
if (index === -1) return this.policy.getNextSibling(current);
index++;
if (index >= flyoutContents.length) {
index = 0;
return null;
}
return flyoutContents[index].getElement();
@@ -80,10 +80,10 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
(flyoutItem) => flyoutItem.getElement() === current,
);
if (index === -1) return null;
if (index === -1) return this.policy.getPreviousSibling(current);
index--;
if (index < 0) {
index = flyoutContents.length - 1;
return null;
}
return flyoutContents[index].getElement();
+65 -18
View File
@@ -24,6 +24,16 @@ import {Rect} from '../utils/rect.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import {Marker} from './marker.js';
/**
* Representation of the direction of travel within a navigation context.
*/
export enum NavigationDirection {
NEXT,
PREVIOUS,
IN,
OUT,
}
/**
* Class for a line cursor.
*/
@@ -54,14 +64,8 @@ export class LineCursor extends Marker {
}
const newNode = this.getNextNode(
curNode,
(candidate: IFocusableNode | null) => {
return (
(candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock()) ||
candidate instanceof RenderedWorkspaceComment
);
},
true,
this.getValidationFunction(NavigationDirection.NEXT),
this.shouldLoop(NavigationDirection.NEXT),
);
if (newNode) {
@@ -83,7 +87,11 @@ export class LineCursor extends Marker {
return null;
}
const newNode = this.getNextNode(curNode, () => true, true);
const newNode = this.getNextNode(
curNode,
this.getValidationFunction(NavigationDirection.IN),
this.shouldLoop(NavigationDirection.IN),
);
if (newNode) {
this.setCurNode(newNode);
@@ -104,14 +112,8 @@ export class LineCursor extends Marker {
}
const newNode = this.getPreviousNode(
curNode,
(candidate: IFocusableNode | null) => {
return (
(candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock()) ||
candidate instanceof RenderedWorkspaceComment
);
},
true,
this.getValidationFunction(NavigationDirection.PREVIOUS),
this.shouldLoop(NavigationDirection.PREVIOUS),
);
if (newNode) {
@@ -133,7 +135,11 @@ export class LineCursor extends Marker {
return null;
}
const newNode = this.getPreviousNode(curNode, () => true, true);
const newNode = this.getPreviousNode(
curNode,
this.getValidationFunction(NavigationDirection.OUT),
this.shouldLoop(NavigationDirection.OUT),
);
if (newNode) {
this.setCurNode(newNode);
@@ -141,6 +147,47 @@ export class LineCursor extends Marker {
return newNode;
}
/**
* Returns a function that will be used to determine whether a candidate for
* navigation is valid.
*
* @param direction The direction in which the user is navigating.
* @returns A function that takes a proposed navigation candidate and returns
* true if navigation should be allowed to proceed to it, or false to find
* a different candidate.
*/
getValidationFunction(
direction: NavigationDirection,
): (node: IFocusableNode | null) => boolean {
switch (direction) {
case NavigationDirection.IN:
case NavigationDirection.OUT:
return () => true;
case NavigationDirection.NEXT:
case NavigationDirection.PREVIOUS:
return (candidate: IFocusableNode | null) => {
return (
(candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock()) ||
(!!candidate &&
this.workspace.getNavigator().getParent(candidate) ===
this.workspace)
);
};
}
}
/**
* Returns whether or not navigation should loop around when reaching the end/
* beginning of navigable items.
*
* @param direction The direction in which the user is navigating.
* @returns True if navigation should be allowed to loop, otherwise false.
*/
shouldLoop(direction: NavigationDirection): boolean {
return true;
}
/**
* Returns true iff the node to which we would navigate if in() were
* called is the same as the node to which we would navigate if next() were
@@ -5,6 +5,7 @@
*/
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import {isFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
import {WorkspaceSvg} from '../workspace_svg.js';
@@ -21,6 +22,15 @@ export class WorkspaceNavigationPolicy
* @returns The top block of the first block stack, if any.
*/
getFirstChild(current: WorkspaceSvg): IFocusableNode | null {
if (current.isFlyout) {
for (const item of current.targetWorkspace?.getFlyout()?.getContents() ??
[]) {
const element = item.getElement();
if (isFocusableNode(element) && element.canBeFocused()) {
return element;
}
}
}
const blocks = current.getTopBlocks(true);
return blocks.length ? blocks[0] : null;
}