mirror of
https://github.com/google/blockly.git
synced 2026-06-17 08:35:12 +02:00
feat: Lay the foundation for keyboard navigation into flyout items.
This commit is contained in:
+2
-1
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user