Skip to content

Commit 05af6b6

Browse files
authored
feat!: Add support for keyboard navigation (#9634)
* refactor!: Merge `Marker`, `LineCursor` and `Navigator` * refactor!: Use the Navigator to traverse the toolbox and flyout. * feat: Register keyboard shortcuts for navigation * test: Fix and add tests * chore: Export `ToolboxNavigator` * chore: Make the linter happy * chore: Reorganize files * chore: Fix docstrings * chore: Fix variable names * fix: Focus the flyout on T for simple toolboxes * test: Add tests for focus toolbox shortcut * refactor: Remove `WorkspaceSvg.keyboardAccessibilityMode` * refactor: Simplify navigation logic * fix: Fix tests * chore: Normalize imports * fix: Fix bad merge resolution * fix: Fix docstrings * fix: Fix navigation down on blocks with a statement input and no next connection * fix: Be more defensive about navigating to connections * fix: Use FlyoutButton IDs as row IDs
1 parent 3fb96e4 commit 05af6b6

54 files changed

Lines changed: 2458 additions & 2770 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/blockly/core/block_svg.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1905,4 +1905,58 @@ export class BlockSvg
19051905
canBeFocused(): boolean {
19061906
return true;
19071907
}
1908+
1909+
/**
1910+
* Returns a set of all of the parent blocks of the given block.
1911+
*
1912+
* @internal
1913+
* @returns A set of the parents of the given block.
1914+
*/
1915+
getParents(): Set<BlockSvg> {
1916+
const parents = new Set<BlockSvg>();
1917+
let parent = this.getParent();
1918+
while (parent) {
1919+
parents.add(parent);
1920+
parent = parent.getParent();
1921+
}
1922+
1923+
return parents;
1924+
}
1925+
1926+
/**
1927+
* Returns a set of all of the parent blocks connected to an output of the
1928+
* given block or one of its parents. Also includes the given block.
1929+
*
1930+
* @internal
1931+
* @returns A set of the output-connected parents of the given block.
1932+
*/
1933+
getOutputParents(): Set<BlockSvg> {
1934+
const parents = new Set<BlockSvg>();
1935+
parents.add(this);
1936+
let parent = this.outputConnection?.targetBlock();
1937+
while (parent) {
1938+
parents.add(parent);
1939+
parent = parent.outputConnection?.targetBlock();
1940+
}
1941+
1942+
return parents;
1943+
}
1944+
1945+
/**
1946+
* Returns an ID for the visual "row" this block is part of.
1947+
*
1948+
* @internal
1949+
*/
1950+
getRowId(): string {
1951+
const connectedInput =
1952+
this.outputConnection?.targetConnection?.getParentInput();
1953+
// Blocks with an output value have the same ID as the input they're
1954+
// connected to.
1955+
if (connectedInput) {
1956+
return connectedInput.getRowId();
1957+
}
1958+
1959+
// All other blocks are their own row.
1960+
return this.id;
1961+
}
19081962
}

packages/blockly/core/blockly.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,13 @@ import {
177177
import {IVariableMap} from './interfaces/i_variable_map.js';
178178
import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
179179
import * as internalConstants from './internal_constants.js';
180-
import {LineCursor} from './keyboard_nav/line_cursor.js';
181-
import {Marker} from './keyboard_nav/marker.js';
180+
import {ToolboxNavigator} from './keyboard_nav/navigators/toolbox_navigator.js';
182181
import {
183182
KeyboardNavigationController,
184183
keyboardNavigationController,
185184
} from './keyboard_navigation_controller.js';
186185
import type {LayerManager} from './layer_manager.js';
187186
import * as layers from './layers.js';
188-
import {MarkerManager} from './marker_manager.js';
189187
import {Menu} from './menu.js';
190188
import {MenuItem} from './menuitem.js';
191189
import {MetricsManager} from './metrics_manager.js';
@@ -439,16 +437,21 @@ Names.prototype.populateProcedures = function (
439437
};
440438
// clang-format on
441439

442-
export * from './flyout_navigator.js';
443440
export * from './interfaces/i_navigation_policy.js';
444-
export * from './keyboard_nav/block_navigation_policy.js';
445-
export * from './keyboard_nav/connection_navigation_policy.js';
446-
export * from './keyboard_nav/field_navigation_policy.js';
447-
export * from './keyboard_nav/flyout_button_navigation_policy.js';
448-
export * from './keyboard_nav/flyout_navigation_policy.js';
449-
export * from './keyboard_nav/flyout_separator_navigation_policy.js';
450-
export * from './keyboard_nav/workspace_navigation_policy.js';
451-
export * from './navigator.js';
441+
export * from './keyboard_nav/navigation_policies/block_comment_navigation_policy.js';
442+
export * from './keyboard_nav/navigation_policies/block_navigation_policy.js';
443+
export * from './keyboard_nav/navigation_policies/comment_bar_button_navigation_policy.js';
444+
export * from './keyboard_nav/navigation_policies/comment_editor_navigation_policy.js';
445+
export * from './keyboard_nav/navigation_policies/connection_navigation_policy.js';
446+
export * from './keyboard_nav/navigation_policies/field_navigation_policy.js';
447+
export * from './keyboard_nav/navigation_policies/flyout_button_navigation_policy.js';
448+
export * from './keyboard_nav/navigation_policies/flyout_separator_navigation_policy.js';
449+
export * from './keyboard_nav/navigation_policies/icon_navigation_policy.js';
450+
export * from './keyboard_nav/navigation_policies/toolbox_item_navigation_policy.js';
451+
export * from './keyboard_nav/navigation_policies/workspace_comment_navigation_policy.js';
452+
export * from './keyboard_nav/navigation_policies/workspace_navigation_policy.js';
453+
export * from './keyboard_nav/navigators/flyout_navigator.js';
454+
export * from './keyboard_nav/navigators/navigator.js';
452455
export * from './toast.js';
453456

454457
// Re-export submodules that no longer declareLegacyNamespace.
@@ -471,7 +474,6 @@ export {
471474
DragTarget,
472475
Events,
473476
Extensions,
474-
LineCursor,
475477
Procedures,
476478
ShortcutItems,
477479
Themes,
@@ -596,8 +598,6 @@ export {
596598
KeyboardNavigationController,
597599
LabelFlyoutInflater,
598600
LayerManager,
599-
Marker,
600-
MarkerManager,
601601
Menu,
602602
MenuGenerator,
603603
MenuGeneratorFunction,
@@ -619,6 +619,7 @@ export {
619619
Toolbox,
620620
ToolboxCategory,
621621
ToolboxItem,
622+
ToolboxNavigator,
622623
ToolboxSeparator,
623624
Trashcan,
624625
UnattachedFieldError,

packages/blockly/core/comments/comment_editor.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const COMMENT_EDITOR_FOCUS_IDENTIFIER = '_comment_textarea_';
2525

2626
/** The part of a comment that can be typed into. */
2727
export class CommentEditor implements IFocusableNode {
28-
id?: string;
28+
id: string;
2929
/** The foreignObject containing the HTML text area. */
3030
private foreignObject: SVGForeignObjectElement;
3131

@@ -42,7 +42,7 @@ export class CommentEditor implements IFocusableNode {
4242

4343
constructor(
4444
public workspace: WorkspaceSvg,
45-
commentId?: string,
45+
commentId: string,
4646
private onFinishEditing?: () => void,
4747
) {
4848
this.foreignObject = dom.createSvgElement(Svg.FOREIGNOBJECT, {
@@ -67,10 +67,8 @@ export class CommentEditor implements IFocusableNode {
6767
body.appendChild(this.textArea);
6868
this.foreignObject.appendChild(body);
6969

70-
if (commentId) {
71-
this.id = commentId + COMMENT_EDITOR_FOCUS_IDENTIFIER;
72-
this.textArea.setAttribute('id', this.id);
73-
}
70+
this.id = commentId + COMMENT_EDITOR_FOCUS_IDENTIFIER;
71+
this.textArea.setAttribute('id', this.id);
7472

7573
// Register browser event listeners for the user typing in the textarea.
7674
browserEvents.conditionalBind(

packages/blockly/core/field_input.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import './events/events_block_change.js';
1616

1717
import {BlockSvg} from './block_svg.js';
18+
import {IFocusableNode} from './blockly.js';
1819
import * as browserEvents from './browser_events.js';
1920
import * as bumpObjects from './bump_objects.js';
2021
import * as dialog from './dialog.js';
@@ -28,7 +29,6 @@ import {
2829
UnattachedFieldError,
2930
} from './field.js';
3031
import {getFocusManager} from './focus_manager.js';
31-
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
3232
import {Msg} from './msg.js';
3333
import * as renderManagement from './render_management.js';
3434
import * as aria from './utils/aria.js';
@@ -600,16 +600,20 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
600600
dropDownDiv.hideWithoutAnimation();
601601
} else if (e.key === 'Tab') {
602602
e.preventDefault();
603-
const cursor = this.workspace_?.getCursor();
603+
const navigator = this.workspace_?.getNavigator();
604604

605605
const isValidDestination = (node: IFocusableNode | null) =>
606606
(node instanceof FieldInput ||
607607
(node instanceof BlockSvg && node.isSimpleReporter())) &&
608608
node !== this.getSourceBlock();
609609

610-
let target = e.shiftKey
611-
? cursor?.getPreviousNode(this, isValidDestination, false)
612-
: cursor?.getNextNode(this, isValidDestination, false);
610+
// eslint-disable-next-line @typescript-eslint/no-this-alias
611+
let target: IFocusableNode | null | undefined = this;
612+
do {
613+
target = e.shiftKey
614+
? navigator?.getOutNode(target)
615+
: navigator?.getInNode(target);
616+
} while (target && !isValidDestination(target));
613617
target =
614618
target instanceof BlockSvg && target.isSimpleReporter()
615619
? target.getFields().next().value
@@ -625,7 +629,9 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
625629
targetSourceBlock instanceof BlockSvg
626630
) {
627631
getFocusManager().focusNode(targetSourceBlock);
628-
} else getFocusManager().focusNode(target);
632+
} else {
633+
getFocusManager().focusNode(target);
634+
}
629635
target.showEditor();
630636
}
631637
}

packages/blockly/core/flyout_base.ts

Lines changed: 2 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,11 @@ import {EventType} from './events/type.js';
1919
import * as eventUtils from './events/utils.js';
2020
import {FlyoutItem} from './flyout_item.js';
2121
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
22-
import {FlyoutNavigator} from './flyout_navigator.js';
2322
import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js';
2423
import {IAutoHideable} from './interfaces/i_autohideable.js';
2524
import type {IFlyout} from './interfaces/i_flyout.js';
2625
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
27-
import {IFocusableNode} from './interfaces/i_focusable_node.js';
28-
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
26+
import {FlyoutNavigator} from './keyboard_nav/navigators/flyout_navigator.js';
2927
import type {Options} from './options.js';
3028
import * as registry from './registry.js';
3129
import * as renderManagement from './render_management.js';
@@ -42,7 +40,7 @@ import {WorkspaceSvg} from './workspace_svg.js';
4240
*/
4341
export abstract class Flyout
4442
extends DeleteArea
45-
implements IAutoHideable, IFlyout, IFocusableNode
43+
implements IAutoHideable, IFlyout
4644
{
4745
/**
4846
* Position the flyout.
@@ -797,86 +795,4 @@ export abstract class Flyout
797795

798796
return null;
799797
}
800-
801-
/**
802-
* See IFocusableNode.getFocusableElement.
803-
*
804-
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
805-
*/
806-
getFocusableElement(): HTMLElement | SVGElement {
807-
throw new Error('Flyouts are not directly focusable.');
808-
}
809-
810-
/**
811-
* See IFocusableNode.getFocusableTree.
812-
*
813-
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
814-
*/
815-
getFocusableTree(): IFocusableTree {
816-
throw new Error('Flyouts are not directly focusable.');
817-
}
818-
819-
/** See IFocusableNode.onNodeFocus. */
820-
onNodeFocus(): void {}
821-
822-
/** See IFocusableNode.onNodeBlur. */
823-
onNodeBlur(): void {}
824-
825-
/** See IFocusableNode.canBeFocused. */
826-
canBeFocused(): boolean {
827-
return false;
828-
}
829-
830-
/**
831-
* See IFocusableNode.getRootFocusableNode.
832-
*
833-
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
834-
*/
835-
getRootFocusableNode(): IFocusableNode {
836-
throw new Error('Flyouts are not directly focusable.');
837-
}
838-
839-
/**
840-
* See IFocusableNode.getRestoredFocusableNode.
841-
*
842-
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
843-
*/
844-
getRestoredFocusableNode(
845-
_previousNode: IFocusableNode | null,
846-
): IFocusableNode | null {
847-
throw new Error('Flyouts are not directly focusable.');
848-
}
849-
850-
/**
851-
* See IFocusableNode.getNestedTrees.
852-
*
853-
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
854-
*/
855-
getNestedTrees(): Array<IFocusableTree> {
856-
throw new Error('Flyouts are not directly focusable.');
857-
}
858-
859-
/**
860-
* See IFocusableNode.lookUpFocusableNode.
861-
*
862-
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
863-
*/
864-
lookUpFocusableNode(_id: string): IFocusableNode | null {
865-
throw new Error('Flyouts are not directly focusable.');
866-
}
867-
868-
/** See IFocusableTree.onTreeFocus. */
869-
onTreeFocus(
870-
_node: IFocusableNode,
871-
_previousTree: IFocusableTree | null,
872-
): void {}
873-
874-
/**
875-
* See IFocusableNode.onTreeBlur.
876-
*
877-
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
878-
*/
879-
onTreeBlur(_nextTree: IFocusableTree | null): void {
880-
throw new Error('Flyouts are not directly focusable.');
881-
}
882798
}

packages/blockly/core/flyout_button.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,13 @@ export class FlyoutButton
414414
canBeFocused(): boolean {
415415
return true;
416416
}
417+
418+
/**
419+
* Returns the ID of this FlyoutButton.
420+
*/
421+
getId() {
422+
return this.id;
423+
}
417424
}
418425

419426
/** CSS for buttons and labels. See css.js for use. */

packages/blockly/core/flyout_navigator.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)