feat: add keyboard navigation controller (#8924)

* feat: add keyboard navigation controller

* chore: add tests

* chore: fix tsdoc
This commit is contained in:
Maribeth Moffatt
2025-05-29 13:48:54 -07:00
committed by GitHub
parent 3cbca8e4b6
commit 0498ed6174
5 changed files with 112 additions and 0 deletions

View File

@@ -173,6 +173,10 @@ import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
import * as internalConstants from './internal_constants.js'; import * as internalConstants from './internal_constants.js';
import {LineCursor} from './keyboard_nav/line_cursor.js'; import {LineCursor} from './keyboard_nav/line_cursor.js';
import {Marker} from './keyboard_nav/marker.js'; import {Marker} from './keyboard_nav/marker.js';
import {
KeyboardNavigationController,
keyboardNavigationController,
} from './keyboard_navigation_controller.js';
import type {LayerManager} from './layer_manager.js'; import type {LayerManager} from './layer_manager.js';
import * as layers from './layers.js'; import * as layers from './layers.js';
import {MarkerManager} from './marker_manager.js'; import {MarkerManager} from './marker_manager.js';
@@ -580,6 +584,7 @@ export {
ImageProperties, ImageProperties,
Input, Input,
InsertionMarkerPreviewer, InsertionMarkerPreviewer,
KeyboardNavigationController,
LabelFlyoutInflater, LabelFlyoutInflater,
LayerManager, LayerManager,
Marker, Marker,
@@ -631,6 +636,7 @@ export {
isSelectable, isSelectable,
isSerializable, isSerializable,
isVariableBackedParameterModel, isVariableBackedParameterModel,
keyboardNavigationController,
layers, layers,
renderManagement, renderManagement,
serialization, serialization,

View File

@@ -31,6 +31,7 @@ import {IDraggable, isDraggable} from './interfaces/i_draggable.js';
import {IDragger} from './interfaces/i_dragger.js'; import {IDragger} from './interfaces/i_dragger.js';
import type {IFlyout} from './interfaces/i_flyout.js'; import type {IFlyout} from './interfaces/i_flyout.js';
import type {IIcon} from './interfaces/i_icon.js'; import type {IIcon} from './interfaces/i_icon.js';
import {keyboardNavigationController} from './keyboard_navigation_controller.js';
import * as registry from './registry.js'; import * as registry from './registry.js';
import * as Tooltip from './tooltip.js'; import * as Tooltip from './tooltip.js';
import * as Touch from './touch.js'; import * as Touch from './touch.js';
@@ -541,8 +542,10 @@ export class Gesture {
// have higher priority than workspaces. The ordering within drags does // have higher priority than workspaces. The ordering within drags does
// not matter, because the three types of dragging are exclusive. // not matter, because the three types of dragging are exclusive.
if (this.dragger) { if (this.dragger) {
keyboardNavigationController.setIsActive(false);
this.dragger.onDragEnd(e, this.currentDragDeltaXY); this.dragger.onDragEnd(e, this.currentDragDeltaXY);
} else if (this.workspaceDragger) { } else if (this.workspaceDragger) {
keyboardNavigationController.setIsActive(false);
this.workspaceDragger.endDrag(this.currentDragDeltaXY); this.workspaceDragger.endDrag(this.currentDragDeltaXY);
} else if (this.isBubbleClick()) { } else if (this.isBubbleClick()) {
// Do nothing, bubbles don't currently respond to clicks. // Do nothing, bubbles don't currently respond to clicks.
@@ -743,6 +746,8 @@ export class Gesture {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
keyboardNavigationController.setIsActive(false);
this.dispose(); this.dispose();
} }

View File

@@ -0,0 +1,63 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* The KeyboardNavigationController handles coordinating Blockly-wide
* keyboard navigation behavior, such as enabling/disabling full
* cursor visualization.
*/
export class KeyboardNavigationController {
/** Whether the user is actively using keyboard navigation. */
private isActive = false;
/** Css class name added to body if keyboard nav is active. */
private activeClassName = 'blocklyKeyboardNavigation';
/**
* Sets whether a user is actively using keyboard navigation.
*
* If they are, apply a css class to the entire page so that
* focused items can apply additional styling for keyboard users.
*
* Note that since enabling keyboard navigation presents significant UX changes
* (such as cursor visualization and move mode), callers should take care to
* only set active keyboard navigation when they have a high confidence in that
* being the correct state. In general, in any given mouse or key input situation
* callers can choose one of three paths:
* 1. Do nothing. This should be the choice for neutral actions that don't
* predominantly imply keyboard or mouse usage (such as clicking to select a block).
* 2. Disable keyboard navigation. This is the best choice when a user is definitely
* predominantly using the mouse (such as using a right click to open the context menu).
* 3. Enable keyboard navigation. This is the best choice when there's high confidence
* a user actually intends to use it (such as attempting to use the arrow keys to move
* around).
*
* @param isUsing
*/
setIsActive(isUsing: boolean = true) {
this.isActive = isUsing;
this.updateActiveVisualization();
}
/**
* @returns true if the user is actively using keyboard navigation
* (e.g., has recently taken some action that is only relevant to keyboard users)
*/
getIsActive(): boolean {
return this.isActive;
}
/** Adds or removes the css class that indicates keyboard navigation is active. */
private updateActiveVisualization() {
if (this.isActive) {
document.body.classList.add(this.activeClassName);
} else {
document.body.classList.remove(this.activeClassName);
}
}
}
/** Singleton instance of the keyboard navigation controller. */
export const keyboardNavigationController = new KeyboardNavigationController();

View File

@@ -219,6 +219,7 @@
import './jso_deserialization_test.js'; import './jso_deserialization_test.js';
import './jso_serialization_test.js'; import './jso_serialization_test.js';
import './json_test.js'; import './json_test.js';
import './keyboard_navigation_controller_test.js';
import './layering_test.js'; import './layering_test.js';
import './blocks/lists_test.js'; import './blocks/lists_test.js';
import './blocks/logic_ternary_test.js'; import './blocks/logic_ternary_test.js';

View File

@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {assert} from '../../node_modules/chai/chai.js';
import {
sharedTestSetup,
sharedTestTeardown,
} from './test_helpers/setup_teardown.js';
suite('Keyboard Navigation Controller', function () {
setup(function () {
sharedTestSetup.call(this);
Blockly.keyboardNavigationController.setIsActive(false);
});
teardown(function () {
sharedTestTeardown.call(this);
Blockly.keyboardNavigationController.setIsActive(false);
});
test('Setting active keyboard navigation adds css class', function () {
Blockly.keyboardNavigationController.setIsActive(true);
assert.isTrue(
document.body.classList.contains('blocklyKeyboardNavigation'),
);
});
test('Disabling active keyboard navigation removes css class', function () {
Blockly.keyboardNavigationController.setIsActive(false);
assert.isFalse(
document.body.classList.contains('blocklyKeyboardNavigation'),
);
});
});