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
158 changes: 123 additions & 35 deletions packages/blockly/core/shortcut_items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

// Former goog.module ID: Blockly.ShortcutItems

import {computeAriaLabel} from './block_aria_composer.js';
import {BlockSvg} from './block_svg.js';
import * as clipboard from './clipboard.js';
import {RenderedWorkspaceComment} from './comments.js';
Expand Down Expand Up @@ -63,6 +64,7 @@ export enum names {
NEXT_STACK = 'next_stack',
PREVIOUS_STACK = 'previous_stack',
INFORMATION = 'information',
EXTENDED_INFORMATION = 'extended_information',
PERFORM_ACTION = 'perform_action',
DUPLICATE = 'duplicate',
CLEANUP = 'cleanup',
Expand Down Expand Up @@ -762,51 +764,136 @@ export function registerFocusToolbox() {
}

/**
* Registers keyboard shortcut to get count of block stacks and comments.
* Registers keyboard shortcut to announce information about the focused
* element.
*/
export function registerWorkspaceOverview() {
export function registerReadInformation() {
const announceBlockInformation = (block: BlockSvg) => {
const description = computeAriaLabel(
block,
aria.Verbosity.LOQUACIOUS,
false,
);
aria.announceDynamicAriaState(description);
};

const announceWorkspaceInformation = (workspace: WorkspaceSvg) => {
const rootWorkspace = resolveWorkspace(workspace);
const stackCount = rootWorkspace.getTopBlocks().length;
const commentCount = rootWorkspace.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);
};

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';
preconditionFn: () => true,
callback: (workspace) => {
const focusedNode = getFocusManager().getFocusedNode();
const block = workspace
.getNavigator()
.getSourceBlockFromNode(focusedNode);
if (block) {
announceBlockInformation(block);
return true;
} else if (focusedNode === workspace) {
announceWorkspaceInformation(workspace);
return true;
}
return false;
},
keyCodes: [KeyCodes.I],
displayText: () => Msg['SHORTCUTS_INFORMATION'],
};
ShortcutRegistry.registry.register(shortcut);
}

// Build comment suffix.
let suffix = '';
if (commentCount > 0) {
suffix = Msg[
commentCount === 1
? 'WORKSPACE_CONTENTS_COMMENTS_ONE'
: 'WORKSPACE_CONTENTS_COMMENTS_MANY'
].replace('%1', String(commentCount));
/**
* Registers keyboard shortcut to announce an extended description of the
* focused element.
*/
export function registerReadExtendedInformation() {
const shiftI = ShortcutRegistry.registry.createSerializedKey(KeyCodes.I, [
KeyCodes.SHIFT,
]);
const shortcut: KeyboardShortcut = {
name: names.EXTENDED_INFORMATION,
preconditionFn: () => true,
callback: (workspace) => {
const block = workspace
.getNavigator()
.getSourceBlockFromNode(getFocusManager().getFocusedNode());
if (!block) return false;

const toAnnounce = [];
// First go up the chain of output connections and start finding parents
// from there because the outputs of a block are read anyway, so we don't
// need to repeat them.
let startBlock = block;
while (startBlock.outputConnection?.isConnected()) {
startBlock = startBlock.getParent()!;
}

// Build final message.
const msg = Msg[baseMsgKey]
.replace('%1', String(stackCount))
.replace('%2', suffix);
if (startBlock !== block) {
toAnnounce.push(
computeAriaLabel(startBlock, aria.Verbosity.TERSE, false),
);
}

aria.announceDynamicAriaState(msg);
let parent = startBlock.getParent();
while (parent) {
toAnnounce.push(computeAriaLabel(parent, aria.Verbosity.TERSE, false));
parent = parent.getParent();
}

if (toAnnounce.length) {
toAnnounce.reverse();
if (!block.outputConnection?.isConnected()) {
// The current block was already read out earlier if it has an output
// connection.
toAnnounce.push(
Msg['CURRENT_BLOCK_ANNOUNCEMENT'].replace(
'%1',
computeAriaLabel(block, aria.Verbosity.TERSE, false),
),
);
}

aria.announceDynamicAriaState(
Msg['PARENT_BLOCKS_ANNOUNCEMENT'].replace('%1', toAnnounce.join(',')),
);
} else {
aria.announceDynamicAriaState(Msg['NO_PARENT_ANNOUNCEMENT']);
}
return true;
},
keyCodes: [KeyCodes.I],
displayText: () => Msg['SHORTCUTS_INFORMATION'],
keyCodes: [shiftI],
displayText: () => Msg['SHORTCUTS_EXTENDED_INFORMATION'],
Comment thread
gonfunko marked this conversation as resolved.
};
ShortcutRegistry.registry.register(shortcut);
}
Expand Down Expand Up @@ -1058,7 +1145,8 @@ export function registerKeyboardNavigationShortcuts() {
* Registers keyboard shortcuts used to announce screen reader information.
*/
export function registerScreenReaderShortcuts() {
registerWorkspaceOverview();
registerReadInformation();
registerReadExtendedInformation();
}

registerDefaultShortcuts();
Expand Down
8 changes: 6 additions & 2 deletions packages/blockly/msg/json/en.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"@metadata": {
"author": "Ellen Spertus <ellen.spertus@gmail.com>",
"lastupdated": "2026-05-11 14:15:58.197621",
"lastupdated": "2026-05-13 09:26:53.422108",
"locale": "en",
"messagedocumentation" : "qqq"
},
Expand Down Expand Up @@ -444,6 +444,7 @@
"SHORTCUTS_FOCUS_WORKSPACE": "Focus workspace",
"SHORTCUTS_FOCUS_TOOLBOX": "Focus toolbox",
"SHORTCUTS_INFORMATION": "Announce information",
"SHORTCUTS_EXTENDED_INFORMATION": "Announce detailed information",
"SHORTCUTS_DISCONNECT": "Disconnect block",
"SHORTCUTS_NEXT_STACK": "Next stack",
"SHORTCUTS_PREVIOUS_STACK": "Previous stack",
Expand Down Expand Up @@ -517,5 +518,8 @@
"ICON_LABEL_WARNING_OPEN": "Close Warning",
"ARIA_LABEL_COMMENT": "Comment",
"ARIA_LABEL_COMMENT_COLLAPSE": "Collapse Comment",
"ARIA_LABEL_COMMENT_EXPAND": "Expand Comment"
"ARIA_LABEL_COMMENT_EXPAND": "Expand Comment",
"CURRENT_BLOCK_ANNOUNCEMENT": "Current block: %1",
"PARENT_BLOCKS_ANNOUNCEMENT": "Parent blocks: %1",
"NO_PARENT_ANNOUNCEMENT": "Current block has no parent"
}
20 changes: 5 additions & 15 deletions packages/blockly/msg/json/qqq.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,4 @@
{
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.

normally i manually undo this change but i vote we just keep it off and see what happens

"@metadata": {
"authors": [
"Ajeje Brazorf",
"Amire80",
"Espertus",
"Liuxinyu970226",
"McDutchie",
"Metalhead64",
"Nike",
"Robby",
"Shirayuki",
"YaronSh"
]
},
"VARIABLES_DEFAULT_NAME": "default name - A simple, general default name for a variable, preferably short. For more context, see [[Translating:Blockly#infrequent_message_types]].\n{{Identical|Item}}",
"UNNAMED_KEY": "default name - A simple, default name for an unnamed function or variable. Preferably indicates that the item is unnamed.",
"TODAY": "button text - Button that sets a calendar to today's date.\n{{Identical|Today}}",
Expand Down Expand Up @@ -452,6 +438,7 @@
"SHORTCUTS_FOCUS_WORKSPACE": "shortcut display text for the focus workspace shortcut, which moves focus to the workspace.",
"SHORTCUTS_FOCUS_TOOLBOX": "shortcut display text for the focus toolbox shortcut, which moves focus to the toolbox or flyout.",
"SHORTCUTS_INFORMATION": "shortcut display text for the information shortcut, which announces information about a focused element.",
"SHORTCUTS_EXTENDED_INFORMATION": "Description for the Shift-I keyboard shortcut that announces extended context about the currently focused element to screenreaders.",
"SHORTCUTS_DISCONNECT": "shortcut display text for the disconnect shortcut, which disconnects a block from its neighbor.",
"SHORTCUTS_NEXT_STACK": "shortcut display text for the next stack shortcut, which navigates to the next block stack.",
"SHORTCUTS_PREVIOUS_STACK": "shortcut display text for the previous stack shortcut, which navigates to the previous block stack.",
Expand Down Expand Up @@ -525,5 +512,8 @@
"ICON_LABEL_WARNING_OPEN": "Label for an icon, used by screen readers to identify an open warning. Clicking on the icon closes the warning's bubble.",
"ARIA_LABEL_COMMENT": "ARIA label for a comment.",
"ARIA_LABEL_COMMENT_COLLAPSE": "ARIA label for an expanded comment's collapse button.",
"ARIA_LABEL_COMMENT_EXPAND": "ARIA label for a collapsed comment's expand button."
"ARIA_LABEL_COMMENT_EXPAND": "ARIA label for a collapsed comment's expand button.",
"CURRENT_BLOCK_ANNOUNCEMENT": "Screenreader announcement providing context about the currently focused block.",
"PARENT_BLOCKS_ANNOUNCEMENT": "Screenreader announcement providing context about the currently focused block's parents.",
"NO_PARENT_ANNOUNCEMENT": "Screenreader announcement informing users that the currently focused block has no parent blocks."
}
14 changes: 13 additions & 1 deletion packages/blockly/msg/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,9 @@ Blockly.Msg.SHORTCUTS_FOCUS_TOOLBOX = 'Focus toolbox';
/// shortcut display text for the information shortcut, which announces information about a focused element.
Blockly.Msg.SHORTCUTS_INFORMATION = 'Announce information';
/** @type {string} */
/// Description for the Shift-I keyboard shortcut that announces extended context about the currently focused element to screenreaders.
Blockly.Msg.SHORTCUTS_EXTENDED_INFORMATION = 'Announce detailed information';
/** @type {string} */
/// shortcut display text for the disconnect shortcut, which disconnects a block from its neighbor.
Blockly.Msg.SHORTCUTS_DISCONNECT = 'Disconnect block';
/** @type {string} */
Expand Down Expand Up @@ -2046,4 +2049,13 @@ Blockly.Msg.ARIA_LABEL_COMMENT = 'Comment';
Blockly.Msg.ARIA_LABEL_COMMENT_COLLAPSE = 'Collapse Comment';
/** @type {string} */
/// ARIA label for a collapsed comment's expand button.
Blockly.Msg.ARIA_LABEL_COMMENT_EXPAND = 'Expand Comment';
Blockly.Msg.ARIA_LABEL_COMMENT_EXPAND = 'Expand Comment';
/** @type {string} */
/// Screenreader announcement providing context about the currently focused block.
Blockly.Msg.CURRENT_BLOCK_ANNOUNCEMENT = 'Current block: %1';
/** @type {string} */
/// Screenreader announcement providing context about the currently focused block's parents.
Blockly.Msg.PARENT_BLOCKS_ANNOUNCEMENT = 'Parent blocks: %1';
/** @type {string} */
/// Screenreader announcement informing users that the currently focused block has no parent blocks.
Blockly.Msg.NO_PARENT_ANNOUNCEMENT = 'Current block has no parent';
Loading