Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/blockly/core/block_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {EventType} from './events/type.js';
import * as eventUtils from './events/utils.js';
import {FieldLabel} from './field_label.js';
import {getFocusManager} from './focus_manager.js';
import * as hints from './hints.js';
import {IconType} from './icons/icon_types.js';
import {MutatorIcon} from './icons/mutator_icon.js';
import {WarningIcon} from './icons/warning_icon.js';
Expand All @@ -52,6 +53,7 @@ import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import {IIcon} from './interfaces/i_icon.js';
import * as internalConstants from './internal_constants.js';
import {KeyboardMover} from './keyboard_nav/keyboard_mover.js';
import {Msg} from './msg.js';
import * as renderManagement from './render_management.js';
import {RenderedConnection} from './rendered_connection.js';
Expand Down Expand Up @@ -1903,6 +1905,34 @@ export class BlockSvg
return true;
}

/**
* Handles the user acting on this block via keyboard navigation.
* If this block is in the flyout, a new copy is spawned in move mode on the
* main workspace. If this block has a single full-block field, that field
* will be focused. Otherwise, this is a no-op.
*/
performAction() {
if (this.workspace.isFlyout) {
KeyboardMover.mover.startMove(this);
return;
} else if (this.isSimpleReporter()) {
for (const input of this.inputList) {
for (const field of input.fieldRow) {
if (field.isClickable() && field.isFullBlockField()) {
field.showEditor();
return;
}
}
}
}

if (this.workspace.getNavigator().getFirstChild(this)) {
hints.showBlockNavigationHint(this.workspace);
} else {
hints.showHelpHint(this.workspace);
}
}

/**
* Returns a set of all of the parent blocks of the given block.
*
Expand Down
9 changes: 9 additions & 0 deletions packages/blockly/core/bubbles/mini_workspace_bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import type {BlocklyOptions} from '../blockly_options.js';
import {Abstract as AbstractEvent} from '../events/events_abstract.js';
import {getFocusManager} from '../focus_manager.js';
import {KeyboardMover} from '../keyboard_nav/keyboard_mover.js';
import {Options} from '../options.js';
import {Coordinate} from '../utils/coordinate.js';
Expand Down Expand Up @@ -287,4 +288,12 @@ export class MiniWorkspaceBubble extends Bubble {
'monkey-patched in by blockly.ts',
);
}

/**
* Handles the user acting on this bubble via keyboard navigation by focusing
* the mutator workspace.
*/
performAction() {
getFocusManager().focusTree(this.getWorkspace());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason to add this to two Bubble subclasses as opposed to just the base class?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they're doing different things; the mini workspace bubble focuses its workspace and the text input bubble focuses its comment editor. Bubble is an abstract class and there's no common behavior, so each subclass does its own thing.

}
}
8 changes: 8 additions & 0 deletions packages/blockly/core/bubbles/textinput_bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,14 @@ export class TextInputBubble extends Bubble {
getEditor() {
return this.editor;
}

/**
* Handles the user acting on this bubble via keyboard navigation by focusing
* the comment editor.
*/
performAction() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we're adding this to these Bubble subclasses instead of the base class itself?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, see the other reply on the workspace bubble.

getFocusManager().focusNode(this.getEditor());
}
}

Css.register(`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,4 +358,13 @@ export class RenderedWorkspaceComment
canBeFocused(): boolean {
return true;
}

/**
* Handles the user acting on this comment via keyboard navigation.
* Expands the comment and focuses its editor.
*/
performAction() {
this.setCollapsed(false);
getFocusManager().focusNode(this.getEditorFocusableNode());
}
}
8 changes: 8 additions & 0 deletions packages/blockly/core/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,14 @@ export abstract class Field<T = any>
return true;
}

/**
* Handles the user acting on this field via keyboard navigation.
* Shows and focuses the field editor.
*/
performAction() {
this.showEditor();
}

/**
* Subclasses should reimplement this method to construct their Field
* subclass from a JSON arg object.
Expand Down
13 changes: 13 additions & 0 deletions packages/blockly/core/flyout_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,19 @@ export class FlyoutButton
getId() {
return this.id;
}

/**
* Handles the user acting on this button via keyboard navigation.
* Invokes the click handler callback.
*/
performAction(): void {
if (!this.isFlyoutLabel) {
const callback = this.targetWorkspace.getButtonCallback(this.callbackKey);
if (callback) {
callback(this);
}
}
}
}

/** CSS for buttons and labels. See css.js for use. */
Expand Down
38 changes: 38 additions & 0 deletions packages/blockly/core/hints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@

import {Msg} from './msg.js';
import {Toast} from './toast.js';
import {getShortActionShortcut} from './utils/shortcut_formatting.js';
import * as userAgent from './utils/useragent.js';
import type {WorkspaceSvg} from './workspace_svg.js';

const unconstrainedMoveHintId = 'unconstrainedMoveHint';
const constrainedMoveHintId = 'constrainedMoveHint';
const helpHintId = 'helpHint';
const blockNavigationHintId = 'blockNavigationHint';
const workspaceNavigationHintId = 'workspaceNavigationHint';

/**
* Nudge the user to use unconstrained movement.
Expand Down Expand Up @@ -62,3 +66,37 @@ export function clearMoveHints(workspace: WorkspaceSvg) {
Toast.hide(workspace, constrainedMoveHintId);
Toast.hide(workspace, unconstrainedMoveHintId);
}

/**
* Nudge the user to open the help.
*
* @param workspace The workspace.
*/
export function showHelpHint(workspace: WorkspaceSvg) {
const shortcut = getShortActionShortcut('list_shortcuts');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we handle the potential case where shortcut is an empty string? I think it's possible that the shortcut could have been unregistered which would lead to a help prompt with a nonsensical message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a possibility, I'm not sure how we might helpfully handle it though. End users wouldn't be able to do anything about it, I suppose we could just not show the toast? Or did you have something else in mind?

const message = Msg['HELP_PROMPT'].replace('%1', shortcut);
const id = helpHintId;
Toast.show(workspace, {message, id});
}

/**
* Tell the user how to navigate inside blocks.
*
* @param workspace The workspace.
*/
export function showBlockNavigationHint(workspace: WorkspaceSvg) {
const message = Msg['KEYBOARD_NAV_BLOCK_NAVIGATION_HINT'];
const id = blockNavigationHintId;
Toast.show(workspace, {message, id});
}

/**
* Tell the user how to navigate inside the workspace.
*
* @param workspace The workspace.
*/
export function showWorkspaceNavigationHint(workspace: WorkspaceSvg) {
const message = Msg['KEYBOARD_NAV_WORKSPACE_NAVIGATION_HINT'];
const id = workspaceNavigationHintId;
Toast.show(workspace, {message, id});
}
18 changes: 18 additions & 0 deletions packages/blockly/core/icons/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import type {Block} from '../block.js';
import type {BlockSvg} from '../block_svg.js';
import * as browserEvents from '../browser_events.js';
import {getFocusManager} from '../focus_manager.js';
import type {IContextMenu} from '../interfaces/i_contextmenu.js';
import type {IFocusableTree} from '../interfaces/i_focusable_tree.js';
import {hasBubble} from '../interfaces/i_has_bubble.js';
import type {IIcon} from '../interfaces/i_icon.js';
import * as renderManagement from '../render_management.js';
import * as tooltip from '../tooltip.js';
import {Coordinate} from '../utils/coordinate.js';
import * as dom from '../utils/dom.js';
Expand Down Expand Up @@ -189,6 +191,22 @@ export abstract class Icon implements IIcon, IContextMenu {
return true;
}

/**
* Handles the user acting on this icon via keyboard navigation.
* Performs the same action as a click would, and focuses this icon's bubble
* if it has one.
*/
performAction() {
this.onClick();
renderManagement.finishQueuedRenders().then(() => {
if (hasBubble(this) && this.bubbleIsVisible()) {
const bubble = this.getBubble();
if (!bubble) return;
getFocusManager().focusNode(bubble);
}
});
}

/**
* Returns the block that this icon is attached to.
*
Expand Down
7 changes: 7 additions & 0 deletions packages/blockly/core/interfaces/i_focusable_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ export interface IFocusableNode {
* @returns Whether this node can be focused by FocusManager.
*/
canBeFocused(): boolean;

/**
* Optional method invoked when this node has focus and the user acts on it by
* pressing Enter or Space. Behavior should generally be similar to the node
* being clicked on.
*/
performAction?(): void;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/blockly/core/keyboard_nav/keyboard_mover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class KeyboardMover {
* @param event The keyboard event that triggered this move.
* @returns True iff a move has successfully begun.
*/
startMove(draggable: IDraggable, event: KeyboardEvent) {
startMove(draggable: IDraggable, event?: KeyboardEvent) {
if (!this.canMove(draggable) || this.isMoving()) return false;

const DraggerClass = registry.getClassFromOptions(
Expand Down
25 changes: 25 additions & 0 deletions packages/blockly/core/shortcut_items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export enum names {
NAVIGATE_UP = 'up',
NAVIGATE_DOWN = 'down',
DISCONNECT = 'disconnect',
PERFORM_ACTION = 'perform_action',
NEXT_STACK = 'next_stack',
PREVIOUS_STACK = 'previous_stack',
}
Expand Down Expand Up @@ -719,6 +720,29 @@ export function registerDisconnectBlock() {
ShortcutRegistry.registry.register(disconnectShortcut);
}

/**
* Registers keyboard shortcut to perform an action on the focused element.
*/
export function registerPerformAction() {
const performActionShortcut: KeyboardShortcut = {
name: names.PERFORM_ACTION,
preconditionFn: (workspace) => !workspace.isDragging(),
callback: (_workspace, e) => {
keyboardNavigationController.setIsActive(true);
const focusedNode = getFocusManager().getFocusedNode();
if (focusedNode && 'performAction' in focusedNode) {
e.preventDefault();
focusedNode.performAction?.();
return true;
}
return false;
},
keyCodes: [KeyCodes.ENTER, KeyCodes.SPACE],
allowCollision: true,
};
ShortcutRegistry.registry.register(performActionShortcut);
}

/**
* Registers keyboard shortcuts to jump between stacks/top-level items on the
* workspace.
Expand Down Expand Up @@ -815,6 +839,7 @@ export function registerKeyboardNavigationShortcuts() {
registerFocusToolbox();
registerArrowNavigation();
registerDisconnectBlock();
registerPerformAction();
registerStackNavigation();
}

Expand Down
Loading
Loading