Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/blockly/core/bubbles/mini_workspace_bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class MiniWorkspaceBubble extends Bubble {
options.moveOptions.drag
) {
throw new Error(
'The miniworkspace bubble does not scrolling/moving the workspace',
'The miniworkspace bubble does not allow scrolling/moving the workspace',
);
}
if (options.horizontalLayout) {
Expand Down
206 changes: 180 additions & 26 deletions packages/blockly/core/shortcut_items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ export enum names {
JUMP_LAST_BLOCK = 'jump_to_last_block',
JUMP_PREVIOUS_PAGE = 'jump_to_previous_page',
JUMP_NEXT_PAGE = 'jump_to_next_page',
SCROLL_LEFT = 'scroll_left',
SCROLL_RIGHT = 'scroll_right',
SCROLL_UP = 'scroll_up',
SCROLL_DOWN = 'scroll_down',
}

/**
Expand Down Expand Up @@ -478,6 +482,27 @@ export function registerRedo() {
ShortcutRegistry.registry.register(redoShortcut);
}

/**
* Ctrl/Cmd + arrow keys. Shared by unconstrained keyboard move (`MOVE_*`) and
* workspace scroll (`SCROLL_*`). Scroll shortcuts are safely registered after move
* shortcuts because scroll shortcuts do not work when a keyboard move is in progress.
*/
const CTRL_CMD_LEFT = ShortcutRegistry.registry.createSerializedKey(
KeyCodes.LEFT,
[KeyCodes.CTRL_CMD],
);
const CTRL_CMD_RIGHT = ShortcutRegistry.registry.createSerializedKey(
KeyCodes.RIGHT,
[KeyCodes.CTRL_CMD],
);
const CTRL_CMD_UP = ShortcutRegistry.registry.createSerializedKey(KeyCodes.UP, [
KeyCodes.CTRL_CMD,
]);
const CTRL_CMD_DOWN = ShortcutRegistry.registry.createSerializedKey(
KeyCodes.DOWN,
[KeyCodes.CTRL_CMD],
);

/**
* Registers keyboard shortcuts for keyboard-driven movement of workspace
* elements.
Expand Down Expand Up @@ -554,12 +579,7 @@ export function registerMovementShortcuts() {
e.preventDefault();
return KeyboardMover.mover.move(Direction.LEFT, e as KeyboardEvent);
},
keyCodes: [
KeyCodes.LEFT,
ShortcutRegistry.registry.createSerializedKey(KeyCodes.LEFT, [
KeyCodes.CTRL_CMD,
]),
],
keyCodes: [KeyCodes.LEFT, CTRL_CMD_LEFT],
allowCollision: true,
displayText: () => Msg['SHORTCUTS_MOVE_LEFT'],
},
Expand All @@ -570,12 +590,7 @@ export function registerMovementShortcuts() {
e.preventDefault();
return KeyboardMover.mover.move(Direction.RIGHT, e as KeyboardEvent);
},
keyCodes: [
KeyCodes.RIGHT,
ShortcutRegistry.registry.createSerializedKey(KeyCodes.RIGHT, [
KeyCodes.CTRL_CMD,
]),
],
keyCodes: [KeyCodes.RIGHT, CTRL_CMD_RIGHT],
allowCollision: true,
displayText: () => Msg['SHORTCUTS_MOVE_RIGHT'],
},
Expand All @@ -586,12 +601,7 @@ export function registerMovementShortcuts() {
e.preventDefault();
return KeyboardMover.mover.move(Direction.UP, e as KeyboardEvent);
},
keyCodes: [
KeyCodes.UP,
ShortcutRegistry.registry.createSerializedKey(KeyCodes.UP, [
KeyCodes.CTRL_CMD,
]),
],
keyCodes: [KeyCodes.UP, CTRL_CMD_UP],
allowCollision: true,
displayText: () => Msg['SHORTCUTS_MOVE_UP'],
},
Expand All @@ -602,12 +612,7 @@ export function registerMovementShortcuts() {
e.preventDefault();
return KeyboardMover.mover.move(Direction.DOWN, e as KeyboardEvent);
},
keyCodes: [
KeyCodes.DOWN,
ShortcutRegistry.registry.createSerializedKey(KeyCodes.DOWN, [
KeyCodes.CTRL_CMD,
]),
],
keyCodes: [KeyCodes.DOWN, CTRL_CMD_DOWN],
allowCollision: true,
displayText: () => Msg['SHORTCUTS_MOVE_DOWN'],
},
Expand Down Expand Up @@ -1808,6 +1813,153 @@ export function registerJumpNextPage() {
ShortcutRegistry.registry.register(jumpNextPageShortcut);
}

/**
* Pixels to scroll with each workspace shortcut.
*/
const SCROLL_PIXELS = 50;

/** Cardinal directions in which the workspace can be scrolled. */
enum ScrollDirection {
LEFT,
RIGHT,
UP,
DOWN,
}

/**
* Maps scroll directions to the message keys used to announce them to the ARIA live region.
*/
const SCROLL_ANNOUNCEMENT_KEYS: Record<ScrollDirection, string> = {
[ScrollDirection.LEFT]: 'ANNOUNCE_SCROLLED_LEFT',
[ScrollDirection.RIGHT]: 'ANNOUNCE_SCROLLED_RIGHT',
[ScrollDirection.UP]: 'ANNOUNCE_SCROLLED_UP',
[ScrollDirection.DOWN]: 'ANNOUNCE_SCROLLED_DOWN',
};

/**
* Returns true if the workspace can be scrolled by a shortcut.
* Workspaces inside mutator bubbles are never scrollable.
*/
const canScrollWorkspace = (workspace: WorkspaceSvg) => {
return (
!workspace.isDragging() &&
workspace.isMovable() &&
!workspace.targetWorkspace?.isMutator &&
!KeyboardMover.mover.isMoving() &&
!dropDownDiv.isVisible() &&
!widgetDiv.isVisible()
);
};

/**
* Beeps and announces that the workspace cannot scroll further.
*/
const scrollFailure = (workspace: WorkspaceSvg) => {
workspace.getAudioManager().playErrorBeep();
aria.announceDynamicAriaState(Msg['ANNOUNCE_CANT_SCROLL_FURTHER']);
};

/**
* Scrolls the workspace in the given direction.
*
* @param workspace The workspace to scroll.
* @param e The key event that triggered the shortcut.
* @param direction The direction to scroll.
* @returns True so the key is consumed.
*/
const scrollWorkspace = (
workspace: WorkspaceSvg,
e: Event,
direction: ScrollDirection,
): boolean => {
e.preventDefault();

const horizontal =
direction === ScrollDirection.LEFT || direction === ScrollDirection.RIGHT;
const canScroll = horizontal
? workspace.isMovableHorizontally()
: workspace.isMovableVertically();
if (!canScroll) {
scrollFailure(workspace);
return true;
}

let deltaX = 0;
let deltaY = 0;
switch (direction) {
case ScrollDirection.LEFT:
deltaX = SCROLL_PIXELS;
break;
case ScrollDirection.RIGHT:
deltaX = -SCROLL_PIXELS;
break;
case ScrollDirection.UP:
deltaY = SCROLL_PIXELS;
break;
case ScrollDirection.DOWN:
deltaY = -SCROLL_PIXELS;
break;
}
const oldX = workspace.scrollX;
const oldY = workspace.scrollY;
workspace.scroll(oldX + deltaX, oldY + deltaY);
if (workspace.scrollX === oldX && workspace.scrollY === oldY) {
scrollFailure(workspace);
return true;
}
aria.announceDynamicAriaState(Msg[SCROLL_ANNOUNCEMENT_KEYS[direction]]);
return true;
};

/**
* Registers keyboard shortcuts to scroll the focused workspace or flyout with
* Ctrl/Cmd + arrow keys.
*/
export function registerWorkspaceScrollShortcuts() {
const shortcuts: KeyboardShortcut[] = [
{
name: names.SCROLL_LEFT,
preconditionFn: canScrollWorkspace,
callback: (workspace, e) =>
scrollWorkspace(workspace, e, ScrollDirection.LEFT),
keyCodes: [CTRL_CMD_LEFT],
allowCollision: true,
displayText: () => Msg['SHORTCUTS_SCROLL_LEFT'],
},
{
name: names.SCROLL_RIGHT,
preconditionFn: canScrollWorkspace,
callback: (workspace, e) =>
scrollWorkspace(workspace, e, ScrollDirection.RIGHT),
keyCodes: [CTRL_CMD_RIGHT],
allowCollision: true,
displayText: () => Msg['SHORTCUTS_SCROLL_RIGHT'],
},
{
name: names.SCROLL_UP,
preconditionFn: canScrollWorkspace,
callback: (workspace, e) =>
scrollWorkspace(workspace, e, ScrollDirection.UP),
keyCodes: [CTRL_CMD_UP],
allowCollision: true,
displayText: () => Msg['SHORTCUTS_SCROLL_UP'],
},
{
name: names.SCROLL_DOWN,
preconditionFn: canScrollWorkspace,
callback: (workspace, e) =>
scrollWorkspace(workspace, e, ScrollDirection.DOWN),
keyCodes: [CTRL_CMD_DOWN],
allowCollision: true,
displayText: () => Msg['SHORTCUTS_SCROLL_DOWN'],
},
];

for (const shortcut of shortcuts) {
ShortcutRegistry.registry.register(shortcut);
}
}

/**
* Registers all default keyboard shortcut item. This should be called once per
* instance of KeyboardShortcutRegistry.
Expand Down Expand Up @@ -1853,8 +2005,9 @@ export function registerScreenReaderShortcuts() {
}

/**
* Registers keyboard shortcuts used to jump between blocks and stacks in the workspace,
* and between items in the toolbox and flyout.
* Registers keyboard shortcuts used to jump between blocks and stacks in the
* workspace, between items in the toolbox and flyout, and to scroll the
* workspace or flyout.
* Note these are not registered by default, so call this function to enable them if desired.
*/
export function registerNavigationShortcuts() {
Expand All @@ -1866,6 +2019,7 @@ export function registerNavigationShortcuts() {
registerJumpLastBlock();
registerJumpPreviousPage();
registerJumpNextPage();
registerWorkspaceScrollShortcuts();
}

registerDefaultShortcuts();
Expand Down
9 changes: 9 additions & 0 deletions packages/blockly/msg/json/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,15 @@
"SHORTCUTS_JUMP_LAST_BLOCK": "Jump to last block",
"SHORTCUTS_JUMP_PREVIOUS_PAGE": "Jump to previous page",
"SHORTCUTS_JUMP_NEXT_PAGE": "Jump to next page",
"SHORTCUTS_SCROLL_LEFT": "Scroll left",
"SHORTCUTS_SCROLL_RIGHT": "Scroll right",
"SHORTCUTS_SCROLL_UP": "Scroll up",
"SHORTCUTS_SCROLL_DOWN": "Scroll down",
"ANNOUNCE_SCROLLED_LEFT": "Scrolled left.",
"ANNOUNCE_SCROLLED_RIGHT": "Scrolled right.",
"ANNOUNCE_SCROLLED_UP": "Scrolled up.",
"ANNOUNCE_SCROLLED_DOWN": "Scrolled down.",
"ANNOUNCE_CANT_SCROLL_FURTHER": "Can't scroll further.",
"KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT": "Hold %1 and use arrow keys to move freely, then %2 to accept the position.",
"KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Use the arrow keys to move, then %1 to accept the position.",
"KEYBOARD_NAV_COPIED_HINT": "Copied. Press %1 to paste.",
Expand Down
9 changes: 9 additions & 0 deletions packages/blockly/msg/json/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,15 @@
"SHORTCUTS_JUMP_LAST_BLOCK": "shortcut display text for a shortcut that jumps focus to the last block in the workspace.",
"SHORTCUTS_JUMP_PREVIOUS_PAGE": "shortcut display text for a shortcut that jumps focus back one page of items in the toolbox or flyout.",
"SHORTCUTS_JUMP_NEXT_PAGE": "shortcut display text for a shortcut that jumps focus forward one page of items in the toolbox or flyout.",
"SHORTCUTS_SCROLL_LEFT": "shortcut display text for a shortcut that scrolls the workspace or flyout to the left.",
"SHORTCUTS_SCROLL_RIGHT": "shortcut display text for a shortcut that scrolls the workspace or flyout to the right.",
"SHORTCUTS_SCROLL_UP": "shortcut display text for a shortcut that scrolls the workspace or flyout up.",
"SHORTCUTS_SCROLL_DOWN": "shortcut display text for a shortcut that scrolls the workspace or flyout down.",
"ANNOUNCE_SCROLLED_LEFT": "ARIA live region message announcing that the workspace or flyout scrolled left.",
"ANNOUNCE_SCROLLED_RIGHT": "ARIA live region message announcing that the workspace or flyout scrolled right.",
"ANNOUNCE_SCROLLED_UP": "ARIA live region message announcing that the workspace or flyout scrolled up.",
"ANNOUNCE_SCROLLED_DOWN": "ARIA live region message announcing that the workspace or flyout scrolled down.",
"ANNOUNCE_CANT_SCROLL_FURTHER": "ARIA live region message announcing that the workspace or flyout cannot scroll further in the requested direction.",
"KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT": "Message shown to inform users how to move blocks to arbitrary locations with the keyboard.",
"KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Message shown to inform users how to move blocks with the keyboard.",
"KEYBOARD_NAV_COPIED_HINT": "Message shown when an item is copied in keyboard navigation mode.",
Expand Down
27 changes: 27 additions & 0 deletions packages/blockly/msg/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -1923,6 +1923,33 @@ Blockly.Msg.SHORTCUTS_JUMP_PREVIOUS_PAGE = 'Jump to previous page';
/// shortcut display text for a shortcut that jumps focus forward one page of items in the toolbox or flyout.
Blockly.Msg.SHORTCUTS_JUMP_NEXT_PAGE = 'Jump to next page';
/** @type {string} */
/// shortcut display text for a shortcut that scrolls the workspace or flyout to the left.
Blockly.Msg.SHORTCUTS_SCROLL_LEFT = 'Scroll left';
/** @type {string} */
/// shortcut display text for a shortcut that scrolls the workspace or flyout to the right.
Blockly.Msg.SHORTCUTS_SCROLL_RIGHT = 'Scroll right';
/** @type {string} */
/// shortcut display text for a shortcut that scrolls the workspace or flyout up.
Blockly.Msg.SHORTCUTS_SCROLL_UP = 'Scroll up';
/** @type {string} */
/// shortcut display text for a shortcut that scrolls the workspace or flyout down.
Blockly.Msg.SHORTCUTS_SCROLL_DOWN = 'Scroll down';
/** @type {string} */
/// ARIA live region message announcing that the workspace or flyout scrolled left.
Blockly.Msg.ANNOUNCE_SCROLLED_LEFT = 'Scrolled left.';
/** @type {string} */
/// ARIA live region message announcing that the workspace or flyout scrolled right.
Blockly.Msg.ANNOUNCE_SCROLLED_RIGHT = 'Scrolled right.';
/** @type {string} */
/// ARIA live region message announcing that the workspace or flyout scrolled up.
Blockly.Msg.ANNOUNCE_SCROLLED_UP = 'Scrolled up.';
/** @type {string} */
/// ARIA live region message announcing that the workspace or flyout scrolled down.
Blockly.Msg.ANNOUNCE_SCROLLED_DOWN = 'Scrolled down.';
/** @type {string} */
/// ARIA live region message announcing that the workspace or flyout cannot scroll further in the requested direction.
Blockly.Msg.ANNOUNCE_CANT_SCROLL_FURTHER = 'Can\'t scroll further.';
/** @type {string} */
/// Message shown to inform users how to move blocks to arbitrary locations
/// with the keyboard.
Blockly.Msg.KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT = 'Hold %1 and use arrow keys to move freely, then %2 to accept the position.';
Expand Down
8 changes: 8 additions & 0 deletions packages/blockly/tests/mocha/keyboard_navigation_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,10 @@ suite('Toolbox and flyout jump shortcuts (Ctrl/Cmd + Home / End)', function () {
'jump_to_last_block',
'jump_to_previous_page',
'jump_to_next_page',
'scroll_left',
'scroll_right',
'scroll_up',
'scroll_down',
]) {
Blockly.ShortcutRegistry.registry.unregister(shortcut);
}
Expand Down Expand Up @@ -1249,6 +1253,10 @@ suite('Toolbox and flyout paging shortcuts (Page Up / Page Down)', function () {
'jump_to_last_block',
'jump_to_previous_page',
'jump_to_next_page',
'scroll_left',
'scroll_right',
'scroll_up',
'scroll_down',
]) {
Blockly.ShortcutRegistry.registry.unregister(shortcut);
}
Expand Down
Loading
Loading