Skip to content

Commit 0498ed6

Browse files
authored
feat: add keyboard navigation controller (#8924)
* feat: add keyboard navigation controller * chore: add tests * chore: fix tsdoc
1 parent 3cbca8e commit 0498ed6

File tree

5 files changed

+112
-0
lines changed

5 files changed

+112
-0
lines changed

core/blockly.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
173173
import * as internalConstants from './internal_constants.js';
174174
import {LineCursor} from './keyboard_nav/line_cursor.js';
175175
import {Marker} from './keyboard_nav/marker.js';
176+
import {
177+
KeyboardNavigationController,
178+
keyboardNavigationController,
179+
} from './keyboard_navigation_controller.js';
176180
import type {LayerManager} from './layer_manager.js';
177181
import * as layers from './layers.js';
178182
import {MarkerManager} from './marker_manager.js';
@@ -580,6 +584,7 @@ export {
580584
ImageProperties,
581585
Input,
582586
InsertionMarkerPreviewer,
587+
KeyboardNavigationController,
583588
LabelFlyoutInflater,
584589
LayerManager,
585590
Marker,
@@ -631,6 +636,7 @@ export {
631636
isSelectable,
632637
isSerializable,
633638
isVariableBackedParameterModel,
639+
keyboardNavigationController,
634640
layers,
635641
renderManagement,
636642
serialization,

core/gesture.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {IDraggable, isDraggable} from './interfaces/i_draggable.js';
3131
import {IDragger} from './interfaces/i_dragger.js';
3232
import type {IFlyout} from './interfaces/i_flyout.js';
3333
import type {IIcon} from './interfaces/i_icon.js';
34+
import {keyboardNavigationController} from './keyboard_navigation_controller.js';
3435
import * as registry from './registry.js';
3536
import * as Tooltip from './tooltip.js';
3637
import * as Touch from './touch.js';
@@ -541,8 +542,10 @@ export class Gesture {
541542
// have higher priority than workspaces. The ordering within drags does
542543
// not matter, because the three types of dragging are exclusive.
543544
if (this.dragger) {
545+
keyboardNavigationController.setIsActive(false);
544546
this.dragger.onDragEnd(e, this.currentDragDeltaXY);
545547
} else if (this.workspaceDragger) {
548+
keyboardNavigationController.setIsActive(false);
546549
this.workspaceDragger.endDrag(this.currentDragDeltaXY);
547550
} else if (this.isBubbleClick()) {
548551
// Do nothing, bubbles don't currently respond to clicks.
@@ -743,6 +746,8 @@ export class Gesture {
743746
e.preventDefault();
744747
e.stopPropagation();
745748

749+
keyboardNavigationController.setIsActive(false);
750+
746751
this.dispose();
747752
}
748753

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/**
8+
* The KeyboardNavigationController handles coordinating Blockly-wide
9+
* keyboard navigation behavior, such as enabling/disabling full
10+
* cursor visualization.
11+
*/
12+
export class KeyboardNavigationController {
13+
/** Whether the user is actively using keyboard navigation. */
14+
private isActive = false;
15+
/** Css class name added to body if keyboard nav is active. */
16+
private activeClassName = 'blocklyKeyboardNavigation';
17+
18+
/**
19+
* Sets whether a user is actively using keyboard navigation.
20+
*
21+
* If they are, apply a css class to the entire page so that
22+
* focused items can apply additional styling for keyboard users.
23+
*
24+
* Note that since enabling keyboard navigation presents significant UX changes
25+
* (such as cursor visualization and move mode), callers should take care to
26+
* only set active keyboard navigation when they have a high confidence in that
27+
* being the correct state. In general, in any given mouse or key input situation
28+
* callers can choose one of three paths:
29+
* 1. Do nothing. This should be the choice for neutral actions that don't
30+
* predominantly imply keyboard or mouse usage (such as clicking to select a block).
31+
* 2. Disable keyboard navigation. This is the best choice when a user is definitely
32+
* predominantly using the mouse (such as using a right click to open the context menu).
33+
* 3. Enable keyboard navigation. This is the best choice when there's high confidence
34+
* a user actually intends to use it (such as attempting to use the arrow keys to move
35+
* around).
36+
*
37+
* @param isUsing
38+
*/
39+
setIsActive(isUsing: boolean = true) {
40+
this.isActive = isUsing;
41+
this.updateActiveVisualization();
42+
}
43+
44+
/**
45+
* @returns true if the user is actively using keyboard navigation
46+
* (e.g., has recently taken some action that is only relevant to keyboard users)
47+
*/
48+
getIsActive(): boolean {
49+
return this.isActive;
50+
}
51+
52+
/** Adds or removes the css class that indicates keyboard navigation is active. */
53+
private updateActiveVisualization() {
54+
if (this.isActive) {
55+
document.body.classList.add(this.activeClassName);
56+
} else {
57+
document.body.classList.remove(this.activeClassName);
58+
}
59+
}
60+
}
61+
62+
/** Singleton instance of the keyboard navigation controller. */
63+
export const keyboardNavigationController = new KeyboardNavigationController();

tests/mocha/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@
219219
import './jso_deserialization_test.js';
220220
import './jso_serialization_test.js';
221221
import './json_test.js';
222+
import './keyboard_navigation_controller_test.js';
222223
import './layering_test.js';
223224
import './blocks/lists_test.js';
224225
import './blocks/logic_ternary_test.js';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {assert} from '../../node_modules/chai/chai.js';
8+
import {
9+
sharedTestSetup,
10+
sharedTestTeardown,
11+
} from './test_helpers/setup_teardown.js';
12+
13+
suite('Keyboard Navigation Controller', function () {
14+
setup(function () {
15+
sharedTestSetup.call(this);
16+
Blockly.keyboardNavigationController.setIsActive(false);
17+
});
18+
19+
teardown(function () {
20+
sharedTestTeardown.call(this);
21+
Blockly.keyboardNavigationController.setIsActive(false);
22+
});
23+
24+
test('Setting active keyboard navigation adds css class', function () {
25+
Blockly.keyboardNavigationController.setIsActive(true);
26+
assert.isTrue(
27+
document.body.classList.contains('blocklyKeyboardNavigation'),
28+
);
29+
});
30+
31+
test('Disabling active keyboard navigation removes css class', function () {
32+
Blockly.keyboardNavigationController.setIsActive(false);
33+
assert.isFalse(
34+
document.body.classList.contains('blocklyKeyboardNavigation'),
35+
);
36+
});
37+
});

0 commit comments

Comments
 (0)