mirror of
https://github.com/google/blockly.git
synced 2026-01-06 16:40:07 +01:00
feat: add keyboard navigation controller (#8924)
* feat: add keyboard navigation controller * chore: add tests * chore: fix tsdoc
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
core/keyboard_navigation_controller.ts
Normal file
63
core/keyboard_navigation_controller.ts
Normal 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();
|
||||||
@@ -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';
|
||||||
|
|||||||
37
tests/mocha/keyboard_navigation_controller_test.js
Normal file
37
tests/mocha/keyboard_navigation_controller_test.js
Normal 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'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user