-
Notifications
You must be signed in to change notification settings - Fork 3.8k
feat: i shortcut on workspace gives overview #9677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v13
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,9 @@ import {type IDraggable, isDraggable} from './interfaces/i_draggable.js'; | |
| import {type IFocusableNode} from './interfaces/i_focusable_node.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'; | ||
|
|
@@ -53,6 +55,7 @@ export enum names { | |
| NAVIGATE_UP = 'up', | ||
| NAVIGATE_DOWN = 'down', | ||
| DISCONNECT = 'disconnect', | ||
| INFORMATION = 'information', | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -635,20 +638,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(), | ||
|
|
@@ -689,6 +692,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 stacks = workspace.getTopBlocks().length; | ||
| const comments = workspace.getTopComments().length; | ||
|
|
||
| // Build base string with block stack count. | ||
| let baseMsgKey; | ||
| if (stacks === 0) { | ||
| baseMsgKey = 'ARIA_WORKSPACE_BLOCKS_ZERO'; | ||
| } else if (stacks === 1) { | ||
| baseMsgKey = 'ARIA_WORKSPACE_BLOCKS_ONE'; | ||
| } else { | ||
| baseMsgKey = 'ARIA_WORKSPACE_BLOCKS_MANY'; | ||
| } | ||
|
|
||
| // Build comment suffix. | ||
| let suffix = ''; | ||
| if (comments > 0) { | ||
| suffix = Msg[ | ||
| comments === 1 | ||
| ? 'ARIA_WORKSPACE_COMMENTS_ONE' | ||
| : 'ARIA_WORKSPACE_COMMENTS_MANY' | ||
| ].replace('%1', String(comments)); | ||
| } | ||
|
|
||
| // Build final message. | ||
| const msg = Msg[baseMsgKey] | ||
| .replace('%1', String(stacks)) | ||
| .replace('%2', suffix); | ||
|
|
||
| aria.announceDynamicAriaState(msg); | ||
|
|
||
| return true; | ||
| }, | ||
| keyCodes: [KeyCodes.I], | ||
| }; | ||
| ShortcutRegistry.registry.register(shortcut); | ||
| } | ||
|
|
||
| /** | ||
| * Registers keyboard shortcut to disconnect the focused block. | ||
| */ | ||
|
|
@@ -745,5 +797,13 @@ export function registerKeyboardNavigationShortcuts() { | |
| registerDisconnectBlock(); | ||
| } | ||
|
|
||
| /** | ||
| * Registers keyboard shortcuts used to announce screen reader information. | ||
| */ | ||
| export function registerScreenReaderShortcuts() { | ||
| registerWorkspaceOverview(); | ||
| } | ||
|
|
||
| registerDefaultShortcuts(); | ||
| registerKeyboardNavigationShortcuts(); | ||
| registerScreenReaderShortcuts(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we actually want to keep this out, for now. The idea is we'll use an optional plugin to enable these based on user prompting. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| { | ||
| "@metadata": { | ||
| "author": "Ellen Spertus <ellen.spertus@gmail.com>", | ||
| "lastupdated": "2026-02-12 13:23:33.999357", | ||
| "lastupdated": "2026-04-01 11:22:00.739918", | ||
| "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.", | ||
| "ARIA_WORKSPACE_BLOCKS_MANY": "%1 stacks of blocks%2 in workspace.", | ||
| "ARIA_WORKSPACE_BLOCKS_ONE": "One stack of blocks%2 in workspace.", | ||
| "ARIA_WORKSPACE_BLOCKS_ZERO": "No blocks%2 in workspace.", | ||
| "ARIA_WORKSPACE_COMMENTS_MANY": " and %1 comments", | ||
| "ARIA_WORKSPACE_COMMENTS_ONE": " and one comment" | ||
|
Comment on lines
+424
to
+428
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm finding this a bit hard to grok, and I suspect a translator might as well. Perhaps we could be slightly repetitive to simplify things. What if we avoided the double concatenation and had four different variables for the label:
Separately, I'm not sure about the |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1695,4 +1695,23 @@ 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 | ||
| Blockly.Msg.ARIA_WORKSPACE_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 comments. | ||
| /// \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space | ||
| Blockly.Msg.ARIA_WORKSPACE_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 comments. | ||
| /// \n\nParameters:\n* %2 - optional phrase announcing comments, including leading space | ||
| Blockly.Msg.ARIA_WORKSPACE_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.ARIA_WORKSPACE_COMMENTS_MANY = ' and %1 comments'; | ||
| /** @type {string} */ | ||
| /// ARIA live region phrase appended when there is exactly one workspace comment. | ||
| Blockly.Msg.ARIA_WORKSPACE_COMMENTS_ONE = ' and one comment'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: please re-add EOF line terminator. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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('Announces block stacks and comments', function () { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest splitting these up a bit. The Blockly team doesn't exactly follow the arrange-act-assert structure strictly in the codebase but it's a good baseline to follow particularly for longer tests like this as more granular failures can be more informative when investigating. |
||
| // Start with empty workspace. | ||
| Blockly.getFocusManager().focusNode(this.workspace); | ||
| this.assertAnnouncement('No blocks in workspace.'); | ||
|
|
||
| // Add one block. | ||
| const block1 = this.workspace.newBlock('stack_block'); | ||
| Blockly.getFocusManager().focusNode(this.workspace); | ||
| this.assertAnnouncement('One stack of blocks in workspace.'); | ||
|
|
||
| // Add second block. | ||
| const block2 = this.workspace.newBlock('stack_block'); | ||
| Blockly.getFocusManager().focusNode(this.workspace); | ||
| this.assertAnnouncement('2 stacks of blocks in workspace.'); | ||
|
|
||
| // Add one comment. | ||
| const comment1 = this.workspace.newComment(); | ||
| Blockly.getFocusManager().focusNode(this.workspace); | ||
| this.assertAnnouncement( | ||
| '2 stacks of blocks and one comment in workspace.', | ||
| ); | ||
|
|
||
| // Add second comment. | ||
| const comment2 = this.workspace.newComment(); | ||
| Blockly.getFocusManager().focusNode(this.workspace); | ||
| this.assertAnnouncement( | ||
| '2 stacks of blocks and 2 comments in workspace.', | ||
| ); | ||
|
|
||
| // Remove second block stack. | ||
| block2.dispose(); | ||
| Blockly.getFocusManager().focusNode(this.workspace); | ||
| this.assertAnnouncement( | ||
| 'One stack of blocks and 2 comments in workspace.', | ||
| ); | ||
|
|
||
| // Remove last block. | ||
| block1.dispose(); | ||
| Blockly.getFocusManager().focusNode(this.workspace); | ||
| this.assertAnnouncement('No blocks and 2 comments in workspace.'); | ||
|
|
||
| // Remove second comment. | ||
| comment2.dispose(); | ||
| Blockly.getFocusManager().focusNode(this.workspace); | ||
| this.assertAnnouncement('No blocks and one comment in workspace.'); | ||
|
|
||
| // Remove last comment. | ||
| comment1.dispose(); | ||
| Blockly.getFocusManager().focusNode(this.workspace); | ||
| this.assertAnnouncement('No blocks 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([ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Perhaps use
stackCountandcommentCountfor a bit more clarity on what's being contained in this variables (since they read a bit as arrays at the moment).