Skip to content

Commit 219eb37

Browse files
authored
refactor: Move arrow key shortcuts into their own class. (#304)
* refactor: Move arrow key shortcuts into their own class. * chore: Remove inadvertently reintroduced code. * chore: Update names of arrow key actions and remove unused actions.
1 parent 955e1fd commit 219eb37

File tree

3 files changed

+219
-174
lines changed

3 files changed

+219
-174
lines changed

src/actions/arrow_navigation.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {ASTNode, ShortcutRegistry, utils as BlocklyUtils} from 'blockly/core';
8+
9+
import type {Field, Toolbox, WorkspaceSvg} from 'blockly/core';
10+
11+
import * as Constants from '../constants';
12+
import type {Navigation} from '../navigation';
13+
14+
const KeyCodes = BlocklyUtils.KeyCodes;
15+
16+
/**
17+
* Class for registering shortcuts for navigating the workspace with arrow keys.
18+
*/
19+
export class ArrowNavigation {
20+
constructor(
21+
private navigation: Navigation,
22+
private canCurrentlyNavigate: (ws: WorkspaceSvg) => boolean,
23+
) {}
24+
25+
/**
26+
* Gives the cursor to the field to handle if the cursor is on a field.
27+
*
28+
* @param workspace The workspace to check.
29+
* @param shortcut The shortcut
30+
* to give to the field.
31+
* @returns True if the shortcut was handled by the field, false
32+
* otherwise.
33+
*/
34+
fieldShortcutHandler(
35+
workspace: WorkspaceSvg,
36+
shortcut: ShortcutRegistry.KeyboardShortcut,
37+
): boolean {
38+
const cursor = workspace.getCursor();
39+
if (!cursor || !cursor.getCurNode()) {
40+
return false;
41+
}
42+
const curNode = cursor.getCurNode();
43+
if (curNode.getType() === ASTNode.types.FIELD) {
44+
return (curNode.getLocation() as Field).onShortcut(shortcut);
45+
}
46+
return false;
47+
}
48+
49+
/**
50+
* Adds all arrow key navigation shortcuts to the registry.
51+
*/
52+
install() {
53+
const shortcuts: {
54+
[name: string]: ShortcutRegistry.KeyboardShortcut;
55+
} = {
56+
/** Go to the next location to the right. */
57+
right: {
58+
name: Constants.SHORTCUT_NAMES.RIGHT,
59+
preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace),
60+
callback: (workspace, _, shortcut) => {
61+
const toolbox = workspace.getToolbox() as Toolbox;
62+
let isHandled = false;
63+
switch (this.navigation.getState(workspace)) {
64+
case Constants.STATE.WORKSPACE:
65+
isHandled = this.fieldShortcutHandler(workspace, shortcut);
66+
if (!isHandled && workspace) {
67+
workspace.getCursor()?.in();
68+
isHandled = true;
69+
}
70+
return isHandled;
71+
case Constants.STATE.TOOLBOX:
72+
isHandled =
73+
toolbox && typeof toolbox.onShortcut === 'function'
74+
? toolbox.onShortcut(shortcut)
75+
: false;
76+
if (!isHandled) {
77+
this.navigation.focusFlyout(workspace);
78+
}
79+
return true;
80+
default:
81+
return false;
82+
}
83+
},
84+
keyCodes: [KeyCodes.RIGHT],
85+
},
86+
87+
/** Go to the next location to the left. */
88+
left: {
89+
name: Constants.SHORTCUT_NAMES.LEFT,
90+
preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace),
91+
callback: (workspace, _, shortcut) => {
92+
const toolbox = workspace.getToolbox() as Toolbox;
93+
let isHandled = false;
94+
switch (this.navigation.getState(workspace)) {
95+
case Constants.STATE.WORKSPACE:
96+
isHandled = this.fieldShortcutHandler(workspace, shortcut);
97+
if (!isHandled && workspace) {
98+
workspace.getCursor()?.out();
99+
isHandled = true;
100+
}
101+
return isHandled;
102+
case Constants.STATE.FLYOUT:
103+
this.navigation.focusToolbox(workspace);
104+
return true;
105+
case Constants.STATE.TOOLBOX:
106+
return toolbox && typeof toolbox.onShortcut === 'function'
107+
? toolbox.onShortcut(shortcut)
108+
: false;
109+
default:
110+
return false;
111+
}
112+
},
113+
keyCodes: [KeyCodes.LEFT],
114+
},
115+
116+
/** Go down to the next location. */
117+
down: {
118+
name: Constants.SHORTCUT_NAMES.DOWN,
119+
preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace),
120+
callback: (workspace, _, shortcut) => {
121+
const toolbox = workspace.getToolbox() as Toolbox;
122+
const flyout = workspace.getFlyout();
123+
let isHandled = false;
124+
switch (this.navigation.getState(workspace)) {
125+
case Constants.STATE.WORKSPACE:
126+
isHandled = this.fieldShortcutHandler(workspace, shortcut);
127+
if (!isHandled && workspace) {
128+
workspace.getCursor()?.next();
129+
isHandled = true;
130+
}
131+
return isHandled;
132+
case Constants.STATE.FLYOUT:
133+
isHandled = this.fieldShortcutHandler(workspace, shortcut);
134+
if (!isHandled && flyout) {
135+
flyout.getWorkspace()?.getCursor()?.next();
136+
isHandled = true;
137+
}
138+
return isHandled;
139+
case Constants.STATE.TOOLBOX:
140+
return toolbox && typeof toolbox.onShortcut === 'function'
141+
? toolbox.onShortcut(shortcut)
142+
: false;
143+
default:
144+
return false;
145+
}
146+
},
147+
keyCodes: [KeyCodes.DOWN],
148+
},
149+
/** Go up to the previous location. */
150+
up: {
151+
name: Constants.SHORTCUT_NAMES.UP,
152+
preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace),
153+
callback: (workspace, _, shortcut) => {
154+
const flyout = workspace.getFlyout();
155+
const toolbox = workspace.getToolbox() as Toolbox;
156+
let isHandled = false;
157+
switch (this.navigation.getState(workspace)) {
158+
case Constants.STATE.WORKSPACE:
159+
isHandled = this.fieldShortcutHandler(workspace, shortcut);
160+
if (!isHandled) {
161+
workspace.getCursor()?.prev();
162+
isHandled = true;
163+
}
164+
return isHandled;
165+
case Constants.STATE.FLYOUT:
166+
isHandled = this.fieldShortcutHandler(workspace, shortcut);
167+
if (!isHandled && flyout) {
168+
flyout.getWorkspace()?.getCursor()?.prev();
169+
isHandled = true;
170+
}
171+
return isHandled;
172+
case Constants.STATE.TOOLBOX:
173+
return toolbox && typeof toolbox.onShortcut === 'function'
174+
? toolbox.onShortcut(shortcut)
175+
: false;
176+
default:
177+
return false;
178+
}
179+
},
180+
keyCodes: [KeyCodes.UP],
181+
},
182+
};
183+
184+
for (const shortcut of Object.values(shortcuts)) {
185+
ShortcutRegistry.registry.register(shortcut);
186+
}
187+
}
188+
189+
/**
190+
* Removes all the arrow navigation shortcuts.
191+
*/
192+
uninstall() {
193+
ShortcutRegistry.registry.unregister(Constants.SHORTCUT_NAMES.LEFT);
194+
ShortcutRegistry.registry.unregister(Constants.SHORTCUT_NAMES.RIGHT);
195+
ShortcutRegistry.registry.unregister(Constants.SHORTCUT_NAMES.DOWN);
196+
ShortcutRegistry.registry.unregister(Constants.SHORTCUT_NAMES.UP);
197+
}
198+
}

src/constants.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ export enum STATE {
2424
* Default keyboard navigation shortcut names.
2525
*/
2626
export enum SHORTCUT_NAMES {
27-
PREVIOUS = 'previous',
28-
NEXT = 'next',
29-
IN = 'in',
30-
OUT = 'out',
27+
UP = 'up',
28+
DOWN = 'down',
29+
RIGHT = 'right',
30+
LEFT = 'left',
3131
INSERT = 'insert',
3232
EDIT_OR_CONFIRM = 'edit_or_confirm',
3333
DISCONNECT = 'disconnect',
@@ -43,15 +43,8 @@ export enum SHORTCUT_NAMES {
4343
MOVE_WS_CURSOR_DOWN = 'workspace_down',
4444
MOVE_WS_CURSOR_LEFT = 'workspace_left',
4545
MOVE_WS_CURSOR_RIGHT = 'workspace_right',
46-
TOGGLE_KEYBOARD_NAV = 'toggle_keyboard_nav',
4746
/* eslint-enable @typescript-eslint/naming-convention */
4847
LIST_SHORTCUTS = 'list_shortcuts',
49-
ANNOUNCE = 'announce',
50-
GO_TO_NEXT_SIBLING = 'go_to_next_sibling',
51-
GO_TO_PREVIOUS_SIBLING = 'go_to_previous_sibling',
52-
JUMP_TO_ROOT = 'jump_to_root_of_current_stack',
53-
CONTEXT_OUT = 'context_out',
54-
CONTEXT_IN = 'context_in',
5548
CLEAN_UP = 'clean_up_workspace',
5649
}
5750

@@ -95,12 +88,9 @@ export const SHORTCUT_CATEGORIES: Record<
9588
'redo',
9689
],
9790
'Code navigation': [
98-
SHORTCUT_NAMES.PREVIOUS,
99-
SHORTCUT_NAMES.NEXT,
100-
SHORTCUT_NAMES.IN,
101-
SHORTCUT_NAMES.OUT,
102-
SHORTCUT_NAMES.GO_TO_NEXT_SIBLING,
103-
SHORTCUT_NAMES.GO_TO_PREVIOUS_SIBLING,
104-
SHORTCUT_NAMES.JUMP_TO_ROOT,
91+
SHORTCUT_NAMES.UP,
92+
SHORTCUT_NAMES.DOWN,
93+
SHORTCUT_NAMES.RIGHT,
94+
SHORTCUT_NAMES.LEFT,
10595
],
10696
};

0 commit comments

Comments
 (0)