Skip to content

Commit b49fe1e

Browse files
authored
feat: i shortcut on workspace gives overview (#9677)
* feat: i shortcut on workspace gives overview * fix: code review changes
1 parent 34c265f commit b49fe1e

6 files changed

Lines changed: 181 additions & 15 deletions

File tree

packages/blockly/core/shortcut_items.ts

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import {type IFocusableNode} from './interfaces/i_focusable_node.js';
2020
import {isSelectable} from './interfaces/i_selectable.js';
2121
import {Direction, KeyboardMover} from './keyboard_nav/keyboard_mover.js';
2222
import {keyboardNavigationController} from './keyboard_navigation_controller.js';
23+
import {Msg} from './msg.js';
2324
import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';
25+
import {aria} from './utils.js';
2426
import {Coordinate} from './utils/coordinate.js';
2527
import {KeyCodes} from './utils/keycodes.js';
2628
import {Rect} from './utils/rect.js';
@@ -56,6 +58,7 @@ export enum names {
5658
DISCONNECT = 'disconnect',
5759
NEXT_STACK = 'next_stack',
5860
PREVIOUS_STACK = 'previous_stack',
61+
INFORMATION = 'information',
5962
}
6063

6164
/**
@@ -638,20 +641,20 @@ export function registerArrowNavigation() {
638641
}
639642
}
640643

644+
const resolveWorkspace = (workspace: WorkspaceSvg) => {
645+
if (workspace.isFlyout) {
646+
const target = workspace.targetWorkspace;
647+
if (target) {
648+
return resolveWorkspace(target);
649+
}
650+
}
651+
return workspace.getRootWorkspace() ?? workspace;
652+
};
653+
641654
/**
642655
* Registers keyboard shortcut to focus the workspace.
643656
*/
644657
export function registerFocusWorkspace() {
645-
const resolveWorkspace = (workspace: WorkspaceSvg) => {
646-
if (workspace.isFlyout) {
647-
const target = workspace.targetWorkspace;
648-
if (target) {
649-
return resolveWorkspace(target);
650-
}
651-
}
652-
return workspace.getRootWorkspace() ?? workspace;
653-
};
654-
655658
const focusWorkspaceShortcut: KeyboardShortcut = {
656659
name: names.FOCUS_WORKSPACE,
657660
preconditionFn: (workspace) => !workspace.isDragging(),
@@ -692,6 +695,55 @@ export function registerFocusToolbox() {
692695
ShortcutRegistry.registry.register(focusToolboxShortcut);
693696
}
694697

698+
/**
699+
* Registers keyboard shortcut to get count of block stacks and comments.
700+
*/
701+
export function registerWorkspaceOverview() {
702+
const shortcut: KeyboardShortcut = {
703+
name: names.INFORMATION,
704+
preconditionFn: (workspace, scope) => {
705+
const focused = scope.focusedNode;
706+
return focused === workspace;
707+
},
708+
callback: (_workspace) => {
709+
const workspace = resolveWorkspace(_workspace);
710+
const stackCount = workspace.getTopBlocks().length;
711+
const commentCount = workspace.getTopComments().length;
712+
713+
// Build base string with block stack count.
714+
let baseMsgKey;
715+
if (stackCount === 0) {
716+
baseMsgKey = 'WORKSPACE_CONTENTS_BLOCKS_ZERO';
717+
} else if (stackCount === 1) {
718+
baseMsgKey = 'WORKSPACE_CONTENTS_BLOCKS_ONE';
719+
} else {
720+
baseMsgKey = 'WORKSPACE_CONTENTS_BLOCKS_MANY';
721+
}
722+
723+
// Build comment suffix.
724+
let suffix = '';
725+
if (commentCount > 0) {
726+
suffix = Msg[
727+
commentCount === 1
728+
? 'WORKSPACE_CONTENTS_COMMENTS_ONE'
729+
: 'WORKSPACE_CONTENTS_COMMENTS_MANY'
730+
].replace('%1', String(commentCount));
731+
}
732+
733+
// Build final message.
734+
const msg = Msg[baseMsgKey]
735+
.replace('%1', String(stackCount))
736+
.replace('%2', suffix);
737+
738+
aria.announceDynamicAriaState(msg);
739+
740+
return true;
741+
},
742+
keyCodes: [KeyCodes.I],
743+
};
744+
ShortcutRegistry.registry.register(shortcut);
745+
}
746+
695747
/**
696748
* Registers keyboard shortcut to disconnect the focused block.
697749
*/
@@ -818,5 +870,13 @@ export function registerKeyboardNavigationShortcuts() {
818870
registerStackNavigation();
819871
}
820872

873+
/**
874+
* Registers keyboard shortcuts used to announce screen reader information.
875+
*/
876+
export function registerScreenReaderShortcuts() {
877+
registerWorkspaceOverview();
878+
}
879+
821880
registerDefaultShortcuts();
822881
registerKeyboardNavigationShortcuts();
882+
registerScreenReaderShortcuts();

packages/blockly/core/utils/aria.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ export function removeRole(element: Element) {
188188
*/
189189
export function setRole(element: Element, roleName: Role | null) {
190190
if (!roleName) {
191-
console.log('Removing role from element', element, roleName);
192191
removeRole(element);
193192
} else {
194193
element.setAttribute(ROLE_ATTRIBUTE, roleName);

packages/blockly/msg/json/en.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"@metadata": {
33
"author": "Ellen Spertus <ellen.spertus@gmail.com>",
4-
"lastupdated": "2026-02-12 13:23:33.999357",
4+
"lastupdated": "2026-04-03 10:36:19.846436",
55
"locale": "en",
66
"messagedocumentation" : "qqq"
77
},
@@ -420,5 +420,10 @@
420420
"KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT": "Hold %1 and use arrow keys to move freely, then %2 to accept the position",
421421
"KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Use the arrow keys to move, then %1 to accept the position",
422422
"KEYBOARD_NAV_COPIED_HINT": "Copied. Press %1 to paste.",
423-
"KEYBOARD_NAV_CUT_HINT": "Cut. Press %1 to paste."
423+
"KEYBOARD_NAV_CUT_HINT": "Cut. Press %1 to paste.",
424+
"WORKSPACE_CONTENTS_BLOCKS_MANY": "%1 stacks of blocks%2 in workspace.",
425+
"WORKSPACE_CONTENTS_BLOCKS_ONE": "One stack of blocks%2 in workspace.",
426+
"WORKSPACE_CONTENTS_BLOCKS_ZERO": "No blocks%2 in workspace.",
427+
"WORKSPACE_CONTENTS_COMMENTS_MANY": " and %1 comments",
428+
"WORKSPACE_CONTENTS_COMMENTS_ONE": " and one comment"
424429
}

packages/blockly/msg/json/qqq.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,5 +427,10 @@
427427
"KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT": "Message shown to inform users how to move blocks to arbitrary locations with the keyboard.",
428428
"KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Message shown to inform users how to move blocks with the keyboard.",
429429
"KEYBOARD_NAV_COPIED_HINT": "Message shown when an item is copied in keyboard navigation mode.",
430-
"KEYBOARD_NAV_CUT_HINT": "Message shown when an item is cut in keyboard navigation mode."
430+
"KEYBOARD_NAV_CUT_HINT": "Message shown when an item is cut in keyboard navigation mode.",
431+
"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.'",
432+
"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.'",
433+
"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.'",
434+
"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)",
435+
"WORKSPACE_CONTENTS_COMMENTS_ONE": "ARIA live region phrase appended when there is exactly one workspace comment."
431436
}

packages/blockly/msg/messages.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1695,4 +1695,26 @@ Blockly.Msg.KEYBOARD_NAV_CONSTRAINED_MOVE_HINT = 'Use the arrow keys to move, th
16951695
Blockly.Msg.KEYBOARD_NAV_COPIED_HINT = 'Copied. Press %1 to paste.';
16961696
/** @type {string} */
16971697
/// Message shown when an item is cut in keyboard navigation mode.
1698-
Blockly.Msg.KEYBOARD_NAV_CUT_HINT = 'Cut. Press %1 to paste.';
1698+
Blockly.Msg.KEYBOARD_NAV_CUT_HINT = 'Cut. Press %1 to paste.';
1699+
/** @type {string} */
1700+
/// ARIA live region message announcing the number of stacks of blocks in the workspace, optionally including comments.
1701+
/// \n\nParameters:\n* %1 - the number of stacks (integer greater than 1)\n* %2 - optional phrase announcing comments, including leading space
1702+
/// \n\nExamples:\n* "5 stacks of blocks in workspace."\n* "5 stacks of blocks and 2 comments in workspace."
1703+
Blockly.Msg.WORKSPACE_CONTENTS_BLOCKS_MANY = '%1 stacks of blocks%2 in workspace.';
1704+
/** @type {string} */
1705+
/// ARIA live region message announcing there is one stack of blocks in the workspace, optionally including a count of comments.
1706+
/// \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space
1707+
/// \n\nExamples:\n* "One stack of blocks in workspace."\n* "One stack of blocks and 1 comment in workspace."
1708+
Blockly.Msg.WORKSPACE_CONTENTS_BLOCKS_ONE = 'One stack of blocks%2 in workspace.';
1709+
/** @type {string} */
1710+
/// ARIA live region message announcing there are no blocks in the workspace, optionally including a count of comments.
1711+
/// \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space
1712+
/// \n\nExamples:\n* "No blocks in workspace."\n* "No blocks and 3 comments in workspace."
1713+
Blockly.Msg.WORKSPACE_CONTENTS_BLOCKS_ZERO = 'No blocks%2 in workspace.';
1714+
/** @type {string} */
1715+
/// ARIA live region phrase appended when there are multiple workspace comments.
1716+
/// \n\nParameters:\n* %1 - the number of comments (integer greater than 1)
1717+
Blockly.Msg.WORKSPACE_CONTENTS_COMMENTS_MANY = ' and %1 comments';
1718+
/** @type {string} */
1719+
/// ARIA live region phrase appended when there is exactly one workspace comment.
1720+
Blockly.Msg.WORKSPACE_CONTENTS_COMMENTS_ONE = ' and one comment';

packages/blockly/tests/mocha/shortcut_items_test.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,81 @@ suite('Keyboard Shortcut Items', function () {
552552
});
553553
});
554554

555+
suite('Workspace Information (I)', function () {
556+
setup(function () {
557+
const keyEvent = createKeyDownEvent(Blockly.utils.KeyCodes.I);
558+
// Helper to trigger the shortcut and assert the live region text.
559+
this.assertAnnouncement = (expected) => {
560+
this.injectionDiv.dispatchEvent(keyEvent);
561+
// Wait for the live region to update after the event.
562+
this.clock.tick(11);
563+
// The announcement may include an additional non-breaking space.
564+
assert.include(this.liveRegion.textContent, expected);
565+
};
566+
this.liveRegion = document.getElementById('blocklyAriaAnnounce');
567+
});
568+
569+
test('Empty workspace', function () {
570+
// Start with empty workspace.
571+
Blockly.getFocusManager().focusNode(this.workspace);
572+
this.assertAnnouncement('No blocks in workspace.');
573+
});
574+
575+
test('One block', function () {
576+
this.workspace.newBlock('stack_block');
577+
Blockly.getFocusManager().focusNode(this.workspace);
578+
this.assertAnnouncement('One stack of blocks in workspace.');
579+
});
580+
581+
test('Two blocks', function () {
582+
this.workspace.newBlock('stack_block');
583+
this.workspace.newBlock('stack_block');
584+
Blockly.getFocusManager().focusNode(this.workspace);
585+
this.assertAnnouncement('2 stacks of blocks in workspace.');
586+
});
587+
588+
test('One comment', function () {
589+
this.workspace.newComment();
590+
Blockly.getFocusManager().focusNode(this.workspace);
591+
this.assertAnnouncement('No blocks and one comment in workspace.');
592+
});
593+
594+
test('Two comments', function () {
595+
this.workspace.newComment();
596+
this.workspace.newComment();
597+
Blockly.getFocusManager().focusNode(this.workspace);
598+
this.assertAnnouncement('No blocks and 2 comments in workspace.');
599+
});
600+
601+
test('One block, one comment', function () {
602+
this.workspace.newBlock('stack_block');
603+
this.workspace.newComment();
604+
Blockly.getFocusManager().focusNode(this.workspace);
605+
this.assertAnnouncement(
606+
'One stack of blocks and one comment in workspace.',
607+
);
608+
});
609+
610+
test('Two blocks, two comments', function () {
611+
this.workspace.newBlock('stack_block');
612+
this.workspace.newBlock('stack_block');
613+
this.workspace.newComment();
614+
this.workspace.newComment();
615+
Blockly.getFocusManager().focusNode(this.workspace);
616+
this.assertAnnouncement(
617+
'2 stacks of blocks and 2 comments in workspace.',
618+
);
619+
});
620+
621+
suite('Preconditions', function () {
622+
test('Not called when focus is not on workspace', function () {
623+
this.block = this.workspace.newBlock('stack_block');
624+
Blockly.getFocusManager().focusNode(this.block);
625+
this.assertAnnouncement('');
626+
});
627+
});
628+
});
629+
555630
suite('Focus Toolbox (T)', function () {
556631
setup(function () {
557632
Blockly.defineBlocksWithJsonArray([

0 commit comments

Comments
 (0)