mirror of
https://github.com/google/blockly.git
synced 2026-04-26 07:00:23 +02:00
feat: i shortcut on workspace gives overview (#9677)
* feat: i shortcut on workspace gives overview * fix: code review changes
This commit is contained in:
@@ -20,7 +20,9 @@ import {type IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||
import {isSelectable} from './interfaces/i_selectable.js';
|
||||
import {Direction, KeyboardMover} from './keyboard_nav/keyboard_mover.js';
|
||||
import {keyboardNavigationController} from './keyboard_navigation_controller.js';
|
||||
import {Msg} from './msg.js';
|
||||
import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';
|
||||
import {aria} from './utils.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import {KeyCodes} from './utils/keycodes.js';
|
||||
import {Rect} from './utils/rect.js';
|
||||
@@ -56,6 +58,7 @@ export enum names {
|
||||
DISCONNECT = 'disconnect',
|
||||
NEXT_STACK = 'next_stack',
|
||||
PREVIOUS_STACK = 'previous_stack',
|
||||
INFORMATION = 'information',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -638,20 +641,20 @@ export function registerArrowNavigation() {
|
||||
}
|
||||
}
|
||||
|
||||
const resolveWorkspace = (workspace: WorkspaceSvg) => {
|
||||
if (workspace.isFlyout) {
|
||||
const target = workspace.targetWorkspace;
|
||||
if (target) {
|
||||
return resolveWorkspace(target);
|
||||
}
|
||||
}
|
||||
return workspace.getRootWorkspace() ?? workspace;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers keyboard shortcut to focus the workspace.
|
||||
*/
|
||||
export function registerFocusWorkspace() {
|
||||
const resolveWorkspace = (workspace: WorkspaceSvg) => {
|
||||
if (workspace.isFlyout) {
|
||||
const target = workspace.targetWorkspace;
|
||||
if (target) {
|
||||
return resolveWorkspace(target);
|
||||
}
|
||||
}
|
||||
return workspace.getRootWorkspace() ?? workspace;
|
||||
};
|
||||
|
||||
const focusWorkspaceShortcut: KeyboardShortcut = {
|
||||
name: names.FOCUS_WORKSPACE,
|
||||
preconditionFn: (workspace) => !workspace.isDragging(),
|
||||
@@ -692,6 +695,55 @@ export function registerFocusToolbox() {
|
||||
ShortcutRegistry.registry.register(focusToolboxShortcut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers keyboard shortcut to get count of block stacks and comments.
|
||||
*/
|
||||
export function registerWorkspaceOverview() {
|
||||
const shortcut: KeyboardShortcut = {
|
||||
name: names.INFORMATION,
|
||||
preconditionFn: (workspace, scope) => {
|
||||
const focused = scope.focusedNode;
|
||||
return focused === workspace;
|
||||
},
|
||||
callback: (_workspace) => {
|
||||
const workspace = resolveWorkspace(_workspace);
|
||||
const stackCount = workspace.getTopBlocks().length;
|
||||
const commentCount = workspace.getTopComments().length;
|
||||
|
||||
// Build base string with block stack count.
|
||||
let baseMsgKey;
|
||||
if (stackCount === 0) {
|
||||
baseMsgKey = 'WORKSPACE_CONTENTS_BLOCKS_ZERO';
|
||||
} else if (stackCount === 1) {
|
||||
baseMsgKey = 'WORKSPACE_CONTENTS_BLOCKS_ONE';
|
||||
} else {
|
||||
baseMsgKey = 'WORKSPACE_CONTENTS_BLOCKS_MANY';
|
||||
}
|
||||
|
||||
// Build comment suffix.
|
||||
let suffix = '';
|
||||
if (commentCount > 0) {
|
||||
suffix = Msg[
|
||||
commentCount === 1
|
||||
? 'WORKSPACE_CONTENTS_COMMENTS_ONE'
|
||||
: 'WORKSPACE_CONTENTS_COMMENTS_MANY'
|
||||
].replace('%1', String(commentCount));
|
||||
}
|
||||
|
||||
// Build final message.
|
||||
const msg = Msg[baseMsgKey]
|
||||
.replace('%1', String(stackCount))
|
||||
.replace('%2', suffix);
|
||||
|
||||
aria.announceDynamicAriaState(msg);
|
||||
|
||||
return true;
|
||||
},
|
||||
keyCodes: [KeyCodes.I],
|
||||
};
|
||||
ShortcutRegistry.registry.register(shortcut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers keyboard shortcut to disconnect the focused block.
|
||||
*/
|
||||
@@ -818,5 +870,13 @@ export function registerKeyboardNavigationShortcuts() {
|
||||
registerStackNavigation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers keyboard shortcuts used to announce screen reader information.
|
||||
*/
|
||||
export function registerScreenReaderShortcuts() {
|
||||
registerWorkspaceOverview();
|
||||
}
|
||||
|
||||
registerDefaultShortcuts();
|
||||
registerKeyboardNavigationShortcuts();
|
||||
registerScreenReaderShortcuts();
|
||||
|
||||
@@ -188,7 +188,6 @@ export function removeRole(element: Element) {
|
||||
*/
|
||||
export function setRole(element: Element, roleName: Role | null) {
|
||||
if (!roleName) {
|
||||
console.log('Removing role from element', element, roleName);
|
||||
removeRole(element);
|
||||
} else {
|
||||
element.setAttribute(ROLE_ATTRIBUTE, roleName);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"author": "Ellen Spertus <ellen.spertus@gmail.com>",
|
||||
"lastupdated": "2026-02-12 13:23:33.999357",
|
||||
"lastupdated": "2026-04-03 10:36:19.846436",
|
||||
"locale": "en",
|
||||
"messagedocumentation" : "qqq"
|
||||
},
|
||||
@@ -420,5 +420,10 @@
|
||||
"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.",
|
||||
"KEYBOARD_NAV_CUT_HINT": "Cut. Press %1 to paste."
|
||||
"KEYBOARD_NAV_CUT_HINT": "Cut. Press %1 to paste.",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_MANY": "%1 stacks of blocks%2 in workspace.",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_ONE": "One stack of blocks%2 in workspace.",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_ZERO": "No blocks%2 in workspace.",
|
||||
"WORKSPACE_CONTENTS_COMMENTS_MANY": " and %1 comments",
|
||||
"WORKSPACE_CONTENTS_COMMENTS_ONE": " and one comment"
|
||||
}
|
||||
|
||||
@@ -427,5 +427,10 @@
|
||||
"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.",
|
||||
"KEYBOARD_NAV_CUT_HINT": "Message shown when an item is cut in keyboard navigation mode."
|
||||
"KEYBOARD_NAV_CUT_HINT": "Message shown when an item is cut in keyboard navigation mode.",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_MANY": "ARIA live region message announcing the number of stacks of blocks in the workspace, optionally including comments. \n\nParameters:\n* %1 - the number of stacks (integer greater than 1)\n* %2 - optional phrase announcing comments, including leading space \n\nExamples:\n* '5 stacks of blocks in workspace.'\n* '5 stacks of blocks and 2 comments in workspace.'",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_ONE": "ARIA live region message announcing there is one stack of blocks in the workspace, optionally including a count of comments. \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space \n\nExamples:\n* 'One stack of blocks in workspace.'\n* 'One stack of blocks and 1 comment in workspace.'",
|
||||
"WORKSPACE_CONTENTS_BLOCKS_ZERO": "ARIA live region message announcing there are no blocks in the workspace, optionally including a count of comments. \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space \n\nExamples:\n* 'No blocks in workspace.'\n* 'No blocks and 3 comments in workspace.'",
|
||||
"WORKSPACE_CONTENTS_COMMENTS_MANY": "ARIA live region phrase appended when there are multiple workspace comments. \n\nParameters:\n* %1 - the number of comments (integer greater than 1)",
|
||||
"WORKSPACE_CONTENTS_COMMENTS_ONE": "ARIA live region phrase appended when there is exactly one workspace comment."
|
||||
}
|
||||
|
||||
@@ -1695,4 +1695,26 @@ Blockly.Msg.KEYBOARD_NAV_CONSTRAINED_MOVE_HINT = 'Use the arrow keys to move, th
|
||||
Blockly.Msg.KEYBOARD_NAV_COPIED_HINT = 'Copied. Press %1 to paste.';
|
||||
/** @type {string} */
|
||||
/// Message shown when an item is cut in keyboard navigation mode.
|
||||
Blockly.Msg.KEYBOARD_NAV_CUT_HINT = 'Cut. Press %1 to paste.';
|
||||
Blockly.Msg.KEYBOARD_NAV_CUT_HINT = 'Cut. Press %1 to paste.';
|
||||
/** @type {string} */
|
||||
/// ARIA live region message announcing the number of stacks of blocks in the workspace, optionally including comments.
|
||||
/// \n\nParameters:\n* %1 - the number of stacks (integer greater than 1)\n* %2 - optional phrase announcing comments, including leading space
|
||||
/// \n\nExamples:\n* "5 stacks of blocks in workspace."\n* "5 stacks of blocks and 2 comments in workspace."
|
||||
Blockly.Msg.WORKSPACE_CONTENTS_BLOCKS_MANY = '%1 stacks of blocks%2 in workspace.';
|
||||
/** @type {string} */
|
||||
/// ARIA live region message announcing there is one stack of blocks in the workspace, optionally including a count of comments.
|
||||
/// \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space
|
||||
/// \n\nExamples:\n* "One stack of blocks in workspace."\n* "One stack of blocks and 1 comment in workspace."
|
||||
Blockly.Msg.WORKSPACE_CONTENTS_BLOCKS_ONE = 'One stack of blocks%2 in workspace.';
|
||||
/** @type {string} */
|
||||
/// ARIA live region message announcing there are no blocks in the workspace, optionally including a count of comments.
|
||||
/// \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space
|
||||
/// \n\nExamples:\n* "No blocks in workspace."\n* "No blocks and 3 comments in workspace."
|
||||
Blockly.Msg.WORKSPACE_CONTENTS_BLOCKS_ZERO = 'No blocks%2 in workspace.';
|
||||
/** @type {string} */
|
||||
/// ARIA live region phrase appended when there are multiple workspace comments.
|
||||
/// \n\nParameters:\n* %1 - the number of comments (integer greater than 1)
|
||||
Blockly.Msg.WORKSPACE_CONTENTS_COMMENTS_MANY = ' and %1 comments';
|
||||
/** @type {string} */
|
||||
/// ARIA live region phrase appended when there is exactly one workspace comment.
|
||||
Blockly.Msg.WORKSPACE_CONTENTS_COMMENTS_ONE = ' and one comment';
|
||||
|
||||
@@ -552,6 +552,81 @@ suite('Keyboard Shortcut Items', function () {
|
||||
});
|
||||
});
|
||||
|
||||
suite('Workspace Information (I)', function () {
|
||||
setup(function () {
|
||||
const keyEvent = createKeyDownEvent(Blockly.utils.KeyCodes.I);
|
||||
// Helper to trigger the shortcut and assert the live region text.
|
||||
this.assertAnnouncement = (expected) => {
|
||||
this.injectionDiv.dispatchEvent(keyEvent);
|
||||
// Wait for the live region to update after the event.
|
||||
this.clock.tick(11);
|
||||
// The announcement may include an additional non-breaking space.
|
||||
assert.include(this.liveRegion.textContent, expected);
|
||||
};
|
||||
this.liveRegion = document.getElementById('blocklyAriaAnnounce');
|
||||
});
|
||||
|
||||
test('Empty workspace', function () {
|
||||
// Start with empty workspace.
|
||||
Blockly.getFocusManager().focusNode(this.workspace);
|
||||
this.assertAnnouncement('No blocks in workspace.');
|
||||
});
|
||||
|
||||
test('One block', function () {
|
||||
this.workspace.newBlock('stack_block');
|
||||
Blockly.getFocusManager().focusNode(this.workspace);
|
||||
this.assertAnnouncement('One stack of blocks in workspace.');
|
||||
});
|
||||
|
||||
test('Two blocks', function () {
|
||||
this.workspace.newBlock('stack_block');
|
||||
this.workspace.newBlock('stack_block');
|
||||
Blockly.getFocusManager().focusNode(this.workspace);
|
||||
this.assertAnnouncement('2 stacks of blocks in workspace.');
|
||||
});
|
||||
|
||||
test('One comment', function () {
|
||||
this.workspace.newComment();
|
||||
Blockly.getFocusManager().focusNode(this.workspace);
|
||||
this.assertAnnouncement('No blocks and one comment in workspace.');
|
||||
});
|
||||
|
||||
test('Two comments', function () {
|
||||
this.workspace.newComment();
|
||||
this.workspace.newComment();
|
||||
Blockly.getFocusManager().focusNode(this.workspace);
|
||||
this.assertAnnouncement('No blocks and 2 comments in workspace.');
|
||||
});
|
||||
|
||||
test('One block, one comment', function () {
|
||||
this.workspace.newBlock('stack_block');
|
||||
this.workspace.newComment();
|
||||
Blockly.getFocusManager().focusNode(this.workspace);
|
||||
this.assertAnnouncement(
|
||||
'One stack of blocks and one comment in workspace.',
|
||||
);
|
||||
});
|
||||
|
||||
test('Two blocks, two comments', function () {
|
||||
this.workspace.newBlock('stack_block');
|
||||
this.workspace.newBlock('stack_block');
|
||||
this.workspace.newComment();
|
||||
this.workspace.newComment();
|
||||
Blockly.getFocusManager().focusNode(this.workspace);
|
||||
this.assertAnnouncement(
|
||||
'2 stacks of blocks and 2 comments in workspace.',
|
||||
);
|
||||
});
|
||||
|
||||
suite('Preconditions', function () {
|
||||
test('Not called when focus is not on workspace', function () {
|
||||
this.block = this.workspace.newBlock('stack_block');
|
||||
Blockly.getFocusManager().focusNode(this.block);
|
||||
this.assertAnnouncement('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Focus Toolbox (T)', function () {
|
||||
setup(function () {
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
|
||||
Reference in New Issue
Block a user